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