#ifdef FOSSIL_ENABLE_JSON /* ** Copyright (c) 2011 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/ ** */ #include "VERSION.h" #include "config.h" #include "json_wiki.h" #if INTERFACE #include "json_detail.h" #endif static cson_value * json_wiki_create(); static cson_value * json_wiki_get(); static cson_value * json_wiki_list(); static cson_value * json_wiki_save(); /* ** Mapping of /json/wiki/XXX commands/paths to callbacks. */ static const JsonPageDef JsonPageDefs_Wiki[] = { {"create", json_wiki_create, 1}, {"get", json_wiki_get, 0}, {"list", json_wiki_list, 0}, {"save", json_wiki_save, 1}, {"timeline", json_timeline_wiki,0}, /* Last entry MUST have a NULL name. */ {NULL,NULL,0} }; /* ** Implements the /json/wiki family of pages/commands. ** */ cson_value * json_page_wiki(){ return json_page_dispatch_helper(&JsonPageDefs_Wiki[0]); } /* ** Loads the given wiki page and creates a JSON object representation ** of it. If the page is not found then NULL is returned. If ** contentFormat is positive true then the page content is HTML-ized ** using fossil's conventional wiki format, if it is negative then no ** parsing is performed, if it is 0 then the content is not returned ** in the response. If contentFormat is 0 then the contentSize reflects ** the number of bytes, not characters, stored in the page. ** ** The returned value, if not NULL, is-a JSON Object owned by the ** caller. If it returns NULL then it may set g.json's error state. */ cson_value * json_get_wiki_page_by_name(char const * zPageName, char contentFormat){ int rid; Manifest *pWiki = 0; char * zUuid = NULL; Stmt q; db_prepare(&q, "SELECT x.rid, b.uuid FROM tag t, tagxref x, blob b" " WHERE x.tagid=t.tagid AND t.tagname='wiki-%q' " " AND b.rid=x.rid" " ORDER BY x.mtime DESC LIMIT 1", zPageName ); if( (SQLITE_ROW != db_step(&q)) ){ db_finalize(&q); json_set_err( FSL_JSON_E_RESOURCE_NOT_FOUND, "Wiki page not found: %s", zPageName ); return NULL; } rid = db_column_int(&q,0); zUuid = db_column_malloc(&q,1); db_finalize(&q); if( NULL == (pWiki = manifest_get(rid, CFTYPE_WIKI)) ){ free(zUuid); json_set_err( FSL_JSON_E_UNKNOWN, "Error reading wiki page from manifest (rid=%d, uuid=%s).", rid, zUuid ); return NULL; }else{ char const * zFormat = NULL; unsigned int len = 0; cson_object * pay = cson_new_object(); char const * zBody = pWiki->zWiki; cson_object_set(pay,"name",json_new_string(zPageName)); cson_object_set(pay,"uuid",json_new_string(zUuid)); free(zUuid); zUuid = NULL; /*cson_object_set(pay,"rid",json_new_int((cson_int_t)rid));*/ cson_object_set(pay,"lastSavedBy",json_new_string(pWiki->zUser)); cson_object_set(pay,FossilJsonKeys.timestamp, json_julian_to_timestamp(pWiki->rDate)); if(0 == contentFormat){ cson_object_set(pay,"contentLength", json_new_int((cson_int_t)(zBody?strlen(zBody):0))); }else{ cson_object_set(pay,"contentFormat",json_new_string(zFormat)); if( contentFormat>0 ){/*HTML-ize it*/ Blob content = empty_blob; Blob raw = empty_blob; if(zBody && *zBody){ blob_append(&raw,zBody,-1); wiki_convert(&raw,&content,0); len = (unsigned int)blob_size(&content); } cson_object_set(pay,"contentLength",json_new_int((cson_int_t)len)); cson_object_set(pay,"content", cson_value_new_string(blob_buffer(&content),len)); blob_reset(&content); blob_reset(&raw); }else{/*raw format*/ len = zBody ? strlen(zBody) : 0; cson_object_set(pay,"contentLength",json_new_int((cson_int_t)len)); cson_object_set(pay,"content",cson_value_new_string(zBody,len)); } } /*TODO: add 'T' (tag) fields*/ /*TODO: add the 'A' card (file attachment) entries?*/ manifest_destroy(pWiki); return cson_object_value(pay); } } /* ** Searches for a wiki page with the given rid. If found it behaves ** like json_get_wiki_page_by_name(pageName, contentFormat), else it returns ** NULL. */ cson_value * json_get_wiki_page_by_rid(int rid, char contentFormat){ char * zPageName = NULL; cson_value * rc = NULL; zPageName = db_text(NULL, "SELECT substr(t.tagname,6) AS name " " FROM tag t, tagxref x, blob b " " WHERE b.rid=%d " " AND t.tagname GLOB 'wiki-*'" " AND x.tagid=t.tagid AND b.rid=x.rid ", rid); if( zPageName ){ rc = json_get_wiki_page_by_name(zPageName, contentFormat); free(zPageName); } return rc; } /* ** Implementation of /json/wiki/get. ** */ static cson_value * json_wiki_get(){ int rid; Manifest *pWiki = 0; char const * zBody = NULL; char const * zPageName; char const * zFormat = NULL; char * zUuid = NULL; char contentFormat = -1; Stmt q; if( !g.perm.RdWiki && !g.perm.Read ){ json_set_err(FSL_JSON_E_DENIED, "Requires 'o' or 'j' access."); return NULL; } zPageName = json_find_option_cstr("name",NULL,"n") /* Damn... fossil automatically sets name to the PATH part after /json, so we need a workaround down here.... */ ; if( zPageName && (NULL != strstr(zPageName, "/"))){ /* Assume that we picked up a path remnant. */ zPageName = NULL; } if( !zPageName && cson_value_is_string(g.json.reqPayload.v) ){ zPageName = cson_string_cstr(cson_value_get_string(g.json.reqPayload.v)); } if(!zPageName){ zPageName = json_command_arg(g.json.dispatchDepth+1); } if(!zPageName||!*zPageName){ json_set_err(FSL_JSON_E_MISSING_ARGS, "'name' argument is missing."); return NULL; } zFormat = json_find_option_cstr("format",NULL,"f"); if(!zFormat || !*zFormat || ('r'==*zFormat)){ contentFormat = -1; } else if('h'==*zFormat){ contentFormat = 1; } else if('n'==*zFormat){ contentFormat = 0; } return json_get_wiki_page_by_name(zPageName, contentFormat); } /* ** Internal impl of /wiki/save and /wiki/create. If createMode is 0 ** and the page already exists then a ** FSL_JSON_E_RESOURCE_ALREADY_EXISTS error is triggered. If ** createMode is false then the FSL_JSON_E_RESOURCE_NOT_FOUND is ** triggered if the page does not already exists. ** ** Note that the error triggered when createMode==0 and no such page ** exists is rather arbitrary - we could just as well create the entry ** here if it doesn't already exist. With that, save/create would ** become one operation. That said, i expect there are people who ** would categorize such behaviour as "being too clever" or "doing too ** much automatically" (and i would likely agree with them). ** ** If allowCreateIfExists is true then this function will allow a new ** page to be created even if createMode is false. */ static cson_value * json_wiki_create_or_save(char createMode, char allowCreateIfExists){ Blob content = empty_blob; /* wiki page content */ cson_value * nameV; /* wiki page name */ char const * zPageName; /* cstr form of page name */ cson_value * contentV; /* passed-in content */ cson_value * emptyContent = NULL; /* placeholder for empty content. */ cson_value * payV = NULL; /* payload/return value */ cson_string const * jstr = NULL; /* temp for cson_value-to-cson_string conversions. */ unsigned int contentLen = 0; int rid; if( (createMode && !g.perm.NewWiki) || (!createMode && !g.perm.WrWiki)){ json_set_err(FSL_JSON_E_DENIED, "Requires '%c' permissions.", (createMode ? 'f' : 'k')); return NULL; } nameV = json_req_payload_get("name"); if(!nameV){ json_set_err( FSL_JSON_E_MISSING_ARGS, "'name' parameter is missing."); return NULL; } zPageName = cson_string_cstr(cson_value_get_string(nameV)); rid = db_int(0, "SELECT x.rid FROM tag t, tagxref x" " WHERE x.tagid=t.tagid AND t.tagname='wiki-%q'" " ORDER BY x.mtime DESC LIMIT 1", zPageName ); if(rid){ if(createMode){ json_set_err(FSL_JSON_E_RESOURCE_ALREADY_EXISTS, "Wiki page '%s' already exists.", zPageName); goto error; } }else if(!allowCreateIfExists){ json_set_err(FSL_JSON_E_RESOURCE_NOT_FOUND, "Wiki page '%s' not found.", zPageName); goto error; } contentV = json_req_payload_get("content"); if( !contentV ){ if( createMode || (!rid && allowCreateIfExists) ){ contentV = emptyContent = cson_value_new_string("",0); }else{ json_set_err(FSL_JSON_E_MISSING_ARGS, "'content' parameter is missing."); goto error; } } if( !cson_value_is_string(nameV) || !cson_value_is_string(contentV)){ json_set_err(FSL_JSON_E_INVALID_ARGS, "'name' and 'content' parameters must be strings."); goto error; } jstr = cson_value_get_string(contentV); contentLen = (int)cson_string_length_bytes(jstr); if(contentLen){ blob_append(&content, cson_string_cstr(jstr),contentLen); } wiki_cmd_commit(zPageName, 0==rid, &content); blob_reset(&content); /* Our return value here has a race condition: if this operation is called concurrently for the same wiki page via two requests, payV could reflect the results of the other save operation. */ payV = json_get_wiki_page_by_name( cson_string_cstr( cson_value_get_string(nameV)), 0); goto ok; error: assert( 0 != g.json.resultCode ); assert( NULL == payV ); ok: if( emptyContent ){ /* We have some potentially tricky memory ownership here, which is why we handle emptyContent separately. This is, in fact, overkill because cson_value_new_string("",0) actually returns a shared singleton instance (i.e. doesn't allocate), but that is a cson implementation detail which i don't want leaking into this code... */ cson_value_free(emptyContent); } return payV; } /* ** Implementation of /json/wiki/create. */ static cson_value * json_wiki_create(){ return json_wiki_create_or_save(1,0); } /* ** Implementation of /json/wiki/save. */ static cson_value * json_wiki_save(){ char const createIfNotExists = json_getenv_bool("createIfNotExists",0); return json_wiki_create_or_save(0,createIfNotExists); } /* ** Implementation of /json/wiki/list. */ static cson_value * json_wiki_list(){ cson_value * listV = NULL; cson_array * list = NULL; Stmt q; char const verbose = json_find_option_bool("verbose",NULL,"v",0); if( !g.perm.RdWiki && !g.perm.Read ){ json_set_err(FSL_JSON_E_DENIED, "Requires 'j' or 'o' permissions."); return NULL; } db_prepare(&q,"SELECT" " substr(tagname,6) as name" " FROM tag WHERE tagname GLOB 'wiki-*'" " ORDER BY lower(name)"); listV = cson_value_new_array(); list = cson_value_get_array(listV); while( SQLITE_ROW == db_step(&q) ){ cson_value * v; if( verbose ){ char const * name = db_column_text(&q,0); v = json_get_wiki_page_by_name(name,0); }else{ v = cson_sqlite3_column_to_value(q.pStmt,0); } if(!v){ json_set_err(FSL_JSON_E_UNKNOWN, "Could not convert wiki name column to JSON."); goto error; }else if( 0 != cson_array_append( list, v ) ){ cson_value_free(v); json_set_err(FSL_JSON_E_ALLOC,"Could not append wiki page name to array.") /* OOM (or maybe numeric overflow) are the only realistic error codes for that particular failure.*/; goto error; } } goto end; error: assert(0 != g.json.resultCode); cson_value_free(listV); listV = NULL; end: db_finalize(&q); return listV; } #endif /* FOSSIL_ENABLE_JSON */