summaryrefslogtreecommitdiff
path: root/src/json.c
diff options
context:
space:
mode:
authorcashmere <cashmere@autistici.org>2026-09-04 09:05:24 +0200
committercashmere <cashmere@autistici.org>2026-09-04 09:05:24 +0200
commit4f49b16079b27d7ce633619e4cddfdf2bffbc952 (patch)
treec296c1e7f3c3e2f6859c6d3a1b1b208779b89679 /src/json.c
downloadrelic-4f49b16079b27d7ce633619e4cddfdf2bffbc952.tar.gz
initial commitHEADmaster
Diffstat (limited to 'src/json.c')
-rw-r--r--src/json.c2445
1 files changed, 2445 insertions, 0 deletions
diff --git a/src/json.c b/src/json.c
new file mode 100644
index 0000000..ced1229
--- /dev/null
+++ b/src/json.c
@@ -0,0 +1,2445 @@
+#ifdef FOSSIL_ENABLE_JSON
+/*
+** Copyright (c) 2011-2022 D. Richard Hipp
+**
+** This program is free software; you can redistribute it and/or
+** modify it under the terms of the Simplified BSD License (also
+** known as the "2-Clause License" or "FreeBSD License".)
+
+** This program is distributed in the hope that it will be useful,
+** but without any warranty; without even the implied warranty of
+** merchantability or fitness for a particular purpose.
+**
+** Author contact information:
+** drh@hwaci.com
+** http://www.hwaci.com/drh/
+**
+*******************************************************************************
+**
+** Code for the JSON API.
+**
+** The JSON API's public interface is documented at:
+**
+** https://fossil-scm.org/fossil/doc/trunk/www/json-api/index.md
+**
+** Notes for hackers...
+**
+** Here's how command/page dispatching works: json_page_top() (in HTTP mode) or
+** json_cmd_top() (in CLI mode) catch the "json" path/command. Those functions
+** then dispatch to a JSON-mode-specific command/page handler with the type
+** fossil_json_f().
+** See the API docs for that typedef (below) for the semantics of the callbacks.
+**
+**
+*/
+#include "VERSION.h"
+#include "config.h"
+#include "json.h"
+#include <assert.h>
+#include <time.h>
+
+#if INTERFACE
+#include "json_detail.h" /* workaround for apparent enum limitation
+ in makeheaders */
+#endif
+
+const FossilJsonKeys_ FossilJsonKeys = {
+ "anonymousSeed" /*anonymousSeed*/,
+ "authToken" /*authToken*/,
+ "COMMAND_PATH" /*commandPath*/,
+ "mtime" /*mtime*/,
+ "payload" /* payload */,
+ "requestId" /*requestId*/,
+ "resultCode" /*resultCode*/,
+ "resultText" /*resultText*/,
+ "timestamp" /*timestamp*/
+};
+
+/*
+** Given the current request path string, this function returns true
+** if it refers to a JSON API path. i.e. if (1) it starts with /json
+** or (2) g.zCmdName is "server" or "cgi" and the path starts with
+** /somereponame/json. Specifically, it returns 1 in the former case
+** and 2 for the latter.
+*/
+int json_request_is_json_api(const char * zPathInfo){
+ int rc = 0;
+ if(zPathInfo==0){
+ rc = 0;
+ }else if(0==strncmp("/json",zPathInfo,5)
+ && (zPathInfo[5]==0 || zPathInfo[5]=='/')){
+ rc = 1;
+ }else if(g.zCmdName!=0 && (0==strcmp("server",g.zCmdName)
+ || 0==strcmp("ui",g.zCmdName)
+ || 0==strcmp("cgi",g.zCmdName)
+ || 0==strcmp("http",g.zCmdName)) ){
+ /* When running in server/cgi "directory" mode, zPathInfo is
+ ** prefixed with the repository's name, so in order to determine
+ ** whether or not we're really running in json mode we have to try
+ ** a bit harder. Problem reported here:
+ ** https://fossil-scm.org/forum/forumpost/e4953666d6
+ */
+ ReCompiled * pReg = 0;
+ const char * zErr = fossil_re_compile(&pReg, "^/[^/]+/json(/.*)?", 0);
+ assert(zErr==0 && "Regex compilation failed?");
+ if(zErr==0 &&
+ re_match(pReg, (const unsigned char *)zPathInfo, -1)){
+ rc = 2;
+ }
+ re_free(pReg);
+ }
+ return rc;
+}
+
+/*
+** Returns true (non-0) if fossil appears to be running in JSON mode.
+** and either has JSON POSTed input awaiting consumption or fossil is
+** running in HTTP mode (in which case certain JSON data *might* be
+** available via GET parameters).
+*/
+int fossil_has_json(){
+ return g.json.isJsonMode && (g.isHTTP || g.json.post.o);
+}
+
+/*
+** Placeholder /json/XXX page impl for NYI (Not Yet Implemented)
+** (but planned) pages/commands.
+*/
+cson_value * json_page_nyi(){
+ g.json.resultCode = FSL_JSON_E_NYI;
+ return NULL;
+}
+
+/*
+** Given a FossilJsonCodes value, it returns a string suitable for use
+** as a resultCode string. Returns some unspecified non-empty string
+** if errCode is not one of the FossilJsonCodes values.
+*/
+static char const * json_err_cstr( int errCode ){
+ switch( errCode ){
+ case 0: return "Success";
+#define C(X,V) case FSL_JSON_E_ ## X: return V
+
+ C(GENERIC,"Generic error");
+ C(INVALID_REQUEST,"Invalid request");
+ C(UNKNOWN_COMMAND,"Unknown command or subcommand");
+ C(UNKNOWN,"Unknown error");
+ C(TIMEOUT,"Timeout reached");
+ C(ASSERT,"Assertion failed");
+ C(ALLOC,"Resource allocation failed");
+ C(NYI,"Not yet implemented");
+ C(PANIC,"x");
+ C(MANIFEST_READ_FAILED,"Reading artifact manifest failed");
+ C(FILE_OPEN_FAILED,"Opening file failed");
+
+ C(AUTH,"Authentication error");
+ C(MISSING_AUTH,"Authentication info missing from request");
+ C(DENIED,"Access denied");
+ C(WRONG_MODE,"Request not allowed (wrong operation mode)");
+ C(LOGIN_FAILED,"Login failed");
+ C(LOGIN_FAILED_NOSEED,"Anonymous login attempt was missing password seed");
+ C(LOGIN_FAILED_NONAME,"Login failed - name not supplied");
+ C(LOGIN_FAILED_NOPW,"Login failed - password not supplied");
+ C(LOGIN_FAILED_NOTFOUND,"Login failed - no match found");
+
+ C(USAGE,"Usage error");
+ C(INVALID_ARGS,"Invalid argument(s)");
+ C(MISSING_ARGS,"Missing argument(s)");
+ C(AMBIGUOUS_UUID,"Resource identifier is ambiguous");
+ C(UNRESOLVED_UUID,"Provided uuid/tag/branch could not be resolved");
+ C(RESOURCE_ALREADY_EXISTS,"Resource already exists");
+ C(RESOURCE_NOT_FOUND,"Resource not found");
+
+ C(DB,"Database error");
+ C(STMT_PREP,"Statement preparation failed");
+ C(STMT_BIND,"Statement parameter binding failed");
+ C(STMT_EXEC,"Statement execution/stepping failed");
+ C(DB_LOCKED,"Database is locked");
+ C(DB_NEEDS_REBUILD,"Fossil repository needs to be rebuilt");
+ C(DB_NOT_FOUND,"Fossil repository db file could not be found.");
+ C(DB_NOT_VALID, "Fossil repository db file is not valid.");
+ C(DB_NEEDS_CHECKOUT, "Command requires a local check-out.");
+#undef C
+ default:
+ return "Unknown Error";
+ }
+}
+
+/*
+** Implements the cson_data_dest_f() interface and outputs the data to
+** a fossil Blob object. pState must be-a initialized (Blob*), to
+** which n bytes of src will be appended.
+**/
+int cson_data_dest_Blob(void * pState, void const * src, unsigned int n){
+ Blob * b = (Blob*)pState;
+ blob_append( b, (char const *)src, (int)n ) /* will die on OOM */;
+ return 0;
+}
+
+/*
+** Convenience wrapper around cson_output() which appends the output
+** to pDest. pOpt may be NULL, in which case g.json.outOpt will be used.
+*/
+int cson_output_Blob( cson_value const * pVal, Blob * pDest,
+ cson_output_opt const * pOpt ){
+ return cson_output( pVal, cson_data_dest_Blob,
+ pDest, pOpt ? pOpt : &g.json.outOpt );
+}
+
+/*
+** Convenience wrapper around cson_parse() which reads its input
+** from pSrc. pSrc is rewound before parsing.
+**
+** pInfo may be NULL. If it is not NULL then it will contain details
+** about the parse state when this function returns.
+**
+** On success a new JSON Object or Array is returned (owned by the
+** caller). On error NULL is returned.
+*/
+cson_value * cson_parse_Blob( Blob * pSrc, cson_parse_info * pInfo ){
+ cson_value * root = NULL;
+ cson_parse_string( &root, blob_str(pSrc), blob_size(pSrc), NULL, pInfo );
+ return root;
+}
+
+/*
+** Implements the cson_data_dest_f() interface and outputs the data to
+** cgi_append_content(). pState is ignored.
+**/
+int cson_data_dest_cgi(void * pState, void const * src, unsigned int n){
+ cgi_append_content( (char const *)src, (int)n );
+ return 0;
+}
+
+/*
+** Returns a string in the form FOSSIL-XXXX, where XXXX is a
+** left-zero-padded value of code. The returned buffer is static, and
+** must be copied if needed for later. The returned value will always
+** be 11 bytes long (not including the trailing NUL byte).
+**
+** In practice we will only ever call this one time per app execution
+** when constructing the JSON response envelope, so the static buffer
+** "shouldn't" be a problem.
+**
+*/
+char const * json_rc_cstr( int code ){
+ enum { BufSize = 13 };
+ static char buf[BufSize] = {'F','O','S','S','I','L','-',0};
+ assert((code >= 1000) && (code <= 9999) && "Invalid Fossil/JSON code.");
+ sqlite3_snprintf((int)BufSize, buf+7,"%04d", code);
+ return buf;
+}
+
+/*
+** Adds v to the API-internal cleanup mechanism. key is ignored
+** (legacy) but might be re-introduced and "should" be a unique
+** (app-wide) value. Failure to insert an item may be caused by any
+** of the following:
+**
+** - Allocation error.
+** - g.json.gc.a is NULL
+** - key is NULL or empty.
+**
+** Returns 0 on success.
+**
+** Ownership of v is transferred to (or shared with) g.json.gc, and v
+** will be valid until that object is cleaned up or some internal code
+** incorrectly removes it from the gc (which we never do). If this
+** function fails, it is fatal to the app (as it indicates an
+** allocation error (more likely than not) or a serious internal error
+** such as numeric overflow).
+*/
+void json_gc_add( char const * key, cson_value * v ){
+ int const rc = cson_array_append( g.json.gc.a, v );
+
+ assert( NULL != g.json.gc.a );
+ if( 0 != rc ){
+ cson_value_free( v );
+ }
+ assert( (0==rc) && "Adding item to GC failed." );
+ if(0!=rc){
+ fprintf(stderr,"%s: FATAL: alloc error.\n", g.argv[0])
+ /* reminder: allocation error is the only reasonable cause of
+ error here, provided g.json.gc.a and v are not NULL.
+ */
+ ;
+ fossil_exit(1)/*not fossil_panic() b/c it might land us somewhere
+ where this function is called again.
+ */;
+ }
+}
+
+
+/*
+** Returns the value of json_rc_cstr(code) as a new JSON
+** string, which is owned by the caller and must eventually
+** be cson_value_free()d or transferred to a JSON container.
+*/
+cson_value * json_rc_string( int code ){
+ return cson_value_new_string( json_rc_cstr(code), 11 );
+}
+
+cson_value * json_new_string( char const * str ){
+ return str
+ ? cson_value_new_string(str,strlen(str))
+ : NULL;
+}
+
+cson_value * json_new_string_f( char const * fmt, ... ){
+ cson_value * v;
+ char * zStr;
+ va_list vargs;
+ va_start(vargs,fmt);
+ zStr = vmprintf(fmt,vargs);
+ va_end(vargs);
+ v = cson_value_new_string(zStr, strlen(zStr));
+ fossil_free(zStr);
+ return v;
+}
+
+cson_value * json_new_int( i64 v ){
+ return cson_value_new_integer((cson_int_t)v);
+}
+
+/*
+** Gets a POST/POST.payload/GET/COOKIE/ENV value. The returned memory
+** is owned by the g.json object (one of its sub-objects). Returns
+** NULL if no match is found.
+**
+** ENV means the system environment (getenv()).
+**
+** Precedence: POST.payload, GET/COOKIE/non-JSON POST, JSON POST, ENV.
+**
+** FIXME: the precedence SHOULD be: GET, POST.payload, POST, COOKIE,
+** ENV, but the amalgamation of the GET/POST vars makes it effectively
+** impossible to do that. Since fossil only uses one cookie, cookie
+** precedence isn't a real/high-priority problem.
+*/
+cson_value * json_getenv( char const * zKey ){
+ cson_value * rc;
+ rc = g.json.reqPayload.o
+ ? cson_object_get( g.json.reqPayload.o, zKey )
+ : NULL;
+ if(rc){
+ return rc;
+ }
+ rc = cson_object_get( g.json.param.o, zKey );
+ if( rc ){
+ return rc;
+ }
+ rc = cson_object_get( g.json.post.o, zKey );
+ if(rc){
+ return rc;
+ }else{
+ char const * cv = PD(zKey,NULL);
+ if(!cv && !g.isHTTP){
+ /* reminder to self: in CLI mode i'd like to try
+ find_option(zKey,NULL,XYZ) here, but we don't have a sane
+ default for the XYZ param here.
+ */
+ cv = fossil_getenv(zKey);
+ }
+ if(cv){/*transform it to JSON for later use.*/
+ /* use sscanf() to figure out if it's an int,
+ and transform it to JSON int if it is.
+
+ FIXME: use strtol(), since it has more accurate
+ error handling.
+ */
+ int intVal = -1;
+ char endOfIntCheck;
+ int const scanRc = sscanf(cv,"%d%c",&intVal, &endOfIntCheck)
+ /* The %c bit there is to make sure that we don't accept 123x
+ as a number. sscanf() returns the number of tokens
+ successfully parsed, so an RC of 1 will be correct for "123"
+ but "123x" will have RC==2.
+
+ But it appears to not be working that way :/
+ */
+ ;
+ if(1==scanRc){
+ json_setenv( zKey, cson_value_new_integer(intVal) );
+ }else{
+ rc = cson_value_new_string(cv,strlen(cv));
+ json_setenv( zKey, rc );
+ }
+ return rc;
+ }else{
+ return NULL;
+ }
+ }
+}
+
+/*
+** Wrapper around json_getenv() which...
+**
+** If it finds a value and that value is-a JSON number or is a string
+** which looks like an integer or is-a JSON bool/null then it is
+** converted to an int. If none of those apply then dflt is returned.
+*/
+int json_getenv_int(char const * pKey, int dflt ){
+ cson_value const * v = json_getenv(pKey);
+ const cson_type_id type = v ? cson_value_type_id(v) : CSON_TYPE_UNDEF;
+ switch(type){
+ case CSON_TYPE_INTEGER:
+ case CSON_TYPE_DOUBLE:
+ return (int)cson_value_get_integer(v);
+ case CSON_TYPE_STRING: {
+ char const * sv = cson_string_cstr(cson_value_get_string(v));
+ assert( (NULL!=sv) && "This is quite unexpected." );
+ return sv ? atoi(sv) : dflt;
+ }
+ case CSON_TYPE_BOOL:
+ return cson_value_get_bool(v) ? 1 : 0;
+ case CSON_TYPE_NULL:
+ return 0;
+ default:
+ return dflt;
+ }
+}
+
+
+/*
+** Wrapper around json_getenv() which tries to evaluate a payload/env
+** value as a boolean. Uses mostly the same logic as
+** json_getenv_int(), with the addition that string values which
+** either start with a digit 1..9 or the letters [tTyY] are considered
+** to be true. If this function cannot find a matching key/value then
+** dflt is returned. e.g. if it finds the key but the value is-a
+** Object then dftl is returned.
+**
+** If an entry is found, this function guarantees that it will return
+** either 0 or 1, as opposed to "0 or non-zero", so that clients can
+** pass a different value as dflt. Thus they can use, e.g. -1 to know
+** whether or not this function found a match (it will return -1 in
+** that case).
+*/
+int json_getenv_bool(char const * pKey, int dflt ){
+ cson_value const * v = json_getenv(pKey);
+ const cson_type_id type = v ? cson_value_type_id(v) : CSON_TYPE_UNDEF;
+ switch(type){
+ case CSON_TYPE_INTEGER:
+ case CSON_TYPE_DOUBLE:
+ return cson_value_get_integer(v) ? 1 : 0;
+ case CSON_TYPE_STRING: {
+ char const * sv = cson_string_cstr(cson_value_get_string(v));
+ assert( (NULL!=sv) && "This is quite unexpected." );
+ if(!*sv || ('0'==*sv)){
+ return 0;
+ }else{
+ return ((('1'<=*sv) && ('9'>=*sv))
+ || ('t'==*sv) || ('T'==*sv)
+ || ('y'==*sv) || ('Y'==*sv)
+ )
+ ? 1 : 0;
+ }
+ }
+ case CSON_TYPE_BOOL:
+ return cson_value_get_bool(v) ? 1 : 0;
+ case CSON_TYPE_NULL:
+ return 0;
+ default:
+ return dflt;
+ }
+}
+
+/*
+** Returns the string form of a json_getenv() value, but ONLY If that
+** value is-a String. Non-strings are not converted to strings for
+** this purpose. Returned memory is owned by g.json or fossil and is
+** valid until end-of-app or the given key is replaced in fossil's
+** internals via cgi_replace_parameter() and friends or json_setenv().
+*/
+char const * json_getenv_cstr( char const * zKey ){
+ return cson_value_get_cstr( json_getenv(zKey) );
+}
+
+/*
+** An extended form of find_option() which tries to look up a combo
+** GET/POST/CLI argument.
+**
+** zKey must be the GET/POST parameter key. zCLILong must be the "long
+** form" CLI flag (NULL means to use zKey). zCLIShort may be NULL or
+** the "short form" CLI flag (if NULL, no short form is used).
+**
+** If argPos is >=0 and no other match is found,
+** json_command_arg(argPos) is also checked.
+**
+** On error (no match found) NULL is returned.
+**
+** This ONLY works for String JSON/GET/CLI values, not JSON
+** booleans and whatnot.
+*/
+char const * json_find_option_cstr2(char const * zKey,
+ char const * zCLILong,
+ char const * zCLIShort,
+ int argPos){
+ char const * rc = NULL;
+ assert(NULL != zKey);
+ if(!g.isHTTP){
+ rc = find_option(zCLILong ? zCLILong : zKey,
+ zCLIShort, 1);
+ }
+ if(!rc && fossil_has_json()){
+ rc = json_getenv_cstr(zKey);
+ if(!rc && zCLIShort){
+ rc = cson_value_get_cstr( cson_object_get( g.json.param.o, zCLIShort) );
+ }
+ }
+ if(!rc && (argPos>=0)){
+ rc = json_command_arg((unsigned char)argPos);
+ }
+ return rc;
+}
+
+/*
+** Short-hand form of json_find_option_cstr2(zKey,zCLILong,zCLIShort,-1).
+*/
+char const * json_find_option_cstr(char const * zKey,
+ char const * zCLILong,
+ char const * zCLIShort){
+ return json_find_option_cstr2(zKey, zCLILong, zCLIShort, -1);
+}
+
+/*
+** The boolean equivalent of json_find_option_cstr().
+** If the option is not found, dftl is returned.
+*/
+int json_find_option_bool(char const * zKey,
+ char const * zCLILong,
+ char const * zCLIShort,
+ int dflt ){
+ int rc = -1;
+ if(!g.isHTTP){
+ if(NULL != find_option(zCLILong ? zCLILong : zKey,
+ zCLIShort, 0)){
+ rc = 1;
+ }
+ }
+ if((-1==rc) && fossil_has_json()){
+ rc = json_getenv_bool(zKey,-1);
+ }
+ return (-1==rc) ? dflt : rc;
+}
+
+/*
+** The integer equivalent of json_find_option_cstr2().
+** If the option is not found, dftl is returned.
+*/
+int json_find_option_int(char const * zKey,
+ char const * zCLILong,
+ char const * zCLIShort,
+ int dflt ){
+ enum { Magic = -1947854832 };
+ int rc = Magic;
+ if(!g.isHTTP){
+ /* FIXME: use strtol() for better error/dflt handling. */
+ char const * opt = find_option(zCLILong ? zCLILong : zKey,
+ zCLIShort, 1);
+ if(NULL!=opt){
+ rc = atoi(opt);
+ }
+ }
+ if(Magic==rc){
+ rc = json_getenv_int(zKey,Magic);
+ }
+ return (Magic==rc) ? dflt : rc;
+}
+
+
+/*
+** Adds v to g.json.param.o using the given key. May cause any prior
+** item with that key to be destroyed (depends on current reference
+** count for that value). On success, transfers (or shares) ownership
+** of v to (or with) g.json.param.o. On error ownership of v is not
+** modified.
+*/
+int json_setenv( char const * zKey, cson_value * v ){
+ return cson_object_set( g.json.param.o, zKey, v );
+}
+
+/*
+** Guesses a RESPONSE Content-Type value based (primarily) on the
+** HTTP_ACCEPT header.
+**
+** It will try to figure out if the client can support
+** application/json, text/javascript, and will fall back to
+** text/plain if it cannot figure out anything more specific.
+**
+** Returned memory is static and immutable, but if the environment
+** changes after calling this then subsequent calls to this function
+** might return different (also static/immutable) values.
+*/
+char const * json_guess_content_type(){
+ char const * cset;
+ char doUtf8;
+ cset = PD("HTTP_ACCEPT_CHARSET",NULL);
+ doUtf8 = ((NULL == cset) || (NULL!=strstr("utf-8",cset)))
+ ? 1 : 0;
+ if( g.json.jsonp ){
+ return doUtf8
+ ? "text/javascript; charset=utf-8"
+ : "text/javascript";
+ }else{
+ /*
+ Content-type
+
+ If the browser does not sent an ACCEPT for application/json
+ then we fall back to text/plain.
+ */
+ char const * cstr;
+ cstr = PD("HTTP_ACCEPT",NULL);
+ if( NULL == cstr ){
+ return doUtf8
+ ? "application/json; charset=utf-8"
+ : "application/json";
+ }else{
+ if( strstr( cstr, "application/json" )
+ || strstr( cstr, "*/*" ) ){
+ return doUtf8
+ ? "application/json; charset=utf-8"
+ : "application/json";
+ }else{
+ return "text/plain";
+ }
+ }
+ }
+}
+
+/*
+ ** Given a request CONTENT_TYPE value, this function returns true
+ ** if it is of a type which the JSON API can ostensibly read.
+ **
+ ** It accepts any of application/json, text/plain,
+ ** application/javascript, or text/javascript. The former is
+ ** preferred, but was not widespread when this API was initially
+ ** built, so the latter forms are permitted as fallbacks.
+ */
+int json_can_consume_content_type(const char * zType){
+ return fossil_strcmp(zType, "application/json")==0
+ || fossil_strcmp(zType,"text/plain")==0/*assume this MIGHT be JSON*/
+ || fossil_strcmp(zType,"text/javascript")==0
+ || fossil_strcmp(zType,"application/javascript")==0;
+}
+
+/*
+** Sends pResponse to the output stream as the response object. This
+** function does no validation of pResponse except to assert() that it
+** is not NULL. The caller is responsible for ensuring that it meets
+** API response envelope conventions.
+**
+** In CLI mode pResponse is sent to stdout immediately. In HTTP
+** mode pResponse replaces any current CGI content but cgi_reply()
+** is not called to flush the output.
+**
+** If g.json.jsonp is not NULL then the content type is set to
+** text/javascript and the output is wrapped in a jsonp
+** wrapper.
+**
+** This function works only the first time it is called. It "should
+** not" ever be called more than once but certain calling
+** constellations might trigger that, in which case the second and
+** subsequent calls are no-ops.
+*/
+void json_send_response( cson_value const * pResponse ){
+ static int once = 0;
+ assert( NULL != pResponse );
+ if( once++ ) return;
+ if( g.isHTTP ){
+ cgi_reset_content();
+ if( g.json.jsonp ){
+ cgi_set_content_type("text/javascript");
+ cgi_printf("%s(",g.json.jsonp);
+ }
+ cson_output( pResponse, cson_data_dest_cgi, NULL, &g.json.outOpt );
+ if( g.json.jsonp ){
+ cgi_append_content(")",1);
+ }
+ }else{/*CLI mode*/
+ if( g.json.jsonp ){
+ fprintf(stdout,"%s(",g.json.jsonp);
+ }
+ cson_output_FILE( pResponse, stdout, &g.json.outOpt );
+ if( g.json.jsonp ){
+ fwrite(")\n", 2, 1, stdout);
+ }
+ }
+}
+
+/*
+** Returns the current request's JSON authentication token, or NULL if
+** none is found. The token's memory is owned by (or shared with)
+** g.json.
+**
+** If an auth token is found in the GET/POST request data then fossil
+** is given that data for use in authentication for this
+** session. i.e. the GET/POST data overrides fossil's authentication
+** cookie value (if any) and also works with clients which do not
+** support cookies.
+**
+** Must be called once before login_check_credentials() is called or
+** we will not be able to replace fossil's internal idea of the auth
+** info in time (and future changes to that state may cause unexpected
+** results).
+**
+** The result of this call are cached for future calls.
+**
+** Special case: if g.useLocalauth is true (i.e. the --localauth flag
+** was used to start the fossil server instance) and the current
+** connection is "local", any authToken provided by the user is
+** ignored and no new token is created: localauth mode trumps the
+** authToken.
+*/
+cson_value * json_auth_token(){
+ assert(g.json.gc.a && "json_bootstrap_early() was not called!");
+ if( g.json.authToken==0 && g.noPswd==0
+ /* g.noPswd!=0 means the user was logged in via a local
+ connection using --localauth. */
+ ){
+ /* Try to get an authorization token from GET parameter, POSTed
+ JSON, or fossil cookie (in that order). */
+ g.json.authToken = json_getenv(FossilJsonKeys.authToken);
+ if(g.json.authToken
+ && cson_value_is_string(g.json.authToken)
+ && !PD(login_cookie_name(),NULL)){
+ /* tell fossil to use this login info.
+
+ FIXME?: because the JSON bits don't carry around
+ login_cookie_name(), there is(?) a potential(?) login hijacking
+ window here. We may need to change the JSON auth token to be in
+ the form: login_cookie_name()=...
+
+ Then again, the hardened cookie value helps ensure that
+ only a proper key/value match is valid.
+ */
+ cgi_replace_parameter( login_cookie_name(),
+ cson_value_get_cstr(g.json.authToken) );
+ }else if( g.isHTTP ){
+ /* try fossil's conventional cookie. */
+ /* Reminder: chicken/egg scenario regarding db access in CLI
+ mode because login_cookie_name() needs the db. CLI
+ mode does not use any authentication, so we don't need
+ to support it here.
+ */
+ char const * zCookie = P(login_cookie_name());
+ if( zCookie && *zCookie ){
+ /* Transfer fossil's cookie to JSON for downstream convenience... */
+ cson_value * v = cson_value_new_string(zCookie, strlen(zCookie));
+ json_gc_add( FossilJsonKeys.authToken, v );
+ g.json.authToken = v;
+ }
+ }
+ }
+ return g.json.authToken;
+}
+
+/*
+** If g.json.reqPayload.o is NULL then NULL is returned, else the
+** given property is searched for in the request payload. If found it
+** is returned. The returned value is owned by (or shares ownership
+** with) g.json, and must NOT be cson_value_free()'d by the
+** caller.
+*/
+cson_value * json_req_payload_get(char const *pKey){
+ return g.json.reqPayload.o
+ ? cson_object_get(g.json.reqPayload.o,pKey)
+ : NULL;
+}
+
+
+/*
+** Returns non-zero if the json_bootstrap_early() function has already
+** been called. In general, this function should be used sparingly,
+** e.g. from low-level support functions like fossil_warning() where
+** there is genuine uncertainty about whether (or not) the JSON setup
+** has already been called.
+*/
+int json_is_bootstrapped_early(void){
+ return ((g.json.gc.v != NULL) && (g.json.gc.a != NULL));
+}
+
+/*
+** Initializes some JSON bits which need to be initialized relatively
+** early on. It should be called by any routine which might need to
+** call into JSON relatively early on in the init process.
+** Specifically, early on in cgi_init() and json_cmd_top(), but also
+** from any error reporting routines which might be triggered (early
+** on in those functions).
+**
+** Initializes g.json.gc and g.json.param. This code does not (and
+** must not) rely on any of the fossil environment having been set
+** up. e.g. it must not use cgi_parameter() and friends because this
+** must be called before those data are initialized.
+**
+** If called multiple times, calls after the first are a no-op.
+*/
+void json_bootstrap_early(void){
+ cson_value * v;
+
+ if(g.json.gc.v!=NULL){
+ /* Avoid multiple bootstrappings. */
+ return;
+ }
+ g.json.timerId = fossil_timer_start();
+ /* g.json.gc is our "garbage collector" - where we put JSON values
+ which need a long lifetime but don't have a logical parent to put
+ them in. */
+ v = cson_value_new_array();
+ g.json.gc.v = v;
+ assert(0 != g.json.gc.v);
+ g.json.gc.a = cson_value_get_array(v);
+ assert(0 != g.json.gc.a);
+ cson_value_add_reference(v)
+ /* Needed to allow us to include this value in other JSON
+ containers without transferring ownership to those containers.
+ All other persistent g.json.XXX.v values get appended to
+ g.json.gc.a, and therefore already have a live reference
+ for this purpose. */
+ ;
+
+ /*
+ g.json.param holds the JSONized counterpart of fossil's
+ cgi_parameter_xxx() family of data. We store them as JSON, as
+ opposed to using fossil's data directly, because we can retain
+ full type information for data this way (as opposed to it always
+ being of type string).
+ */
+ v = cson_value_new_object();
+ g.json.param.v = v;
+ g.json.param.o = cson_value_get_object(v);
+ json_gc_add("$PARAMS", v);
+}
+
+/*
+** Appends a warning object to the (pending) JSON response.
+**
+** Code must be a FSL_JSON_W_xxx value from the FossilJsonCodes enum.
+**
+** A Warning object has this JSON structure:
+**
+** { "code":integer, "text":"string" }
+**
+** But the text part is optional.
+**
+** If msg is non-NULL and not empty then it is used as the "text"
+** property's value. It is copied, and need not refer to static
+** memory.
+**
+** CURRENTLY this code only allows a given warning code to be
+** added one time, and elides subsequent warnings. The intention
+** is to remove that burden from loops which produce warnings.
+**
+** FIXME: if msg is NULL then use a standard string for
+** the given code. If !*msg then elide the "text" property,
+** for consistency with how json_err() works.
+*/
+void json_warn( int code, char const * fmt, ... ){
+ cson_object * obj = NULL;
+ assert( (code>FSL_JSON_W_START)
+ && (code<FSL_JSON_W_END)
+ && "Invalid warning code.");
+ assert(g.json.gc.a && "json_bootstrap_early() was not called!");
+ if(!g.json.warnings){
+ g.json.warnings = cson_new_array();
+ assert((NULL != g.json.warnings) && "Alloc error.");
+ json_gc_add("$WARNINGS",cson_array_value(g.json.warnings));
+ }
+ obj = cson_new_object();
+ cson_array_append(g.json.warnings, cson_object_value(obj));
+ cson_object_set(obj,"code",cson_value_new_integer(code));
+ if(fmt && *fmt){
+ /* FIXME: treat NULL fmt as standard warning message for
+ the code, but we don't have those yet.
+ */
+ va_list vargs;
+ char * msg;
+ va_start(vargs,fmt);
+ msg = vmprintf(fmt,vargs);
+ va_end(vargs);
+ cson_object_set(obj,"text", cson_value_new_string(msg,strlen(msg)));
+ fossil_free(msg);
+ }
+}
+
+/*
+** Splits zStr (which must not be NULL) into tokens separated by the
+** given separator character. If doDeHttp is true then each element
+** will be passed through dehttpize(), otherwise they are used
+** as-is. Note that tokenization happens before dehttpize(),
+** which is significant if the encoded tokens might contain the
+** separator character.
+**
+** Each new element is appended to the given target array object,
+** which must not be NULL and ownership of it is not changed by this
+** call.
+**
+** On success, returns the number of tokens _encountered_. On error a
+** NEGATIVE number is returned - its absolute value is the number of
+** tokens encountered (i.e. it reveals which token in zStr was
+** problematic).
+**
+** Achtung: leading and trailing whitespace of elements are elided.
+**
+** Achtung: empty elements will be skipped, meaning consecutive empty
+** elements are collapsed.
+*/
+int json_string_split( char const * zStr,
+ char separator,
+ int doDeHttp,
+ cson_array * target ){
+ char const * p = zStr /* current byte */;
+ char const * head /* current start-of-token */;
+ unsigned int len = 0 /* current token's length */;
+ int rc = 0 /* return code (number of added elements)*/;
+ assert( zStr && target );
+ while( fossil_isspace(*p) ){
+ ++p;
+ }
+ head = p;
+ for( ; ; ++p){
+ if( !*p || (separator == *p) ){
+ if( len ){/* append head..(head+len) as next array
+ element. */
+ cson_value * part = NULL;
+ char * zPart = NULL;
+ ++rc;
+ assert( head != p );
+ zPart = (char*)fossil_malloc(len+1);
+ memcpy(zPart, head, len);
+ zPart[len] = 0;
+ if(doDeHttp){
+ dehttpize(zPart);
+ }
+ if( *zPart ){
+ /* should only fail if someone manages to url-encoded a NUL byte */
+ part = cson_value_new_string(zPart, strlen(zPart));
+ if( 0 != cson_array_append( target, part ) ){
+ cson_value_free(part);
+ rc = -rc;
+ break;
+ }
+ }else{
+ assert(0 && "i didn't think this was possible!");
+ fprintf(stderr,"%s:%d: My God! It's full of stars!\n",
+ __FILE__, __LINE__);
+ fossil_exit(1)
+ /* Not fossil_panic() b/c this code needs to be able to
+ run before some of the fossil/json bits are initialized,
+ and fossil_panic() calls into the JSON API.
+ */
+ ;
+ }
+ fossil_free(zPart);
+ len = 0;
+ }
+ if( !*p ){
+ break;
+ }
+ head = p+1;
+ while( *head && fossil_isspace(*head) ){
+ ++head;
+ ++p;
+ }
+ if(!*head){
+ break;
+ }
+ continue;
+ }
+ ++len;
+ }
+ return rc;
+}
+
+/*
+** Wrapper around json_string_split(), taking the same first 3
+** parameters as this function, but returns the results as a JSON
+** Array (if splitting produced tokens) or NULL (if splitting failed
+** in any way or produced no tokens).
+**
+** The returned value is owned by the caller. If not NULL then it
+** _will_ have a JSON type of Array.
+*/
+cson_value * json_string_split2( char const * zStr,
+ char separator,
+ int doDeHttp ){
+ cson_array * a = cson_new_array();
+ int rc = json_string_split( zStr, separator, doDeHttp, a );
+ if( 0>=rc ){
+ cson_free_array(a);
+ a = NULL;
+ }
+ return a ? cson_array_value(a) : NULL;
+}
+
+
+/*
+** Performs some common initialization of JSON-related state. Must be
+** called by the json_page_top() and json_cmd_top() dispatching
+** functions to set up the JSON stat used by the dispatched functions.
+**
+** Implicitly sets up the login information state in CGI mode, but
+** does not perform any permissions checking. It _might_ (haven't
+** tested this) die with an error if an auth cookie is malformed.
+**
+** This must be called by the top-level JSON command dispatching code
+** before they do any work.
+**
+** This must only be called once, or an assertion may be triggered.
+*/
+void json_bootstrap_late(){
+ static char once = 0 /* guard against multiple runs */;
+ char const * zPath = P("PATH_INFO");
+ assert(g.json.gc.a && "json_bootstrap_early() was not called!");
+ assert( (0==once) && "json_bootstrap_late() called too many times!");
+ if( once ){
+ return;
+ }else{
+ once = 1;
+ }
+ assert(g.json.isJsonMode
+ && "g.json.isJsonMode should have been set up by now.");
+ g.json.resultCode = 0;
+ g.json.cmd.offset = -1;
+ g.json.jsonp = PD("jsonp",NULL)
+ /* FIXME: do some sanity checking on g.json.jsonp and ignore it
+ if it is not halfway reasonable.
+ */
+ ;
+ if( !g.isHTTP && g.fullHttpReply ){
+ /* workaround for server mode, so we see it as CGI mode. */
+ g.isHTTP = 1;
+ }
+
+ if(g.isHTTP){
+ cgi_set_content_type(json_guess_content_type())
+ /* reminder: must be done after g.json.jsonp is initialized */
+ ;
+#if 0
+ /* Calling this seems to trigger an SQLITE_MISUSE warning???
+ Maybe it's not legal to set the logger more than once?
+ */
+ sqlite3_config(SQLITE_CONFIG_LOG, NULL, 0)
+ /* avoids debug messages on stderr in JSON mode */
+ ;
+#endif
+ }
+
+ g.json.cmd.v = cson_value_new_array();
+ g.json.cmd.a = cson_value_get_array(g.json.cmd.v);
+ json_gc_add( FossilJsonKeys.commandPath, g.json.cmd.v );
+ /*
+ The following if/else block translates the PATH_INFO path (in
+ CLI/server modes) or g.argv (CLI mode) into an internal list so
+ that we can simplify command dispatching later on.
+
+ Note that translating g.argv this way is overkill but allows us to
+ avoid CLI-only special-case handling in other code, e.g.
+ json_command_arg().
+ */
+ if( zPath ){/* Either CGI or server mode... */
+ /* Translate PATH_INFO into JSON array for later convenience. */
+ json_string_split(zPath, '/', 1, g.json.cmd.a);
+ }else{/* assume CLI mode */
+ int i;
+ char const * arg;
+ cson_value * part;
+ for(i = 1/*skip argv[0]*/; i < g.argc; ++i ){
+ arg = g.argv[i];
+ if( !arg || !*arg ){
+ continue;
+ }
+ if('-' == *arg){
+ /* workaround to skip CLI args so that
+ json_command_arg() does not see them.
+ This assumes that all arguments come LAST
+ on the command line.
+ */
+ break;
+ }
+ part = cson_value_new_string(arg,strlen(arg));
+ cson_array_append(g.json.cmd.a, part);
+ }
+ }
+
+ while(!g.isHTTP){
+ /* Simulate JSON POST data via input file. Pedantic reminder:
+ error handling does not honor user-supplied g.json.outOpt
+ because outOpt cannot (generically) be configured until after
+ POST-reading is finished.
+ */
+ FILE * inFile = NULL;
+ char const * jfile = find_option("json-input",NULL,1);
+ Blob json = BLOB_INITIALIZER;
+ if(!jfile || !*jfile){
+ break;
+ }
+ inFile = (0==strcmp("-",jfile))
+ ? stdin
+ : fossil_fopen(jfile,"rb");
+ if(!inFile){
+ g.json.resultCode = FSL_JSON_E_FILE_OPEN_FAILED;
+ fossil_fatal("Could not open JSON file [%s].",jfile)
+ /* Does not return. */
+ ;
+ }
+ blob_read_from_channel(&json, inFile, -1);
+ fossil_fclose(inFile);
+ cgi_parse_POST_JSON(&json);
+ blob_reset(&json);
+ break;
+ }
+
+ /* g.json.reqPayload exists only to simplify some of our access to
+ the request payload. We currently only use this in the context of
+ Object payloads, not Arrays, strings, etc.
+ */
+ g.json.reqPayload.v = cson_object_get( g.json.post.o,FossilJsonKeys.payload );
+ if( g.json.reqPayload.v ){
+ g.json.reqPayload.o = cson_value_get_object( g.json.reqPayload.v )
+ /* g.json.reqPayload.o may legally be NULL, which means only that
+ g.json.reqPayload.v is-not-a Object.
+ */;
+ }
+
+ /* Anything which needs json_getenv() and friends should go after
+ this point.
+ */
+
+ if(1 == cson_array_length_get(g.json.cmd.a)){
+ /* special case: if we're at the top path, look for
+ a "command" request arg which specifies which command
+ to run.
+ */
+ char const * cmd = json_getenv_cstr("command");
+ if(cmd){
+ json_string_split(cmd, '/', 0, g.json.cmd.a);
+ g.json.cmd.commandStr = cmd;
+ }
+ }
+
+
+ if(!g.json.jsonp){
+ g.json.jsonp = json_find_option_cstr("jsonp",NULL,NULL);
+ }
+ if(!g.isHTTP){
+ g.json.errorDetailParanoia = 0;/*disable error code dumb-down for CLI mode*/
+ }
+
+ {/* set up JSON output formatting options. */
+ int indent = -1;
+ indent = json_find_option_int("indent",NULL,"I",-1);
+ g.json.outOpt.indentation = (0>indent)
+ ? (g.isHTTP ? 0 : 1)
+ : (unsigned char)indent;
+ g.json.outOpt.addNewline = g.isHTTP
+ ? 0
+ : (g.json.jsonp ? 0 : 1);
+ }
+
+ if( g.isHTTP ){
+ json_auth_token()/* will copy our auth token, if any, to fossil's
+ core, which we need before we call
+ login_check_credentials(). */;
+ login_check_credentials()/* populates g.perm */;
+ }
+ else{
+ /* FIXME: we need an option which allows us to skip this. At least
+ one known command (/json/version) does not need an opened
+ repo. The problem here is we cannot know which functions need
+ it from here (because command dispatching hasn't yet happened)
+ and all other commands rely on the repo being opened before
+ they are called. A textbook example of lack of foresight :/.
+ */
+ db_find_and_open_repository(OPEN_ANY_SCHEMA,0);
+ }
+}
+
+/*
+** Returns the ndx'th item in the "command path", where index 0 is the
+** position of the "json" part of the path. Returns NULL if ndx is out
+** of bounds or there is no "json" path element.
+**
+** In CLI mode the "path" is the list of arguments (skipping argv[0]).
+** In server/CGI modes the path is taken from PATH_INFO.
+**
+** The returned bytes are owned by g.json.cmd.v and _may_ be
+** invalidated if that object is modified (depending on how it is
+** modified).
+**
+** Note that CLI options are not included in the command path. Use
+** find_option() to get those.
+**
+*/
+char const * json_command_arg(unsigned short ndx){
+ cson_array * ar = g.json.cmd.a;
+ assert((NULL!=ar) && "Internal error. Was json_bootstrap_late() called?");
+ assert((g.argc>1) &&"Internal error - we never should have gotten this far.");
+ if( g.json.cmd.offset < 0 ){
+ /* first-time setup. */
+ short i = 0;
+#define NEXT cson_string_cstr( \
+ cson_value_get_string( \
+ cson_array_get(ar,i) \
+ ))
+ char const * tok = NEXT;
+ while( tok ){
+ if( g.isHTTP/*workaround for "abbreviated name" in CLI mode*/
+ ? (0==strncmp("json",tok,4))
+ : (0==strcmp(g.argv[1],tok))
+ ){
+ g.json.cmd.offset = i;
+ break;
+ }
+ ++i;
+ tok = NEXT;
+ }
+ }
+#undef NEXT
+ if(g.json.cmd.offset < 0){
+ return NULL;
+ }else{
+ ndx = g.json.cmd.offset + ndx;
+ return cson_string_cstr(cson_value_get_string(
+ cson_array_get( ar, g.json.cmd.offset + ndx )));
+ }
+}
+
+/* Returns the C-string form of json_auth_token(), or NULL
+** if json_auth_token() returns NULL.
+*/
+char const * json_auth_token_cstr(){
+ return cson_value_get_cstr( json_auth_token() );
+}
+
+/*
+** Returns the JsonPageDef with the given name, or NULL if no match is
+** found.
+**
+** head must be a pointer to an array of JsonPageDefs in which the
+** last entry has a NULL name.
+*/
+JsonPageDef const * json_handler_for_name( char const * name,
+ JsonPageDef const * head ){
+ JsonPageDef const * pageDef = head;
+ assert( head != NULL );
+ if(name && *name) for( ; pageDef->name; ++pageDef ){
+ if( 0 == strcmp(name, pageDef->name) ){
+ return pageDef;
+ }
+ }
+ return NULL;
+}
+
+/*
+** Given a Fossil/JSON result code, this function "dumbs it down"
+** according to the current value of g.json.errorDetailParanoia. The
+** dumbed-down value is returned.
+**
+** This function assert()s that code is in the inclusive range 0 to
+** 9999.
+**
+** Note that WARNING codes (1..999) are never dumbed down.
+**
+*/
+static int json_dumbdown_rc( int code ){
+ if(!g.json.errorDetailParanoia
+ || !code
+ || ((code>=FSL_JSON_W_START) && (code<FSL_JSON_W_END))){
+ return code;
+ }else{
+ int modulo = 0;
+ assert((code >= 1000) && (code <= 9999) && "Invalid Fossil/JSON code.");
+ switch( g.json.errorDetailParanoia ){
+ case 1: modulo = 10; break;
+ case 2: modulo = 100; break;
+ case 3: modulo = 1000; break;
+ default: break;
+ }
+ if( modulo ) code = code - (code % modulo);
+ return code;
+ }
+}
+
+/*
+** Convenience routine which converts a Julian time value into a Unix
+** Epoch timestamp. Requires the db, so this cannot be used before the
+** repo is opened (will trigger a fatal error in db_xxx()). The returned
+** value is owned by the caller.
+*/
+cson_value * json_julian_to_timestamp(double j){
+ return cson_value_new_integer((cson_int_t)
+ db_int64(0,"SELECT cast(strftime('%%s',%lf) as int)",j)
+ );
+}
+
+/*
+** Returns a timestamp value.
+*/
+cson_int_t json_timestamp(){
+ return (cson_int_t)time(0);
+}
+
+/*
+** Returns a new JSON value (owned by the caller) representing
+** a timestamp. If timeVal is < 0 then time(0) is used to fetch
+** the time, else timeVal is used as-is. The returned value is
+** owned by the caller.
+*/
+cson_value * json_new_timestamp(cson_int_t timeVal){
+ return cson_value_new_integer((timeVal<0) ? (cson_int_t)time(0) : timeVal);
+}
+
+/*
+** Internal helper for json_create_response(). Appends the first
+** g.json.dispatchDepth elements of g.json.cmd.a, skipping the first
+** one (the "json" part), to a string and returns that string value
+** (which is owned by the caller).
+*/
+static cson_value * json_response_command_path(){
+ if(!g.json.cmd.a){
+ return NULL;
+ }else{
+ cson_value * rc = NULL;
+ Blob path = empty_blob;
+ unsigned int aLen = g.json.dispatchDepth+1;
+ /*cson_array_length_get(g.json.cmd.a);*/
+ unsigned int i = 1;
+ for( ; i < aLen; ++i ){
+ char const * part = cson_string_cstr(cson_value_get_string(
+ cson_array_get(g.json.cmd.a, i)));
+ if(!part){
+#if 1
+ fossil_warning("Iterating further than expected in %s.",
+ __FILE__);
+#endif
+ break;
+ }
+ blob_appendf(&path,"%s%s", (i>1 ? "/": ""), part);
+ }
+ rc = json_new_string((blob_size(&path)>0)
+ ? blob_buffer(&path)
+ : "")
+ /* reminder; we need an empty string instead of NULL
+ in this case, to avoid what outwardly looks like
+ (but is not) an allocation error in
+ json_create_response().
+ */
+ ;
+ blob_reset(&path);
+ return rc;
+ }
+}
+
+/*
+** Returns a JSON Object representation of the global g object.
+** Returned value is owned by the caller.
+*/
+cson_value * json_g_to_json(){
+ cson_object * o = NULL;
+ cson_object * pay = NULL;
+ pay = o = cson_new_object();
+
+#define INT(OBJ,K) cson_object_set(o, #K, json_new_int(OBJ.K))
+#define CSTR(OBJ,K) cson_object_set(o, #K, OBJ.K ? json_new_string(OBJ.K) \
+ : cson_value_null())
+#define VAL(K,V) cson_object_set(o, #K, (V) ? (V) : cson_value_null())
+ VAL(capabilities, json_cap_value());
+ INT(g, argc);
+ INT(g, isConst);
+ CSTR(g, zConfigDbName);
+ INT(g, repositoryOpen);
+ INT(g, localOpen);
+ INT(g, minPrefix);
+ INT(g, fSqlTrace);
+ INT(g, fSqlStats);
+ INT(g, fSqlPrint);
+ INT(g, fQuiet);
+ INT(g, fHttpTrace);
+ INT(g, fSystemTrace);
+ INT(g, fNoSync);
+ INT(g, iErrPriority);
+ INT(g, sslNotAvailable);
+ INT(g, cgiOutput);
+ INT(g, xferPanic);
+ INT(g, fullHttpReply);
+ INT(g, xlinkClusterOnly);
+ INT(g, fTimeFormat);
+ INT(g, markPrivate);
+ INT(g, clockSkewSeen);
+ INT(g, isHTTP);
+ INT(g.url, isFile);
+ INT(g.url, isHttps);
+ INT(g.url, isSsh);
+ INT(g.url, port);
+ INT(g.url, dfltPort);
+ INT(g, useLocalauth);
+ INT(g, noPswd);
+ INT(g, userUid);
+ INT(g, rcvid);
+ INT(g, okCsrf);
+ INT(g, thTrace);
+ INT(g, isHome);
+ INT(g, nAux);
+ INT(g, allowSymlinks);
+
+ CSTR(g, zOpenRevision);
+ CSTR(g, zLocalRoot);
+ CSTR(g, zPath);
+ CSTR(g, zExtra);
+ CSTR(g, zBaseURL);
+ CSTR(g, zTop);
+ CSTR(g, zContentType);
+ CSTR(g, zErrMsg);
+ CSTR(g.url, name);
+ CSTR(g.url, hostname);
+ CSTR(g.url, protocol);
+ CSTR(g.url, path);
+ CSTR(g.url, user);
+ CSTR(g.url, passwd);
+ CSTR(g.url, canonical);
+ CSTR(g.url, proxyAuth);
+ CSTR(g.url, fossil);
+ CSTR(g, zLogin);
+ CSTR(g, zSSLIdentity);
+ CSTR(g, zIpAddr);
+ CSTR(g, zNonce);
+ CSTR(g, zCsrfToken);
+
+ o = cson_new_object();
+ cson_object_set(pay, "json", cson_object_value(o) );
+ INT(g.json, isJsonMode);
+ INT(g.json, resultCode);
+ INT(g.json, errorDetailParanoia);
+ INT(g.json, dispatchDepth);
+ VAL(authToken, g.json.authToken);
+ CSTR(g.json, jsonp);
+ VAL(gc, g.json.gc.v);
+ VAL(cmd, g.json.cmd.v);
+ VAL(param, g.json.param.v);
+ VAL(POST, g.json.post.v);
+ VAL(warnings, cson_array_value(g.json.warnings));
+ /*cson_output_opt outOpt;*/
+
+
+#undef INT
+#undef CSTR
+#undef VAL
+ return cson_object_value(pay);
+}
+
+
+/*
+** Creates a new Fossil/JSON response envelope skeleton. It is owned
+** by the caller, who must eventually free it using cson_value_free(),
+** or add it to a cson container to transfer ownership. Returns NULL
+** on error.
+**
+** If payload is not NULL and resultCode is 0 then it is set as the
+** "payload" property of the returned object. If resultCode is 0 then
+** it defaults to g.json.resultCode. If resultCode is (or defaults to)
+** non-zero and payload is not NULL then this function calls
+** cson_value_free(payload) and does not insert the payload into the
+** response. In either case, ownership of payload is transferred to (or
+** shared with, if the caller holds a reference) this function.
+**
+** pMsg is an optional message string property (resultText) of the
+** response. If resultCode is non-0 and pMsg is NULL then
+** json_err_cstr() is used to get the error string. The caller may
+** provide his own or may use an empty string to suppress the
+** resultText property.
+**
+*/
+static cson_value * json_create_response( int resultCode,
+ char const * pMsg,
+ cson_value * payload){
+ cson_value * v = NULL;
+ cson_value * tmp = NULL;
+ cson_object * o = NULL;
+ int rc;
+ resultCode = json_dumbdown_rc(resultCode ? resultCode : g.json.resultCode);
+ o = cson_new_object();
+ v = cson_object_value(o);
+ if( ! o ) return NULL;
+#define SET(K) if(!tmp) goto cleanup; \
+ cson_value_add_reference(tmp); \
+ rc = cson_object_set( o, K, tmp ); \
+ cson_value_free(tmp); \
+ if(rc) do{ \
+ tmp = NULL; \
+ goto cleanup; \
+ }while(0)
+
+ tmp = json_new_string(MANIFEST_UUID);
+ SET("fossil");
+
+ tmp = json_new_timestamp(-1);
+ SET(FossilJsonKeys.timestamp);
+
+ if( 0 != resultCode ){
+ if( ! pMsg ){
+ pMsg = g.zErrMsg;
+ if(!pMsg){
+ pMsg = json_err_cstr(resultCode);
+ }
+ }
+ tmp = json_new_string(json_rc_cstr(resultCode));
+ SET(FossilJsonKeys.resultCode);
+ }
+
+ if( pMsg && *pMsg ){
+ tmp = json_new_string(pMsg);
+ SET(FossilJsonKeys.resultText);
+ }
+
+ if(g.json.cmd.commandStr){
+ tmp = json_new_string(g.json.cmd.commandStr);
+ }else{
+ tmp = json_response_command_path();
+ }
+ if(!tmp){
+ tmp = json_new_string("???");
+ }
+ SET("command");
+
+ tmp = json_getenv(FossilJsonKeys.requestId);
+ if( tmp ) cson_object_set( o, FossilJsonKeys.requestId, tmp );
+
+ if(0){/* these are only intended for my own testing...*/
+ if(g.json.cmd.v){
+ tmp = g.json.cmd.v;
+ SET("$commandPath");
+ }
+ if(g.json.param.v){
+ tmp = g.json.param.v;
+ SET("$params");
+ }
+ if(0){/*Only for debugging, add some info to the response.*/
+ tmp = cson_value_new_integer( g.json.cmd.offset );
+ SET("cmd.offset");
+ tmp = cson_value_new_bool( g.isHTTP );
+ SET("isCGI");
+ }
+ }
+
+ if(fossil_timer_is_active(g.json.timerId)){
+ /* This is, philosophically speaking, not quite the right place
+ for ending the timer, but this is the one function which all of
+ the JSON exit paths use (and they call it after processing,
+ just before they end).
+ */
+ sqlite3_uint64 span = fossil_timer_stop(g.json.timerId);
+ /* I'm actually seeing sub-uSec runtimes in some tests, but a time of
+ 0 is "just kinda wrong".
+ */
+ cson_object_set(o,"procTimeUs", cson_value_new_integer((cson_int_t)span));
+ span /= 1000/*for milliseconds */;
+ cson_object_set(o,"procTimeMs", cson_value_new_integer((cson_int_t)span));
+ assert(!fossil_timer_is_active(g.json.timerId));
+ g.json.timerId = -1;
+ }
+ if(g.json.warnings){
+ tmp = cson_array_value(g.json.warnings);
+ SET("warnings");
+ }
+
+ /* Only add the payload to SUCCESS responses. Else delete it. */
+ if( NULL != payload ){
+ if( resultCode ){
+ cson_value_free(payload);
+ payload = NULL;
+ }else{
+ tmp = payload;
+ SET(FossilJsonKeys.payload);
+ }
+ }
+
+ if((g.perm.Admin||g.perm.Setup)
+ && json_find_option_bool("debugFossilG","json-debug-g",NULL,0)
+ ){
+ tmp = json_g_to_json();
+ SET("g");
+ }
+
+#undef SET
+ goto ok;
+ cleanup:
+ cson_value_free(v);
+ v = NULL;
+ ok:
+ return v;
+}
+
+/*
+** Outputs a JSON error response to either the cgi_xxx() family of
+** buffers (in CGI/server mode) or stdout (in CLI mode). If rc is 0
+** then g.json.resultCode is used. If that is also 0 then the "Unknown
+** Error" code is used.
+**
+** If g.isHTTP then the generated JSON error response object replaces
+** any currently buffered page output. Because the output goes via
+** the cgi_xxx() family of functions, this function inherits any
+** compression which fossil does for its output.
+**
+** If alsoOutput is true AND g.isHTTP then cgi_reply() is called to
+** flush the output (and headers). Generally only do this if you are
+** about to call exit().
+**
+** If !g.isHTTP then alsoOutput is ignored and all output is sent to
+** stdout immediately.
+**
+** For generating the resultText property: if msg is not NULL then it
+** is used as-is. If it is NULL then g.zErrMsg is checked, and if that
+** is NULL then json_err_cstr(code) is used.
+*/
+void json_err( int code, char const * msg, int alsoOutput ){
+ int rc = code ? code : (g.json.resultCode
+ ? g.json.resultCode
+ : FSL_JSON_E_UNKNOWN);
+ cson_value * resp = NULL;
+ if(g.json.isJsonMode==0) return;
+ rc = json_dumbdown_rc(rc);
+ if( rc && !msg ){
+ msg = g.zErrMsg;
+ if(!msg){
+ msg = json_err_cstr(rc);
+ }
+ }
+ resp = json_create_response(rc, msg, NULL);
+ if(!resp){
+ /* about the only error case here is out-of-memory. DO NOT
+ call fossil_panic() or fossil_fatal() here because those
+ allocate.
+ */
+ fprintf(stderr, "%s: Fatal error: could not allocate "
+ "response object.\n", g.argv[0]);
+ fossil_exit(1);
+ }
+ if( g.isHTTP ){
+ if(alsoOutput){
+ json_send_response(resp);
+ }else{
+ /* almost a duplicate of json_send_response() :( */
+ cgi_reset_content();
+ if( g.json.jsonp ){
+ cgi_printf("%s(",g.json.jsonp);
+ }
+ cson_output( resp, cson_data_dest_cgi, NULL, &g.json.outOpt );
+ if( g.json.jsonp ){
+ cgi_append_content(")",1);
+ }
+ }
+ }else{
+ json_send_response(resp);
+ }
+ cson_value_free(resp);
+}
+
+/*
+** Sets g.json.resultCode and g.zErrMsg, but does not report the error
+** via json_err(). Returns the code passed to it.
+**
+** code must be in the inclusive range 1000..9999.
+*/
+int json_set_err( int code, char const * fmt, ... ){
+ assert( (code>=1000) && (code<=9999) );
+ fossil_free(g.zErrMsg);
+ g.json.resultCode = code;
+ if(!fmt || !*fmt){
+ g.zErrMsg = fossil_strdup(json_err_cstr(code));
+ }else{
+ va_list vargs;
+ char * msg;
+ va_start(vargs,fmt);
+ msg = vmprintf(fmt, vargs);
+ va_end(vargs);
+ g.zErrMsg = msg;
+ }
+ return code;
+}
+
+/*
+** Iterates through a prepared SELECT statement and converts each row
+** to a JSON object. If pTgt is not NULL then this function will
+** append the results to pTgt and return cson_array_value(pTgt). If
+** pTgt is NULL then a new Array object is created and returned (owned
+** by the caller). Each row of pStmt is converted to an Object and
+** appended to the array. If the result set has no rows AND pTgt is
+** NULL then NULL (not an empty array) is returned.
+*/
+cson_value * json_stmt_to_array_of_obj(Stmt *pStmt,
+ cson_array * pTgt){
+ cson_array * a = pTgt;
+ char const * warnMsg = NULL;
+ cson_value * colNamesV = NULL;
+ cson_array * colNames = NULL;
+ while( (SQLITE_ROW==db_step(pStmt)) ){
+ cson_value * row = NULL;
+ if(!a){
+ a = cson_new_array();
+ assert(NULL!=a);
+ }
+ if(!colNames){
+ colNamesV = cson_sqlite3_column_names(pStmt->pStmt);
+ assert(NULL != colNamesV);
+ /*Why? cson_value_add_reference(colNamesV) avoids an ownership problem*/;
+ colNames = cson_value_get_array(colNamesV);
+ assert(NULL != colNames);
+ }
+ row = cson_sqlite3_row_to_object2(pStmt->pStmt, colNames);
+ if(!row && !warnMsg){
+ warnMsg = "Could not convert at least one result row to JSON.";
+ continue;
+ }
+ if( 0 != cson_array_append(a, row) ){
+ cson_value_free(row);
+ if(pTgt != a) {
+ cson_free_array(a);
+ }
+ assert( 0 && "Alloc error.");
+ return NULL;
+ }
+ }
+ cson_value_free(colNamesV);
+ if(warnMsg){
+ json_warn( FSL_JSON_W_ROW_TO_JSON_FAILED, "%s", warnMsg );
+ }
+ return cson_array_value(a);
+}
+
+/*
+** Works just like json_stmt_to_array_of_obj(), but each row in the
+** result set is represented as an Array of values instead of an
+** Object (key/value pairs). If pTgt is NULL and the statement
+** has no results then NULL is returned, not an empty array.
+*/
+cson_value * json_stmt_to_array_of_array(Stmt *pStmt,
+ cson_array * pTgt){
+ cson_array * a = pTgt;
+ while( (SQLITE_ROW==db_step(pStmt)) ){
+ cson_value * row = NULL;
+ if(!a){
+ a = cson_new_array();
+ assert(NULL!=a);
+ }
+ row = cson_sqlite3_row_to_array(pStmt->pStmt);
+ cson_array_append(a, row);
+ }
+ return cson_array_value(a);
+}
+
+cson_value * json_stmt_to_array_of_values(Stmt *pStmt,
+ int resultColumn,
+ cson_array * pTgt){
+ cson_array * a = pTgt;
+ while( (SQLITE_ROW==db_step(pStmt)) ){
+ cson_value * row = cson_sqlite3_column_to_value(pStmt->pStmt,
+ resultColumn);
+ if(row){
+ if(!a){
+ a = cson_new_array();
+ assert(NULL!=a);
+ }
+ cson_array_append(a, row);
+ }
+ }
+ return cson_array_value(a);
+}
+
+/*
+** Executes the given SQL and runs it through
+** json_stmt_to_array_of_obj(), returning the result of that
+** function. If resetBlob is true then blob_reset(pSql) is called
+** after preparing the query.
+**
+** pTgt has the same semantics as described for
+** json_stmt_to_array_of_obj().
+**
+** FIXME: change this to take a (char const *) instead of a blob,
+** to simplify the trivial use-cases (which don't need a Blob).
+*/
+cson_value * json_sql_to_array_of_obj(Blob * pSql, cson_array * pTgt,
+ int resetBlob){
+ Stmt q = empty_Stmt;
+ cson_value * pay = NULL;
+ assert( blob_size(pSql) > 0 );
+ db_prepare(&q, "%s", blob_str(pSql) /*safe-for-%s*/);
+ if(resetBlob){
+ blob_reset(pSql);
+ }
+ pay = json_stmt_to_array_of_obj(&q, pTgt);
+ db_finalize(&q);
+ return pay;
+
+}
+
+/*
+** If the given COMMIT rid has any tags associated with it, this
+** function returns a JSON Array containing the tag names (owned by
+** the caller), else it returns NULL.
+**
+** See info_tags_of_checkin() for more details (this is simply a JSON
+** wrapper for that function).
+**
+** If there are no tags then this function returns NULL, not an empty
+** Array.
+*/
+cson_value * json_tags_for_checkin_rid(int rid, int propagatingOnly){
+ cson_value * v = NULL;
+ char * tags = info_tags_of_checkin(rid, propagatingOnly);
+ if(tags){
+ if(*tags){
+ v = json_string_split2(tags,',',0);
+ }
+ fossil_free(tags);
+ }
+ return v;
+}
+
+/*
+ ** Returns a "new" value representing the boolean value of zVal
+ ** (false if zVal is NULL). Note that cson does not really allocate
+ ** any memory for boolean values, but they "should" (for reasons of
+ ** style and philosophy) be cleaned up like any other values (but
+ ** it's a no-op for bools).
+ */
+cson_value * json_value_to_bool(cson_value const * zVal){
+ return cson_value_get_bool(zVal)
+ ? cson_value_true()
+ : cson_value_false();
+}
+
+/*
+** Impl of /json/resultCodes
+**
+*/
+cson_value * json_page_resultCodes(void){
+ cson_array * list = cson_new_array();
+ cson_object * obj = NULL;
+ cson_string * kRC;
+ cson_string * kSymbol;
+ cson_string * kNumber;
+ cson_string * kDesc;
+ cson_array_reserve( list, 35 );
+ kRC = cson_new_string("resultCode",10);
+ kSymbol = cson_new_string("cSymbol",7);
+ kNumber = cson_new_string("number",6);
+ kDesc = cson_new_string("description",11);
+#define C(K) obj = cson_new_object(); \
+ cson_object_set_s(obj, kRC,json_new_string(json_rc_cstr(FSL_JSON_E_##K))); \
+ cson_object_set_s(obj, kSymbol, json_new_string("FSL_JSON_E_"#K) ); \
+ cson_object_set_s(obj, kNumber, cson_value_new_integer(FSL_JSON_E_##K) ); \
+ cson_object_set_s(obj, kDesc, \
+ json_new_string(json_err_cstr(FSL_JSON_E_##K))); \
+ cson_array_append( list, cson_object_value(obj) ); obj = NULL;
+
+ C(GENERIC);
+ C(INVALID_REQUEST);
+ C(UNKNOWN_COMMAND);
+ C(UNKNOWN);
+ C(TIMEOUT);
+ C(ASSERT);
+ C(ALLOC);
+ C(NYI);
+ C(PANIC);
+ C(MANIFEST_READ_FAILED);
+ C(FILE_OPEN_FAILED);
+
+ C(AUTH);
+ C(MISSING_AUTH);
+ C(DENIED);
+ C(WRONG_MODE);
+ C(LOGIN_FAILED);
+ C(LOGIN_FAILED_NOSEED);
+ C(LOGIN_FAILED_NONAME);
+ C(LOGIN_FAILED_NOPW);
+ C(LOGIN_FAILED_NOTFOUND);
+
+ C(USAGE);
+ C(INVALID_ARGS);
+ C(MISSING_ARGS);
+ C(AMBIGUOUS_UUID);
+ C(UNRESOLVED_UUID);
+ C(RESOURCE_ALREADY_EXISTS);
+ C(RESOURCE_NOT_FOUND);
+
+ C(DB);
+ C(STMT_PREP);
+ C(STMT_BIND);
+ C(STMT_EXEC);
+ C(DB_LOCKED);
+ C(DB_NEEDS_REBUILD);
+ C(DB_NOT_FOUND);
+ C(DB_NOT_VALID);
+#undef C
+ return cson_array_value(list);
+}
+
+
+/*
+** /json/version implementation.
+**
+** Returns the payload object (owned by the caller).
+*/
+cson_value * json_page_version(void){
+ cson_value * jval = NULL;
+ cson_object * jobj = NULL;
+ jval = cson_value_new_object();
+ jobj = cson_value_get_object(jval);
+#define FSET(X,K) cson_object_set( jobj, K, cson_value_new_string(X,strlen(X)))
+ FSET(MANIFEST_UUID,"manifestUuid");
+ FSET(MANIFEST_VERSION,"manifestVersion");
+ FSET(MANIFEST_DATE,"manifestDate");
+ FSET(MANIFEST_YEAR,"manifestYear");
+ FSET(RELEASE_VERSION,"releaseVersion");
+ cson_object_set( jobj, "releaseVersionNumber",
+ cson_value_new_integer(RELEASE_VERSION_NUMBER) );
+ cson_object_set( jobj, "resultCodeParanoiaLevel",
+ cson_value_new_integer(g.json.errorDetailParanoia) );
+ FSET(FOSSIL_JSON_API_VERSION, "jsonApiVersion" );
+#undef FSET
+ return jval;
+}
+
+
+/*
+** Returns the current user's capabilities string as a String value.
+** Returned value is owned by the caller, and will only be NULL if
+** g.userUid is invalid or an out of memory error. Or, it turns out,
+** in CLI mode (where there is no logged-in user).
+*/
+cson_value * json_cap_value(){
+ if(g.userUid<=0){
+ return NULL;
+ }else{
+ Stmt q = empty_Stmt;
+ cson_value * val = NULL;
+ db_prepare(&q, "SELECT cap FROM user WHERE uid=%d", g.userUid);
+ if( db_step(&q)==SQLITE_ROW ){
+ char const * str = (char const *)sqlite3_column_text(q.pStmt,0);
+ if( str ){
+ val = json_new_string(str);
+ }
+ }
+ db_finalize(&q);
+ return val;
+ }
+}
+
+/*
+** Implementation for /json/cap
+**
+** Returned object contains details about the "capabilities" of the
+** current user (what he may/may not do).
+**
+** This is primarily intended for debuggering, but may have
+** a use in client code. (?)
+*/
+cson_value * json_page_cap(void){
+ cson_value * payload = cson_value_new_object();
+ cson_value * sub = cson_value_new_object();
+ Stmt q;
+ cson_object * obj = cson_value_get_object(payload);
+ db_prepare(&q, "SELECT login, cap FROM user WHERE uid=%d", g.userUid);
+ if( db_step(&q)==SQLITE_ROW ){
+ /* reminder: we don't use g.zLogin because it's 0 for the guest
+ user and the HTML UI appears to currently allow the name to be
+ changed (but doing so would break other code). */
+ char const * str = (char const *)sqlite3_column_text(q.pStmt,0);
+ if( str ){
+ cson_object_set( obj, "name",
+ cson_value_new_string(str,strlen(str)) );
+ }
+ str = (char const *)sqlite3_column_text(q.pStmt,1);
+ if( str ){
+ cson_object_set( obj, "capabilities",
+ cson_value_new_string(str,strlen(str)) );
+ }
+ }
+ db_finalize(&q);
+ cson_object_set( obj, "permissionFlags", sub );
+ obj = cson_value_get_object(sub);
+
+#define ADD(X,K) cson_object_set(obj, K, cson_value_new_bool(g.perm.X))
+ ADD(Setup,"setup");
+ ADD(Admin,"admin");
+ ADD(Password,"password");
+ ADD(Write,"checkin");
+ ADD(Read,"checkout");
+ ADD(Hyperlink,"history");
+ ADD(Clone,"clone");
+ ADD(RdWiki,"readWiki");
+ ADD(NewWiki,"createWiki");
+ ADD(ApndWiki,"appendWiki");
+ ADD(WrWiki,"editWiki");
+ ADD(ModWiki,"moderateWiki");
+ ADD(RdTkt,"readTicket");
+ ADD(NewTkt,"createTicket");
+ ADD(ApndTkt,"appendTicket");
+ ADD(WrTkt,"editTicket");
+ ADD(ModTkt,"moderateTicket");
+ ADD(Attach,"attachFile");
+ ADD(TktFmt,"createTicketReport");
+ ADD(RdAddr,"readPrivate");
+ ADD(Zip,"zip");
+ ADD(Private,"xferPrivate");
+ ADD(WrUnver,"writeUnversioned");
+ ADD(RdForum,"readForum");
+ ADD(WrForum,"writeForum");
+ ADD(WrTForum,"writeTrustedForum");
+ ADD(ModForum,"moderateForum");
+ ADD(AdminForum,"adminForum");
+ ADD(EmailAlert,"emailAlert");
+ ADD(Announce,"announce");
+ ADD(Debug,"debug");
+ ADD(Chat,"chat");
+#undef ADD
+ return payload;
+}
+
+/*
+** Implementation of the /json/stat page/command.
+**
+*/
+cson_value * json_page_stat(void){
+ i64 t, fsize;
+ int n, m;
+ int full;
+ enum { BufLen = 1000 };
+ char zBuf[BufLen];
+ cson_value * jv = NULL;
+ cson_object * jo = NULL;
+ cson_value * jv2 = NULL;
+ cson_object * jo2 = NULL;
+ char * zTmp = NULL;
+ if( !g.perm.Read ){
+ json_set_err(FSL_JSON_E_DENIED,
+ "Requires 'o' permissions.");
+ return NULL;
+ }
+ full = json_find_option_bool("full",NULL,"f",
+ json_find_option_bool("verbose",NULL,"v",0));
+#define SETBUF(O,K) cson_object_set(O, K, \
+ cson_value_new_string(zBuf, strlen(zBuf)));
+
+ jv = cson_value_new_object();
+ jo = cson_value_get_object(jv);
+
+ zTmp = db_get("project-name",NULL);
+ cson_object_set(jo, "projectName", json_new_string(zTmp));
+ fossil_free(zTmp);
+ zTmp = db_get("project-description",NULL);
+ cson_object_set(jo, "projectDescription", json_new_string(zTmp));
+ fossil_free(zTmp);
+ zTmp = NULL;
+ fsize = file_size(g.zRepositoryName, ExtFILE);
+ cson_object_set(jo, "repositorySize",
+ cson_value_new_integer((cson_int_t)fsize));
+
+ if(full){
+ n = db_int(0, "SELECT count(*) FROM blob");
+ m = db_int(0, "SELECT count(*) FROM delta");
+ cson_object_set(jo, "blobCount", cson_value_new_integer((cson_int_t)n));
+ cson_object_set(jo, "deltaCount", cson_value_new_integer((cson_int_t)m));
+ if( n>0 ){
+ int a, b;
+ Stmt q;
+ db_prepare(&q, "SELECT total(size), avg(size), max(size)"
+ " FROM blob WHERE size>0");
+ db_step(&q);
+ t = db_column_int64(&q, 0);
+ cson_object_set(jo, "uncompressedArtifactSize",
+ cson_value_new_integer((cson_int_t)t));
+ cson_object_set(jo, "averageArtifactSize",
+ cson_value_new_integer((cson_int_t)db_column_int(&q, 1)));
+ cson_object_set(jo, "maxArtifactSize",
+ cson_value_new_integer((cson_int_t)db_column_int(&q, 2)));
+ db_finalize(&q);
+ if( t/fsize < 5 ){
+ b = 10;
+ fsize /= 10;
+ }else{
+ b = 1;
+ }
+ a = t/fsize;
+ sqlite3_snprintf(BufLen,zBuf, "%d:%d", a, b);
+ SETBUF(jo, "compressionRatio");
+ }
+ n = db_int(0, "SELECT count(distinct mid) FROM mlink /*scan*/");
+ cson_object_set(jo, "checkinCount", cson_value_new_integer((cson_int_t)n));
+ n = db_int(0, "SELECT count(*) FROM filename /*scan*/");
+ cson_object_set(jo, "fileCount", cson_value_new_integer((cson_int_t)n));
+ n = db_int(0, "SELECT count(*) FROM tag /*scan*/"
+ " WHERE +tagname GLOB 'wiki-*'");
+ cson_object_set(jo, "wikiPageCount", cson_value_new_integer((cson_int_t)n));
+ n = db_int(0, "SELECT count(*) FROM tag /*scan*/"
+ " WHERE +tagname GLOB 'tkt-*'");
+ cson_object_set(jo, "ticketCount", cson_value_new_integer((cson_int_t)n));
+ }/*full*/
+ n = db_int(0, "SELECT julianday('now') - (SELECT min(mtime) FROM event)"
+ " + 0.99");
+ cson_object_set(jo, "ageDays", cson_value_new_integer((cson_int_t)n));
+ cson_object_set(jo, "ageYears", cson_value_new_double(n/365.2425));
+ sqlite3_snprintf(BufLen, zBuf, db_get("project-code",""));
+ SETBUF(jo, "projectCode");
+ cson_object_set(jo, "compiler",
+ cson_value_new_string(COMPILER_NAME, strlen(COMPILER_NAME)));
+
+ jv2 = cson_value_new_object();
+ jo2 = cson_value_get_object(jv2);
+ cson_object_set(jo, "sqlite", jv2);
+ sqlite3_snprintf(BufLen, zBuf, "%.19s [%.10s] (%s)", sqlite3_sourceid(),
+ &sqlite3_sourceid()[20], sqlite3_libversion());
+ SETBUF(jo2, "version");
+ cson_object_set(jo2, "pageCount", cson_value_new_integer(
+ (cson_int_t)db_int(0, "PRAGMA repository.page_count")));
+ cson_object_set(jo2, "pageSize", cson_value_new_integer(
+ (cson_int_t)db_int(0, "PRAGMA repository.page_size")));
+ cson_object_set(jo2, "freeList", cson_value_new_integer(
+ (cson_int_t)db_int(0, "PRAGMA repository.freelist_count")));
+ sqlite3_snprintf(BufLen, zBuf, "%s", db_text(0,"PRAGMA repository.encoding"));
+ SETBUF(jo2, "encoding");
+ sqlite3_snprintf(BufLen, zBuf, "%s",
+ db_text(0, "PRAGMA repository.journal_mode"));
+ cson_object_set(jo2, "journalMode", *zBuf ?
+ cson_value_new_string(zBuf, strlen(zBuf)) : cson_value_null());
+ return jv;
+#undef SETBUF
+}
+
+
+
+
+/*
+** Creates a comma-separated list of command names
+** taken from zPages. zPages must be an array of objects
+** whose final entry MUST have a NULL name value or results
+** are undefined.
+**
+** The list is appended to pOut. The number of items (not bytes)
+** appended are returned. If filterByMode is non-0 then the result
+** list will contain only commands which are able to run in the
+** current run mode (CLI vs. HTTP).
+*/
+static int json_pagedefs_to_string(JsonPageDef const * zPages,
+ Blob * pOut, int filterByMode){
+ int i = 0;
+ for( ; zPages->name; ++zPages, ++i ){
+ if(filterByMode){
+ if(g.isHTTP && zPages->runMode < 0) continue;
+ else if(zPages->runMode > 0) continue;
+ }
+ blob_append(pOut, zPages->name, -1);
+ if((zPages+1)->name){
+ blob_append(pOut, ", ",2);
+ }
+ }
+ return i;
+}
+
+/*
+** Creates an error message using zErrPrefix and the given array of
+** JSON command definitions, and sets the g.json error state to
+** reflect FSL_JSON_E_MISSING_ARGS. If zErrPrefix is NULL then
+** some default is used (e.g. "Try one of: "). If it is "" then
+** no prefix is used.
+**
+** The intention is to provide the user (via the response.resultText)
+** a list of available commands/subcommands.
+**
+*/
+void json_dispatch_missing_args_err( JsonPageDef const * pCommands,
+ char const * zErrPrefix ){
+ Blob cmdNames = empty_blob;
+ blob_init(&cmdNames,NULL,0);
+ if( !zErrPrefix ) {
+ zErrPrefix = "Try one of: ";
+ }
+ blob_append( &cmdNames, zErrPrefix, strlen(zErrPrefix) );
+ json_pagedefs_to_string(pCommands, &cmdNames, 1);
+ json_set_err(FSL_JSON_E_MISSING_ARGS, "%s",
+ blob_str(&cmdNames));
+ blob_reset(&cmdNames);
+}
+
+cson_value * json_page_dispatch_helper(JsonPageDef const * pages){
+ JsonPageDef const * def;
+ char const * cmd = json_command_arg(1+g.json.dispatchDepth);
+ assert( NULL != pages );
+ if( ! cmd ){
+ json_dispatch_missing_args_err(pages,
+ "No subcommand specified. "
+ "Try one of: ");
+ return NULL;
+ }
+ def = json_handler_for_name( cmd, pages );
+ if(!def){
+ json_set_err(FSL_JSON_E_UNKNOWN_COMMAND,
+ "Unknown subcommand: %s", cmd);
+ return NULL;
+ }
+ else{
+ ++g.json.dispatchDepth;
+ return (*def->func)();
+ }
+}
+
+
+/*
+** Impl of /json/rebuild. Requires admin privileges.
+*/
+static cson_value * json_page_rebuild(void){
+ if( !g.perm.Admin ){
+ json_set_err(FSL_JSON_E_DENIED,"Requires 'a' privileges.");
+ return NULL;
+ }else{
+ /* Reminder: the db_xxx() ops "should" fail via the fossil core
+ error handlers, which will cause a JSON error and exit(). i.e. we
+ don't handle the errors here. TODO: confirm that all these db
+ routine fail gracefully in JSON mode.
+
+ On large repos (e.g. fossil's) this operation is likely to take
+ longer than the client timeout, which will cause it to fail (but
+ it's sqlite3, so it'll fail gracefully).
+ */
+ db_close(1);
+ db_open_repository(g.zRepositoryName);
+ db_begin_transaction();
+ rebuild_db(0, 0);
+ db_end_transaction(0);
+ return NULL;
+ }
+}
+
+/*
+** Impl of /json/g. Requires admin/setup rights.
+*/
+static cson_value * json_page_g(void){
+ if(!g.perm.Admin || !g.perm.Setup){
+ json_set_err(FSL_JSON_E_DENIED,
+ "Requires 'a' or 's' privileges.");
+ return NULL;
+ }
+ return json_g_to_json();
+}
+
+/* Impl in json_login.c. */
+cson_value * json_page_anon_password(void);
+/* Impl in json_artifact.c. */
+cson_value * json_page_artifact(void);
+/* Impl in json_branch.c. */
+cson_value * json_page_branch(void);
+/* Impl in json_diff.c. */
+cson_value * json_page_diff(void);
+/* Impl in json_dir.c. */
+cson_value * json_page_dir(void);
+/* Impl in json_login.c. */
+cson_value * json_page_login(void);
+/* Impl in json_login.c. */
+cson_value * json_page_logout(void);
+/* Impl in json_query.c. */
+cson_value * json_page_query(void);
+/* Impl in json_report.c. */
+cson_value * json_page_report(void);
+/* Impl in json_tag.c. */
+cson_value * json_page_tag(void);
+/* Impl in json_user.c. */
+cson_value * json_page_user(void);
+/* Impl in json_config.c. */
+cson_value * json_page_config(void);
+/* Impl in json_finfo.c. */
+cson_value * json_page_finfo(void);
+/* Impl in json_status.c. */
+cson_value * json_page_status(void);
+
+/*
+** Mapping of names to JSON pages/commands. Each name is a subpath of
+** /json (in CGI mode) or a subcommand of the json command in CLI mode
+*/
+static const JsonPageDef JsonPageDefs[] = {
+/* please keep alphabetically sorted (case-insensitive)
+ for maintenance reasons. */
+{"anonymousPassword", json_page_anon_password, 0},
+{"artifact", json_page_artifact, 0},
+{"branch", json_page_branch,0},
+{"cap", json_page_cap, 0},
+{"config", json_page_config, 0 },
+{"diff", json_page_diff, 0},
+{"dir", json_page_dir, 0},
+{"finfo", json_page_finfo, 0},
+{"g", json_page_g, 0},
+{"HAI",json_page_version,0},
+{"login",json_page_login,0},
+{"logout",json_page_logout,0},
+{"query",json_page_query,0},
+{"rebuild",json_page_rebuild,0},
+{"report", json_page_report, 0},
+{"resultCodes", json_page_resultCodes,0},
+{"settings",json_page_settings,0},
+{"stat",json_page_stat,0},
+{"status", json_page_status, 0},
+{"tag", json_page_tag,0},
+/*{"ticket", json_page_nyi,0},*/
+{"timeline", json_page_timeline,0},
+{"user",json_page_user,0},
+{"version",json_page_version,0},
+{"whoami",json_page_whoami,0},
+{"wiki",json_page_wiki,0},
+/* Last entry MUST have a NULL name. */
+{NULL,NULL,0}
+};
+
+/*
+** Internal helper for json_cmd_top() and json_page_top().
+**
+** Searches JsonPageDefs for a command with the given name. If found,
+** it is used to generate and output a JSON response. If not found, it
+** generates a JSON-style error response. Returns 0 on success, non-0
+** on error. On error it will set g.json's error state.
+*/
+static int json_dispatch_root_command( char const * zCommand ){
+ int rc = 0;
+ cson_value * payload = NULL;
+ JsonPageDef const * pageDef = NULL;
+ pageDef = json_handler_for_name(zCommand,&JsonPageDefs[0]);
+ if( ! pageDef ){
+ rc = FSL_JSON_E_UNKNOWN_COMMAND;
+ json_set_err( rc, "Unknown command: %s", zCommand );
+ }else if( pageDef->runMode < 0 /*CLI only*/){
+ rc = FSL_JSON_E_WRONG_MODE;
+ }else if( (g.isHTTP && (pageDef->runMode < 0 /*CLI only*/))
+ ||
+ (!g.isHTTP && (pageDef->runMode > 0 /*HTTP only*/))
+ ){
+ rc = FSL_JSON_E_WRONG_MODE;
+ }
+ else{
+ rc = 0;
+ g.json.dispatchDepth = 1;
+ payload = (*pageDef->func)();
+ }
+ payload = json_create_response(rc, NULL, payload);
+ json_send_response(payload);
+ cson_value_free(payload);
+ return rc;
+}
+
+#ifdef FOSSIL_ENABLE_JSON
+/* dupe ifdef needed for mkindex */
+/*
+** WEBPAGE: json
+**
+** Pages under /json/... must be entered into JsonPageDefs.
+** This function dispatches them, and is the HTTP equivalent of
+** json_cmd_top().
+*/
+void json_page_top(void){
+ char const * zCommand;
+ assert(g.json.gc.a && "json_bootstrap_early() was not called!");
+ assert(g.json.cmd.a && "json_bootstrap_late() was not called!");
+ zCommand = json_command_arg(1);
+ if(!zCommand || !*zCommand){
+ json_dispatch_missing_args_err( JsonPageDefs,
+ "No command (sub-path) specified."
+ " Try one of: ");
+ return;
+ }
+ json_dispatch_root_command( zCommand );
+}
+#endif /* FOSSIL_ENABLE_JSON for mkindex */
+
+#ifdef FOSSIL_ENABLE_JSON
+/* dupe ifdef needed for mkindex */
+/*
+** This function dispatches json commands and is the CLI equivalent of
+** json_page_top().
+**
+** COMMAND: json
+**
+** Usage: %fossil json SUBCOMMAND ?OPTIONS?
+**
+** In CLI mode, the -R REPO common option is supported. Due to limitations
+** in the argument dispatching code, any -FLAGS must come after the final
+** sub- (or subsub-) command.
+**
+** The -json-input FILE option can be used to read JSON data and process
+** it like the HTTP interface would. For example:
+**
+** %fossil json -json-input my.json
+**
+** The commands include:
+**
+** anonymousPassword
+** artifact
+** branch
+** cap
+** config
+** diff
+** dir
+** g
+** login
+** logout
+** query
+** rebuild
+** report
+** resultCodes
+** stat
+** tag
+** timeline
+** user
+** version (alias: HAI)
+** whoami
+** wiki
+**
+** Run '%fossil json' without any subcommand to see the full list (but be
+** aware that some listed might not yet be fully implemented).
+**
+*/
+void json_cmd_top(void){
+ char const * cmd = NULL;
+ int rc = 0;
+ memset( &g.perm, 0xff, sizeof(g.perm) )
+ /* In CLI mode fossil does not use permissions
+ and they all default to false. We enable them
+ here because (A) fossil doesn't use them in local
+ mode but (B) having them set gives us one less
+ difference in the CLI/CGI/Server-mode JSON
+ handling.
+ */
+ ;
+ json_bootstrap_early();
+ json_bootstrap_late();
+ if( 2 > cson_array_length_get(g.json.cmd.a) ){
+ goto usage;
+ }
+#if 0
+ json_warn(FSL_JSON_W_ROW_TO_JSON_FAILED, "Just testing.");
+ json_warn(FSL_JSON_W_ROW_TO_JSON_FAILED, "Just testing again.");
+#endif
+ cmd = json_command_arg(1);
+ if( !cmd || !*cmd ){
+ goto usage;
+ }
+ rc = json_dispatch_root_command( cmd );
+ if(0 != rc){
+ /* FIXME: we need a way of passing this error back
+ up to the routine which called this callback.
+ e.g. add g.errCode.
+ */
+ fossil_exit(1);
+ }
+ return;
+ usage:
+ {
+ cson_value * payload;
+ json_dispatch_missing_args_err( JsonPageDefs,
+ "No subcommand specified."
+ " Try one of: ");
+ payload = json_create_response(0, NULL, NULL);
+ json_send_response(payload);
+ cson_value_free(payload);
+ fossil_exit(1);
+ }
+}
+#endif /* FOSSIL_ENABLE_JSON for mkindex */
+
+#endif /* FOSSIL_ENABLE_JSON */