diff options
Diffstat (limited to 'src/json_wiki.c')
| -rw-r--r-- | src/json_wiki.c | 652 |
1 files changed, 652 insertions, 0 deletions
diff --git a/src/json_wiki.c b/src/json_wiki.c new file mode 100644 index 0000000..b427fae --- /dev/null +++ b/src/json_wiki.c @@ -0,0 +1,652 @@ +#ifdef FOSSIL_ENABLE_JSON +/* +** Copyright (c) 2011-12 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(void); +static cson_value * json_wiki_get(void); +static cson_value * json_wiki_list(void); +static cson_value * json_wiki_preview(void); +static cson_value * json_wiki_save(void); +static cson_value * json_wiki_diff(void); +/* +** Mapping of /json/wiki/XXX commands/paths to callbacks. +*/ +static const JsonPageDef JsonPageDefs_Wiki[] = { +{"create", json_wiki_create, 0}, +{"diff", json_wiki_diff, 0}, +{"get", json_wiki_get, 0}, +{"list", json_wiki_list, 0}, +{"preview", json_wiki_preview, 0}, +{"save", json_wiki_save, 0}, +{"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(void){ + return json_page_dispatch_helper(JsonPageDefs_Wiki); +} + +/* +** Returns the UUID for the given wiki blob RID, or NULL if not +** found. The returned string is allocated via db_text() and must be +** free()d by the caller. +*/ +char * json_wiki_get_uuid_for_rid( int rid ) +{ + return db_text(NULL, + "SELECT b.uuid FROM tag t, tagxref x, blob b" + " WHERE x.tagid=t.tagid AND t.tagname GLOB 'wiki-*' " + " AND b.rid=x.rid AND b.rid=%d" + " ORDER BY x.mtime DESC LIMIT 1", + rid + ); +} + +/* +** Tries to load a wiki page from the given rid creates a JSON object +** representation of it. If the page is not found then NULL is +** returned. If contentFormat is positive 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_rid(int rid, int contentFormat){ + Manifest * pWiki = NULL; + if( NULL == (pWiki = manifest_get(rid, CFTYPE_WIKI, 0)) ){ + json_set_err( FSL_JSON_E_UNKNOWN, + "Error reading wiki page from manifest (rid=%d).", + rid ); + return NULL; + }else{ + unsigned int len = 0; + cson_object * pay = cson_new_object(); + char const * zBody = pWiki->zWiki; + char const * zFormat = NULL; + char * zUuid = json_wiki_get_uuid_for_rid(rid); + cson_object_set(pay,"name",json_new_string(pWiki->zWikiTitle)); + cson_object_set(pay,"uuid",json_new_string(zUuid)); + free(zUuid); + zUuid = NULL; + if( pWiki->nParent > 0 ){ + cson_object_set( pay, "parent", json_new_string(pWiki->azParent[0]) ) + /* Reminder: wiki pages do not branch and have only one parent + (except for the initial version, which has no parents). */; + } + /*cson_object_set(pay,"rid",json_new_int((cson_int_t)rid));*/ + cson_object_set(pay,"user",json_new_string(pWiki->zUser)); + cson_object_set(pay,FossilJsonKeys.timestamp, + json_julian_to_timestamp(pWiki->rDate)); + if(0 == contentFormat){ + cson_object_set(pay,"size", + json_new_int((cson_int_t)(zBody?strlen(zBody):0))); + }else{ + if( contentFormat>0 ){/*HTML-ize it*/ + Blob content = empty_blob; + Blob raw = empty_blob; + zFormat = "html"; + if(zBody && *zBody){ + const char *zMimetype = pWiki->zMimetype; + if( zMimetype==0 ) zMimetype = "text/x-fossil-wiki"; + zMimetype = wiki_filter_mimetypes(zMimetype); + blob_append(&raw,zBody,-1); + if( fossil_strcmp(zMimetype, "text/x-fossil-wiki")==0 ){ + wiki_convert(&raw,&content,0); + }else if( fossil_strcmp(zMimetype, "text/x-markdown")==0 ){ + markdown_to_html(&raw,0,&content); + }else if( fossil_strcmp(zMimetype, "text/x-org")==0 ){ + org_to_html_wiki(&raw,&content); + }else if( fossil_strcmp(zMimetype, "text/plain")==0 ){ + htmlize_to_blob(&content,blob_str(&raw),blob_size(&raw)); + }else{ + json_set_err( FSL_JSON_E_UNKNOWN, + "Unsupported MIME type '%s' for wiki page '%s'.", + zMimetype, pWiki->zWikiTitle ); + blob_reset(&content); + blob_reset(&raw); + cson_free_object(pay); + manifest_destroy(pWiki); + return NULL; + } + len = (unsigned int)blob_size(&content); + } + cson_object_set(pay,"size",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*/ + zFormat = "raw"; + len = zBody ? strlen(zBody) : 0; + cson_object_set(pay,"size",json_new_int((cson_int_t)len)); + cson_object_set(pay,"content",cson_value_new_string(zBody,len)); + } + cson_object_set(pay,"contentFormat",json_new_string(zFormat)); + + } + /*TODO: add 'T' (tag) fields*/ + /*TODO: add the 'A' card (file attachment) entries?*/ + manifest_destroy(pWiki); + return cson_object_value(pay); + } +} + +/* +** Searches for the latest version of a wiki page with the given +** name. If found it behaves like json_get_wiki_page_by_rid(theRid, +** contentFormat), else it returns NULL. +*/ +cson_value * json_get_wiki_page_by_name(char const * zPageName, + int contentFormat){ + int rid; + rid = db_int(0, + "SELECT x.rid 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( 0==rid ){ + json_set_err( FSL_JSON_E_RESOURCE_NOT_FOUND, "Wiki page not found: %s", + zPageName ); + return NULL; + } + return json_get_wiki_page_by_rid(rid, contentFormat); +} + + +/* +** Searches json_find_option_ctr("format",NULL,"f") for a flag. +** If not found it returns defaultValue else it returns a value +** depending on the first character of the format option: +** +** [h]tml = 1 +** [n]one = 0 +** [r]aw = -1 +** +** The return value is intended for use with +** json_get_wiki_page_by_rid() and friends. +*/ +int json_wiki_get_content_format_flag( int defaultValue ){ + int contentFormat = defaultValue; + char const * zFormat = json_find_option_cstr("format",NULL,"f"); + if( !zFormat || !*zFormat ){ + return contentFormat; + } + else if('r'==*zFormat){ + contentFormat = -1; + } + else if('h'==*zFormat){ + contentFormat = 1; + } + else if('n'==*zFormat){ + contentFormat = 0; + } + return contentFormat; +} + +/* +** Helper for /json/wiki/get and /json/wiki/preview. At least one of +** zPageName (wiki page name) or zSymname must be set to a +** non-empty/non-NULL value. zSymname takes precedence. On success +** the result of one of json_get_wiki_page_by_rid() or +** json_get_wiki_page_by_name() will be returned (owned by the +** caller). On error g.json's error state is set and NULL is returned. +*/ +static cson_value * json_wiki_get_by_name_or_symname(char const * zPageName, + char const * zSymname, + int contentFormat ){ + if(!zSymname || !*zSymname){ + return json_get_wiki_page_by_name(zPageName, contentFormat); + }else{ + int rid = symbolic_name_to_rid( zSymname ? zSymname : zPageName, "w" ); + if(rid<0){ + json_set_err(FSL_JSON_E_AMBIGUOUS_UUID, + "UUID [%s] is ambiguous.", zSymname); + return NULL; + }else if(rid==0){ + json_set_err(FSL_JSON_E_RESOURCE_NOT_FOUND, + "UUID [%s] does not resolve to a wiki page.", zSymname); + return NULL; + }else{ + return json_get_wiki_page_by_rid(rid, contentFormat); + } + } +} + +/* +** Implementation of /json/wiki/get. +** +*/ +static cson_value * json_wiki_get(void){ + char const * zPageName; + char const * zSymName = NULL; + int contentFormat = -1; + 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_cstr2("name",NULL,"n",g.json.dispatchDepth+1); + + zSymName = json_find_option_cstr("uuid",NULL,"u"); + + if((!zPageName||!*zPageName) && (!zSymName || !*zSymName)){ + json_set_err(FSL_JSON_E_MISSING_ARGS, + "At least one of the 'name' or 'uuid' arguments must be provided."); + return NULL; + } + + /* TODO: see if we have a page named zPageName. If not, try to resolve + zPageName as a UUID. + */ + + contentFormat = json_wiki_get_content_format_flag(contentFormat); + return json_wiki_get_by_name_or_symname( zPageName, zSymName, contentFormat ); +} + +/* +** Implementation of /json/wiki/preview. +*/ +static cson_value * json_wiki_preview(void){ + char const * zContent = NULL; + char const * zMime = NULL; + cson_string * sContent = NULL; + cson_value * pay = NULL; + Blob contentOrig = empty_blob; + Blob contentHtml = empty_blob; + if( !g.perm.WrWiki ){ + json_set_err(FSL_JSON_E_DENIED, + "Requires 'k' access."); + return NULL; + } + + if(g.json.reqPayload.o){ + sContent = cson_value_get_string( + cson_object_get(g.json.reqPayload.o, "body")); + zMime = cson_value_get_cstr(cson_object_get(g.json.reqPayload.o, + "mimetype")); + }else{ + sContent = cson_value_get_string(g.json.reqPayload.v); + } + if(!sContent) { + json_set_err(FSL_JSON_E_MISSING_ARGS, + "The 'payload' property must be either a string containing the " + "Fossil wiki code to preview or an object with body + mimetype " + "properties."); + return NULL; + } + zContent = cson_string_cstr(sContent); + blob_append( &contentOrig, zContent, (int)cson_string_length_bytes(sContent)); + zMime = wiki_filter_mimetypes(zMime); + if( 0==fossil_strcmp(zMime, "text/x-markdown") ){ + markdown_to_html(&contentOrig, 0, &contentHtml); + }else if( 0==fossil_strcmp(zMime, "text/x-org") ){ + org_to_html_wiki(&contentOrig, &contentHtml); + }else if( 0==fossil_strcmp(zMime, "text/plain") ){ + blob_append(&contentHtml, "<pre class='textPlain'>", -1); + blob_append(&contentHtml, blob_str(&contentOrig), blob_size(&contentOrig)); + blob_append(&contentHtml, "</pre>", -1); + }else{ + wiki_convert( &contentOrig, &contentHtml, 0 ); + } + blob_reset( &contentOrig ); + pay = cson_value_new_string( blob_str(&contentHtml), + (unsigned int)blob_size(&contentHtml)); + blob_reset( &contentHtml ); + return pay; +} + + +/* +** 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 allowCreateIfNotExists 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 allowCreateIfNotExists){ + 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. */ + char const * zMimeType = 0; + Manifest *pPrior = 0; + 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)); + if(!zPageName || !*zPageName){ + json_set_err(FSL_JSON_E_INVALID_ARGS, + "'name' parameter must be a non-empty string."); + return NULL; + } + 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){ + pPrior = manifest_get(rid, CFTYPE_WIKI, 0); + if(createMode){ + json_set_err(FSL_JSON_E_RESOURCE_ALREADY_EXISTS, + "Wiki page '%s' already exists.", + zPageName); + goto error; + } + }else if(!createMode && !allowCreateIfNotExists){ + 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 && allowCreateIfNotExists) ){ + 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, + "'content' parameter must be a string."); + 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); + } + + zMimeType = json_find_option_cstr("mimetype","mimetype","M"); + if( zMimeType==0 || zMimeType[0]==0 ){ + zMimeType = pPrior && pPrior->zMimetype + ? pPrior->zMimetype : "text/x-org"; + } + zMimeType = wiki_filter_mimetypes(zMimeType); + + wiki_cmd_commit(zPageName, rid, &content, zMimeType, 0); + 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); + } + manifest_destroy(pPrior); + return payV; + +} + +/* +** Implementation of /json/wiki/create. +*/ +static cson_value * json_wiki_create(void){ + return json_wiki_create_or_save(1,0); +} + +/* +** Implementation of /json/wiki/save. +*/ +static cson_value * json_wiki_save(void){ + 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(void){ + cson_value * listV = NULL; + cson_array * list = NULL; + char const * zGlob = NULL; + Stmt q = empty_Stmt; + Blob sql = empty_blob; + char const verbose = json_find_option_bool("verbose",NULL,"v",0); + char fInvert = json_find_option_bool("invert",NULL,"i",0);; + + if( !g.perm.RdWiki && !g.perm.Read ){ + json_set_err(FSL_JSON_E_DENIED, + "Requires 'j' or 'o' permissions."); + return NULL; + } + blob_append(&sql,"SELECT" + " DISTINCT substr(tagname,6) as name" + " FROM tag JOIN tagxref USING('tagid')" + " WHERE tagname GLOB 'wiki-*'" + " AND TYPEOF(tagxref.value+0)='integer'", + /* ^^^ elide wiki- tags which are not wiki pages */ + -1); + zGlob = json_find_option_cstr("glob",NULL,"g"); + if(zGlob && *zGlob){ + blob_append_sql(&sql," AND name %s GLOB %Q", + fInvert ? "NOT" : "", zGlob); + }else{ + zGlob = json_find_option_cstr("like",NULL,"l"); + if(zGlob && *zGlob){ + blob_append_sql(&sql," AND name %s LIKE %Q", + fInvert ? "NOT" : "", zGlob); + } + } + blob_append(&sql," ORDER BY lower(name)", -1); + db_prepare(&q,"%s", blob_sql_text(&sql)); + blob_reset(&sql); + 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; +} + +static cson_value * json_wiki_diff(void){ + char const * zV1 = NULL; + char const * zV2 = NULL; + cson_object * pay = NULL; + int argPos = g.json.dispatchDepth; + int r1 = 0, r2 = 0; + Manifest * pW1 = NULL, *pW2 = NULL; + Blob w1 = empty_blob, w2 = empty_blob, d = empty_blob; + char const * zErrTag = NULL; + DiffConfig DCfg; + char * zUuid = NULL; + if( !g.perm.Hyperlink ){ + json_set_err(FSL_JSON_E_DENIED, + "Requires 'h' permissions."); + return NULL; + } + + + zV1 = json_find_option_cstr2( "v1",NULL, NULL, ++argPos ); + zV2 = json_find_option_cstr2( "v2",NULL, NULL, ++argPos ); + if(!zV1 || !*zV1 || !zV2 || !*zV2) { + json_set_err(FSL_JSON_E_INVALID_ARGS, + "Requires both 'v1' and 'v2' arguments."); + return NULL; + } + + r1 = symbolic_name_to_rid( zV1, "w" ); + zErrTag = zV1; + if(r1<0){ + goto ambiguous; + }else if(0==r1){ + goto invalid; + } + + r2 = symbolic_name_to_rid( zV2, "w" ); + zErrTag = zV2; + if(r2<0){ + goto ambiguous; + }else if(0==r2){ + goto invalid; + } + + zErrTag = zV1; + pW1 = manifest_get(r1, CFTYPE_WIKI, 0); + if( pW1==0 ) { + goto manifest; + } + zErrTag = zV2; + pW2 = manifest_get(r2, CFTYPE_WIKI, 0); + if( pW2==0 ) { + goto manifest; + } + + blob_init(&w1, pW1->zWiki, -1); + blob_zero(&w2); + blob_init(&w2, pW2->zWiki, -1); + blob_zero(&d); + diff_config_init(&DCfg, DIFF_IGNORE_EOLWS | DIFF_STRIP_EOLCR); + text_diff(&w1, &w2, &d, &DCfg); + blob_reset(&w1); + blob_reset(&w2); + + pay = cson_new_object(); + + zUuid = json_wiki_get_uuid_for_rid( pW1->rid ); + cson_object_set(pay, "v1", json_new_string(zUuid) ); + free(zUuid); + zUuid = json_wiki_get_uuid_for_rid( pW2->rid ); + cson_object_set(pay, "v2", json_new_string(zUuid) ); + free(zUuid); + zUuid = NULL; + + manifest_destroy(pW1); + manifest_destroy(pW2); + + cson_object_set(pay, "diff", + cson_value_new_string( blob_str(&d), + (unsigned int)blob_size(&d))); + + return cson_object_value(pay); + + manifest: + json_set_err(FSL_JSON_E_UNKNOWN, + "Could not load wiki manifest for UUID [%s].", + zErrTag); + goto end; + + ambiguous: + json_set_err(FSL_JSON_E_AMBIGUOUS_UUID, + "UUID [%s] is ambiguous.", zErrTag); + goto end; + + invalid: + json_set_err(FSL_JSON_E_RESOURCE_NOT_FOUND, + "UUID [%s] not found.", zErrTag); + goto end; + + end: + cson_free_object(pay); + return NULL; +} + +#endif /* FOSSIL_ENABLE_JSON */ |
