1 #ifdef FOSSIL_ENABLE_JSON
2 /*
3 ** Copyright (c) 2011-12 D. Richard Hipp
4 **
5 ** This program is free software; you can redistribute it and/or
6 ** modify it under the terms of the Simplified BSD License (also
7 ** known as the "2-Clause License" or "FreeBSD License".)
8 **
9 ** This program is distributed in the hope that it will be useful,
10 ** but without any warranty; without even the implied warranty of
11 ** merchantability or fitness for a particular purpose.
12 **
13 ** Author contact information:
14 ** drh@hwaci.com
15 ** http://www.hwaci.com/drh/
16 **
17 */
18 #include "VERSION.h"
19 #include "config.h"
20 #include "json_wiki.h"
21
22 #if INTERFACE
23 #include "json_detail.h"
24 #endif
25
26 static cson_value * json_wiki_create();
27 static cson_value * json_wiki_get();
28 static cson_value * json_wiki_list();
29 static cson_value * json_wiki_preview();
30 static cson_value * json_wiki_save();
31 static cson_value * json_wiki_diff();
32 /*
33 ** Mapping of /json/wiki/XXX commands/paths to callbacks.
34 */
35 static const JsonPageDef JsonPageDefs_Wiki[] = {
36 {"create", json_wiki_create, 0},
37 {"diff", json_wiki_diff, 0},
38 {"get", json_wiki_get, 0},
39 {"list", json_wiki_list, 0},
40 {"preview", json_wiki_preview, 0},
41 {"save", json_wiki_save, 0},
42 {"timeline", json_timeline_wiki,0},
43 /* Last entry MUST have a NULL name. */
44 {NULL,NULL,0}
45 };
46
47
48 /*
49 ** Implements the /json/wiki family of pages/commands.
50 **
51 */
json_page_wiki()52 cson_value * json_page_wiki(){
53 return json_page_dispatch_helper(JsonPageDefs_Wiki);
54 }
55
56 /*
57 ** Returns the UUID for the given wiki blob RID, or NULL if not
58 ** found. The returned string is allocated via db_text() and must be
59 ** free()d by the caller.
60 */
json_wiki_get_uuid_for_rid(int rid)61 char * json_wiki_get_uuid_for_rid( int rid )
62 {
63 return db_text(NULL,
64 "SELECT b.uuid FROM tag t, tagxref x, blob b"
65 " WHERE x.tagid=t.tagid AND t.tagname GLOB 'wiki-*' "
66 " AND b.rid=x.rid AND b.rid=%d"
67 " ORDER BY x.mtime DESC LIMIT 1",
68 rid
69 );
70 }
71
72 /*
73 ** Tries to load a wiki page from the given rid creates a JSON object
74 ** representation of it. If the page is not found then NULL is
75 ** returned. If contentFormat is positive then the page content
76 ** is HTML-ized using fossil's conventional wiki format, if it is
77 ** negative then no parsing is performed, if it is 0 then the content
78 ** is not returned in the response. If contentFormat is 0 then the
79 ** contentSize reflects the number of bytes, not characters, stored in
80 ** the page.
81 **
82 ** The returned value, if not NULL, is-a JSON Object owned by the
83 ** caller. If it returns NULL then it may set g.json's error state.
84 */
json_get_wiki_page_by_rid(int rid,int contentFormat)85 cson_value * json_get_wiki_page_by_rid(int rid, int contentFormat){
86 Manifest * pWiki = NULL;
87 if( NULL == (pWiki = manifest_get(rid, CFTYPE_WIKI, 0)) ){
88 json_set_err( FSL_JSON_E_UNKNOWN,
89 "Error reading wiki page from manifest (rid=%d).",
90 rid );
91 return NULL;
92 }else{
93 unsigned int len = 0;
94 cson_object * pay = cson_new_object();
95 char const * zBody = pWiki->zWiki;
96 char const * zFormat = NULL;
97 char * zUuid = json_wiki_get_uuid_for_rid(rid);
98 cson_object_set(pay,"name",json_new_string(pWiki->zWikiTitle));
99 cson_object_set(pay,"uuid",json_new_string(zUuid));
100 free(zUuid);
101 zUuid = NULL;
102 if( pWiki->nParent > 0 ){
103 cson_object_set( pay, "parent", json_new_string(pWiki->azParent[0]) )
104 /* Reminder: wiki pages do not branch and have only one parent
105 (except for the initial version, which has no parents). */;
106 }
107 /*cson_object_set(pay,"rid",json_new_int((cson_int_t)rid));*/
108 cson_object_set(pay,"user",json_new_string(pWiki->zUser));
109 cson_object_set(pay,FossilJsonKeys.timestamp,
110 json_julian_to_timestamp(pWiki->rDate));
111 if(0 == contentFormat){
112 cson_object_set(pay,"size",
113 json_new_int((cson_int_t)(zBody?strlen(zBody):0)));
114 }else{
115 if( contentFormat>0 ){/*HTML-ize it*/
116 Blob content = empty_blob;
117 Blob raw = empty_blob;
118 zFormat = "html";
119 if(zBody && *zBody){
120 const char *zMimetype = pWiki->zMimetype;
121 if( zMimetype==0 ) zMimetype = "text/x-fossil-wiki";
122 zMimetype = wiki_filter_mimetypes(zMimetype);
123 blob_append(&raw,zBody,-1);
124 if( fossil_strcmp(zMimetype, "text/x-fossil-wiki")==0 ){
125 wiki_convert(&raw,&content,0);
126 }else if( fossil_strcmp(zMimetype, "text/x-markdown")==0 ){
127 markdown_to_html(&raw,0,&content);
128 }else if( fossil_strcmp(zMimetype, "text/plain")==0 ){
129 htmlize_to_blob(&content,blob_str(&raw),blob_size(&raw));
130 }else{
131 json_set_err( FSL_JSON_E_UNKNOWN,
132 "Unsupported MIME type '%s' for wiki page '%s'.",
133 zMimetype, pWiki->zWikiTitle );
134 blob_reset(&content);
135 blob_reset(&raw);
136 cson_free_object(pay);
137 manifest_destroy(pWiki);
138 return NULL;
139 }
140 len = (unsigned int)blob_size(&content);
141 }
142 cson_object_set(pay,"size",json_new_int((cson_int_t)len));
143 cson_object_set(pay,"content",
144 cson_value_new_string(blob_buffer(&content),len));
145 blob_reset(&content);
146 blob_reset(&raw);
147 }else{/*raw format*/
148 zFormat = "raw";
149 len = zBody ? strlen(zBody) : 0;
150 cson_object_set(pay,"size",json_new_int((cson_int_t)len));
151 cson_object_set(pay,"content",cson_value_new_string(zBody,len));
152 }
153 cson_object_set(pay,"contentFormat",json_new_string(zFormat));
154
155 }
156 /*TODO: add 'T' (tag) fields*/
157 /*TODO: add the 'A' card (file attachment) entries?*/
158 manifest_destroy(pWiki);
159 return cson_object_value(pay);
160 }
161 }
162
163 /*
164 ** Searches for the latest version of a wiki page with the given
165 ** name. If found it behaves like json_get_wiki_page_by_rid(theRid,
166 ** contentFormat), else it returns NULL.
167 */
json_get_wiki_page_by_name(char const * zPageName,int contentFormat)168 cson_value * json_get_wiki_page_by_name(char const * zPageName, int contentFormat){
169 int rid;
170 rid = db_int(0,
171 "SELECT x.rid FROM tag t, tagxref x, blob b"
172 " WHERE x.tagid=t.tagid AND t.tagname='wiki-%q' "
173 " AND b.rid=x.rid"
174 " ORDER BY x.mtime DESC LIMIT 1",
175 zPageName
176 );
177 if( 0==rid ){
178 json_set_err( FSL_JSON_E_RESOURCE_NOT_FOUND, "Wiki page not found: %s",
179 zPageName );
180 return NULL;
181 }
182 return json_get_wiki_page_by_rid(rid, contentFormat);
183 }
184
185
186 /*
187 ** Searches json_find_option_ctr("format",NULL,"f") for a flag.
188 ** If not found it returns defaultValue else it returns a value
189 ** depending on the first character of the format option:
190 **
191 ** [h]tml = 1
192 ** [n]one = 0
193 ** [r]aw = -1
194 **
195 ** The return value is intended for use with
196 ** json_get_wiki_page_by_rid() and friends.
197 */
json_wiki_get_content_format_flag(int defaultValue)198 int json_wiki_get_content_format_flag( int defaultValue ){
199 int contentFormat = defaultValue;
200 char const * zFormat = json_find_option_cstr("format",NULL,"f");
201 if( !zFormat || !*zFormat ){
202 return contentFormat;
203 }
204 else if('r'==*zFormat){
205 contentFormat = -1;
206 }
207 else if('h'==*zFormat){
208 contentFormat = 1;
209 }
210 else if('n'==*zFormat){
211 contentFormat = 0;
212 }
213 return contentFormat;
214 }
215
216 /*
217 ** Helper for /json/wiki/get and /json/wiki/preview. At least one of
218 ** zPageName (wiki page name) or zSymname must be set to a
219 ** non-empty/non-NULL value. zSymname takes precedence. On success
220 ** the result of one of json_get_wiki_page_by_rid() or
221 ** json_get_wiki_page_by_name() will be returned (owned by the
222 ** caller). On error g.json's error state is set and NULL is returned.
223 */
json_wiki_get_by_name_or_symname(char const * zPageName,char const * zSymname,int contentFormat)224 static cson_value * json_wiki_get_by_name_or_symname(char const * zPageName,
225 char const * zSymname,
226 int contentFormat ){
227 if(!zSymname || !*zSymname){
228 return json_get_wiki_page_by_name(zPageName, contentFormat);
229 }else{
230 int rid = symbolic_name_to_rid( zSymname ? zSymname : zPageName, "w" );
231 if(rid<0){
232 json_set_err(FSL_JSON_E_AMBIGUOUS_UUID,
233 "UUID [%s] is ambiguous.", zSymname);
234 return NULL;
235 }else if(rid==0){
236 json_set_err(FSL_JSON_E_RESOURCE_NOT_FOUND,
237 "UUID [%s] does not resolve to a wiki page.", zSymname);
238 return NULL;
239 }else{
240 return json_get_wiki_page_by_rid(rid, contentFormat);
241 }
242 }
243 }
244
245 /*
246 ** Implementation of /json/wiki/get.
247 **
248 */
json_wiki_get()249 static cson_value * json_wiki_get(){
250 char const * zPageName;
251 char const * zSymName = NULL;
252 int contentFormat = -1;
253 if( !g.perm.RdWiki && !g.perm.Read ){
254 json_set_err(FSL_JSON_E_DENIED,
255 "Requires 'o' or 'j' access.");
256 return NULL;
257 }
258 zPageName = json_find_option_cstr2("name",NULL,"n",g.json.dispatchDepth+1);
259
260 zSymName = json_find_option_cstr("uuid",NULL,"u");
261
262 if((!zPageName||!*zPageName) && (!zSymName || !*zSymName)){
263 json_set_err(FSL_JSON_E_MISSING_ARGS,
264 "At least one of the 'name' or 'uuid' arguments must be provided.");
265 return NULL;
266 }
267
268 /* TODO: see if we have a page named zPageName. If not, try to resolve
269 zPageName as a UUID.
270 */
271
272 contentFormat = json_wiki_get_content_format_flag(contentFormat);
273 return json_wiki_get_by_name_or_symname( zPageName, zSymName, contentFormat );
274 }
275
276 /*
277 ** Implementation of /json/wiki/preview.
278 **
279 */
json_wiki_preview()280 static cson_value * json_wiki_preview(){
281 char const * zContent = NULL;
282 cson_value * pay = NULL;
283 Blob contentOrig = empty_blob;
284 Blob contentHtml = empty_blob;
285 if( !g.perm.WrWiki ){
286 json_set_err(FSL_JSON_E_DENIED,
287 "Requires 'k' access.");
288 return NULL;
289 }
290 zContent = cson_string_cstr(cson_value_get_string(g.json.reqPayload.v));
291 if(!zContent) {
292 json_set_err(FSL_JSON_E_MISSING_ARGS,
293 "The 'payload' property must be a string containing the wiki code to preview.");
294 return NULL;
295 }
296 blob_append( &contentOrig, zContent, (int)cson_string_length_bytes(cson_value_get_string(g.json.reqPayload.v)) );
297 wiki_convert( &contentOrig, &contentHtml, 0 );
298 blob_reset( &contentOrig );
299 pay = cson_value_new_string( blob_str(&contentHtml), (unsigned int)blob_size(&contentHtml));
300 blob_reset( &contentHtml );
301 return pay;
302 }
303
304
305 /*
306 ** Internal impl of /wiki/save and /wiki/create. If createMode is 0
307 ** and the page already exists then a
308 ** FSL_JSON_E_RESOURCE_ALREADY_EXISTS error is triggered. If
309 ** createMode is false then the FSL_JSON_E_RESOURCE_NOT_FOUND is
310 ** triggered if the page does not already exists.
311 **
312 ** Note that the error triggered when createMode==0 and no such page
313 ** exists is rather arbitrary - we could just as well create the entry
314 ** here if it doesn't already exist. With that, save/create would
315 ** become one operation. That said, i expect there are people who
316 ** would categorize such behaviour as "being too clever" or "doing too
317 ** much automatically" (and i would likely agree with them).
318 **
319 ** If allowCreateIfNotExists is true then this function will allow a new
320 ** page to be created even if createMode is false.
321 */
json_wiki_create_or_save(char createMode,char allowCreateIfNotExists)322 static cson_value * json_wiki_create_or_save(char createMode,
323 char allowCreateIfNotExists){
324 Blob content = empty_blob; /* wiki page content */
325 cson_value * nameV; /* wiki page name */
326 char const * zPageName; /* cstr form of page name */
327 cson_value * contentV; /* passed-in content */
328 cson_value * emptyContent = NULL; /* placeholder for empty content. */
329 cson_value * payV = NULL; /* payload/return value */
330 cson_string const * jstr = NULL; /* temp for cson_value-to-cson_string conversions. */
331 char const * zMimeType = 0;
332 unsigned int contentLen = 0;
333 int rid;
334 if( (createMode && !g.perm.NewWiki)
335 || (!createMode && !g.perm.WrWiki)){
336 json_set_err(FSL_JSON_E_DENIED,
337 "Requires '%c' permissions.",
338 (createMode ? 'f' : 'k'));
339 return NULL;
340 }
341 nameV = json_req_payload_get("name");
342 if(!nameV){
343 json_set_err( FSL_JSON_E_MISSING_ARGS,
344 "'name' parameter is missing.");
345 return NULL;
346 }
347 zPageName = cson_string_cstr(cson_value_get_string(nameV));
348 if(!zPageName || !*zPageName){
349 json_set_err(FSL_JSON_E_INVALID_ARGS,
350 "'name' parameter must be a non-empty string.");
351 return NULL;
352 }
353 rid = db_int(0,
354 "SELECT x.rid FROM tag t, tagxref x"
355 " WHERE x.tagid=t.tagid AND t.tagname='wiki-%q'"
356 " ORDER BY x.mtime DESC LIMIT 1",
357 zPageName
358 );
359
360 if(rid){
361 if(createMode){
362 json_set_err(FSL_JSON_E_RESOURCE_ALREADY_EXISTS,
363 "Wiki page '%s' already exists.",
364 zPageName);
365 goto error;
366 }
367 }else if(!createMode && !allowCreateIfNotExists){
368 json_set_err(FSL_JSON_E_RESOURCE_NOT_FOUND,
369 "Wiki page '%s' not found.",
370 zPageName);
371 goto error;
372 }
373
374 contentV = json_req_payload_get("content");
375 if( !contentV ){
376 if( createMode || (!rid && allowCreateIfNotExists) ){
377 contentV = emptyContent = cson_value_new_string("",0);
378 }else{
379 json_set_err(FSL_JSON_E_MISSING_ARGS,
380 "'content' parameter is missing.");
381 goto error;
382 }
383 }
384 if( !cson_value_is_string(nameV)
385 || !cson_value_is_string(contentV)){
386 json_set_err(FSL_JSON_E_INVALID_ARGS,
387 "'content' parameter must be a string.");
388 goto error;
389 }
390 jstr = cson_value_get_string(contentV);
391 contentLen = (int)cson_string_length_bytes(jstr);
392 if(contentLen){
393 blob_append(&content, cson_string_cstr(jstr),contentLen);
394 }
395
396 zMimeType = json_find_option_cstr("mimetype","mimetype","M");
397 zMimeType = wiki_filter_mimetypes(zMimeType);
398
399 wiki_cmd_commit(zPageName, rid, &content, zMimeType, 0);
400 blob_reset(&content);
401 /*
402 Our return value here has a race condition: if this operation
403 is called concurrently for the same wiki page via two requests,
404 payV could reflect the results of the other save operation.
405 */
406 payV = json_get_wiki_page_by_name(
407 cson_string_cstr(
408 cson_value_get_string(nameV)),
409 0);
410 goto ok;
411 error:
412 assert( 0 != g.json.resultCode );
413 assert( NULL == payV );
414 ok:
415 if( emptyContent ){
416 /* We have some potentially tricky memory ownership
417 here, which is why we handle emptyContent separately.
418
419 This is, in fact, overkill because cson_value_new_string("",0)
420 actually returns a shared singleton instance (i.e. doesn't
421 allocate), but that is a cson implementation detail which i
422 don't want leaking into this code...
423 */
424 cson_value_free(emptyContent);
425 }
426 return payV;
427
428 }
429
430 /*
431 ** Implementation of /json/wiki/create.
432 */
json_wiki_create()433 static cson_value * json_wiki_create(){
434 return json_wiki_create_or_save(1,0);
435 }
436
437 /*
438 ** Implementation of /json/wiki/save.
439 */
json_wiki_save()440 static cson_value * json_wiki_save(){
441 char const createIfNotExists = json_getenv_bool("createIfNotExists",0);
442 return json_wiki_create_or_save(0,createIfNotExists);
443 }
444
445 /*
446 ** Implementation of /json/wiki/list.
447 */
json_wiki_list()448 static cson_value * json_wiki_list(){
449 cson_value * listV = NULL;
450 cson_array * list = NULL;
451 char const * zGlob = NULL;
452 Stmt q = empty_Stmt;
453 Blob sql = empty_blob;
454 char const verbose = json_find_option_bool("verbose",NULL,"v",0);
455 char fInvert = json_find_option_bool("invert",NULL,"i",0);;
456
457 if( !g.perm.RdWiki && !g.perm.Read ){
458 json_set_err(FSL_JSON_E_DENIED,
459 "Requires 'j' or 'o' permissions.");
460 return NULL;
461 }
462 blob_append(&sql,"SELECT"
463 " DISTINCT substr(tagname,6) as name"
464 " FROM tag JOIN tagxref USING('tagid')"
465 " WHERE tagname GLOB 'wiki-*'"
466 " AND TYPEOF(tagxref.value+0)='integer'",
467 /* ^^^ elide wiki- tags which are not wiki pages */
468 -1);
469 zGlob = json_find_option_cstr("glob",NULL,"g");
470 if(zGlob && *zGlob){
471 blob_append_sql(&sql," AND name %s GLOB %Q",
472 fInvert ? "NOT" : "", zGlob);
473 }else{
474 zGlob = json_find_option_cstr("like",NULL,"l");
475 if(zGlob && *zGlob){
476 blob_append_sql(&sql," AND name %s LIKE %Q",
477 fInvert ? "NOT" : "", zGlob);
478 }
479 }
480 blob_append(&sql," ORDER BY lower(name)", -1);
481 db_prepare(&q,"%s", blob_sql_text(&sql));
482 blob_reset(&sql);
483 listV = cson_value_new_array();
484 list = cson_value_get_array(listV);
485 while( SQLITE_ROW == db_step(&q) ){
486 cson_value * v;
487 if( verbose ){
488 char const * name = db_column_text(&q,0);
489 v = json_get_wiki_page_by_name(name,0);
490 }else{
491 v = cson_sqlite3_column_to_value(q.pStmt,0);
492 }
493 if(!v){
494 json_set_err(FSL_JSON_E_UNKNOWN,
495 "Could not convert wiki name column to JSON.");
496 goto error;
497 }else if( 0 != cson_array_append( list, v ) ){
498 cson_value_free(v);
499 json_set_err(FSL_JSON_E_ALLOC,"Could not append wiki page name to array.")
500 /* OOM (or maybe numeric overflow) are the only realistic
501 error codes for that particular failure.*/;
502 goto error;
503 }
504 }
505 goto end;
506 error:
507 assert(0 != g.json.resultCode);
508 cson_value_free(listV);
509 listV = NULL;
510 end:
511 db_finalize(&q);
512 return listV;
513 }
514
json_wiki_diff()515 static cson_value * json_wiki_diff(){
516 char const * zV1 = NULL;
517 char const * zV2 = NULL;
518 cson_object * pay = NULL;
519 int argPos = g.json.dispatchDepth;
520 int r1 = 0, r2 = 0;
521 Manifest * pW1 = NULL, *pW2 = NULL;
522 Blob w1 = empty_blob, w2 = empty_blob, d = empty_blob;
523 char const * zErrTag = NULL;
524 DiffConfig DCfg;
525 char * zUuid = NULL;
526 if( !g.perm.Hyperlink ){
527 json_set_err(FSL_JSON_E_DENIED,
528 "Requires 'h' permissions.");
529 return NULL;
530 }
531
532
533 zV1 = json_find_option_cstr2( "v1",NULL, NULL, ++argPos );
534 zV2 = json_find_option_cstr2( "v2",NULL, NULL, ++argPos );
535 if(!zV1 || !*zV1 || !zV2 || !*zV2) {
536 json_set_err(FSL_JSON_E_INVALID_ARGS,
537 "Requires both 'v1' and 'v2' arguments.");
538 return NULL;
539 }
540
541 r1 = symbolic_name_to_rid( zV1, "w" );
542 zErrTag = zV1;
543 if(r1<0){
544 goto ambiguous;
545 }else if(0==r1){
546 goto invalid;
547 }
548
549 r2 = symbolic_name_to_rid( zV2, "w" );
550 zErrTag = zV2;
551 if(r2<0){
552 goto ambiguous;
553 }else if(0==r2){
554 goto invalid;
555 }
556
557 zErrTag = zV1;
558 pW1 = manifest_get(r1, CFTYPE_WIKI, 0);
559 if( pW1==0 ) {
560 goto manifest;
561 }
562 zErrTag = zV2;
563 pW2 = manifest_get(r2, CFTYPE_WIKI, 0);
564 if( pW2==0 ) {
565 goto manifest;
566 }
567
568 blob_init(&w1, pW1->zWiki, -1);
569 blob_zero(&w2);
570 blob_init(&w2, pW2->zWiki, -1);
571 blob_zero(&d);
572 diff_config_init(&DCfg, DIFF_IGNORE_EOLWS | DIFF_STRIP_EOLCR);
573 text_diff(&w1, &w2, &d, &DCfg);
574 blob_reset(&w1);
575 blob_reset(&w2);
576
577 pay = cson_new_object();
578
579 zUuid = json_wiki_get_uuid_for_rid( pW1->rid );
580 cson_object_set(pay, "v1", json_new_string(zUuid) );
581 free(zUuid);
582 zUuid = json_wiki_get_uuid_for_rid( pW2->rid );
583 cson_object_set(pay, "v2", json_new_string(zUuid) );
584 free(zUuid);
585 zUuid = NULL;
586
587 manifest_destroy(pW1);
588 manifest_destroy(pW2);
589
590 cson_object_set(pay, "diff",
591 cson_value_new_string( blob_str(&d),
592 (unsigned int)blob_size(&d)));
593
594 return cson_object_value(pay);
595
596 manifest:
597 json_set_err(FSL_JSON_E_UNKNOWN,
598 "Could not load wiki manifest for UUID [%s].",
599 zErrTag);
600 goto end;
601
602 ambiguous:
603 json_set_err(FSL_JSON_E_AMBIGUOUS_UUID,
604 "UUID [%s] is ambiguous.", zErrTag);
605 goto end;
606
607 invalid:
608 json_set_err(FSL_JSON_E_RESOURCE_NOT_FOUND,
609 "UUID [%s] not found.", zErrTag);
610 goto end;
611
612 end:
613 cson_free_object(pay);
614 return NULL;
615 }
616
617 #endif /* FOSSIL_ENABLE_JSON */
618