|
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
|
** [email protected] |
|
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(void); |
|
27
|
static cson_value * json_wiki_get(void); |
|
28
|
static cson_value * json_wiki_list(void); |
|
29
|
static cson_value * json_wiki_preview(void); |
|
30
|
static cson_value * json_wiki_save(void); |
|
31
|
static cson_value * json_wiki_diff(void); |
|
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
|
*/ |
|
52
|
cson_value * json_page_wiki(void){ |
|
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
|
*/ |
|
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
|
*/ |
|
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
|
*/ |
|
168
|
cson_value * json_get_wiki_page_by_name(char const * zPageName, |
|
169
|
int contentFormat){ |
|
170
|
int rid; |
|
171
|
rid = db_int(0, |
|
172
|
"SELECT x.rid FROM tag t, tagxref x, blob b" |
|
173
|
" WHERE x.tagid=t.tagid AND t.tagname='wiki-%q' " |
|
174
|
" AND b.rid=x.rid" |
|
175
|
" ORDER BY x.mtime DESC LIMIT 1", |
|
176
|
zPageName |
|
177
|
); |
|
178
|
if( 0==rid ){ |
|
179
|
json_set_err( FSL_JSON_E_RESOURCE_NOT_FOUND, "Wiki page not found: %s", |
|
180
|
zPageName ); |
|
181
|
return NULL; |
|
182
|
} |
|
183
|
return json_get_wiki_page_by_rid(rid, contentFormat); |
|
184
|
} |
|
185
|
|
|
186
|
|
|
187
|
/* |
|
188
|
** Searches json_find_option_ctr("format",NULL,"f") for a flag. |
|
189
|
** If not found it returns defaultValue else it returns a value |
|
190
|
** depending on the first character of the format option: |
|
191
|
** |
|
192
|
** [h]tml = 1 |
|
193
|
** [n]one = 0 |
|
194
|
** [r]aw = -1 |
|
195
|
** |
|
196
|
** The return value is intended for use with |
|
197
|
** json_get_wiki_page_by_rid() and friends. |
|
198
|
*/ |
|
199
|
int json_wiki_get_content_format_flag( int defaultValue ){ |
|
200
|
int contentFormat = defaultValue; |
|
201
|
char const * zFormat = json_find_option_cstr("format",NULL,"f"); |
|
202
|
if( !zFormat || !*zFormat ){ |
|
203
|
return contentFormat; |
|
204
|
} |
|
205
|
else if('r'==*zFormat){ |
|
206
|
contentFormat = -1; |
|
207
|
} |
|
208
|
else if('h'==*zFormat){ |
|
209
|
contentFormat = 1; |
|
210
|
} |
|
211
|
else if('n'==*zFormat){ |
|
212
|
contentFormat = 0; |
|
213
|
} |
|
214
|
return contentFormat; |
|
215
|
} |
|
216
|
|
|
217
|
/* |
|
218
|
** Helper for /json/wiki/get and /json/wiki/preview. At least one of |
|
219
|
** zPageName (wiki page name) or zSymname must be set to a |
|
220
|
** non-empty/non-NULL value. zSymname takes precedence. On success |
|
221
|
** the result of one of json_get_wiki_page_by_rid() or |
|
222
|
** json_get_wiki_page_by_name() will be returned (owned by the |
|
223
|
** caller). On error g.json's error state is set and NULL is returned. |
|
224
|
*/ |
|
225
|
static cson_value * json_wiki_get_by_name_or_symname(char const * zPageName, |
|
226
|
char const * zSymname, |
|
227
|
int contentFormat ){ |
|
228
|
if(!zSymname || !*zSymname){ |
|
229
|
return json_get_wiki_page_by_name(zPageName, contentFormat); |
|
230
|
}else{ |
|
231
|
int rid = symbolic_name_to_rid( zSymname ? zSymname : zPageName, "w" ); |
|
232
|
if(rid<0){ |
|
233
|
json_set_err(FSL_JSON_E_AMBIGUOUS_UUID, |
|
234
|
"UUID [%s] is ambiguous.", zSymname); |
|
235
|
return NULL; |
|
236
|
}else if(rid==0){ |
|
237
|
json_set_err(FSL_JSON_E_RESOURCE_NOT_FOUND, |
|
238
|
"UUID [%s] does not resolve to a wiki page.", zSymname); |
|
239
|
return NULL; |
|
240
|
}else{ |
|
241
|
return json_get_wiki_page_by_rid(rid, contentFormat); |
|
242
|
} |
|
243
|
} |
|
244
|
} |
|
245
|
|
|
246
|
/* |
|
247
|
** Implementation of /json/wiki/get. |
|
248
|
** |
|
249
|
*/ |
|
250
|
static cson_value * json_wiki_get(void){ |
|
251
|
char const * zPageName; |
|
252
|
char const * zSymName = NULL; |
|
253
|
int contentFormat = -1; |
|
254
|
if( !g.perm.RdWiki && !g.perm.Read ){ |
|
255
|
json_set_err(FSL_JSON_E_DENIED, |
|
256
|
"Requires 'o' or 'j' access."); |
|
257
|
return NULL; |
|
258
|
} |
|
259
|
zPageName = json_find_option_cstr2("name",NULL,"n",g.json.dispatchDepth+1); |
|
260
|
|
|
261
|
zSymName = json_find_option_cstr("uuid",NULL,"u"); |
|
262
|
|
|
263
|
if((!zPageName||!*zPageName) && (!zSymName || !*zSymName)){ |
|
264
|
json_set_err(FSL_JSON_E_MISSING_ARGS, |
|
265
|
"At least one of the 'name' or 'uuid' arguments must be provided."); |
|
266
|
return NULL; |
|
267
|
} |
|
268
|
|
|
269
|
/* TODO: see if we have a page named zPageName. If not, try to resolve |
|
270
|
zPageName as a UUID. |
|
271
|
*/ |
|
272
|
|
|
273
|
contentFormat = json_wiki_get_content_format_flag(contentFormat); |
|
274
|
return json_wiki_get_by_name_or_symname( zPageName, zSymName, contentFormat ); |
|
275
|
} |
|
276
|
|
|
277
|
/* |
|
278
|
** Implementation of /json/wiki/preview. |
|
279
|
*/ |
|
280
|
static cson_value * json_wiki_preview(void){ |
|
281
|
char const * zContent = NULL; |
|
282
|
char const * zMime = NULL; |
|
283
|
cson_string * sContent = NULL; |
|
284
|
cson_value * pay = NULL; |
|
285
|
Blob contentOrig = empty_blob; |
|
286
|
Blob contentHtml = empty_blob; |
|
287
|
if( !g.perm.WrWiki ){ |
|
288
|
json_set_err(FSL_JSON_E_DENIED, |
|
289
|
"Requires 'k' access."); |
|
290
|
return NULL; |
|
291
|
} |
|
292
|
|
|
293
|
if(g.json.reqPayload.o){ |
|
294
|
sContent = cson_value_get_string( |
|
295
|
cson_object_get(g.json.reqPayload.o, "body")); |
|
296
|
zMime = cson_value_get_cstr(cson_object_get(g.json.reqPayload.o, |
|
297
|
"mimetype")); |
|
298
|
}else{ |
|
299
|
sContent = cson_value_get_string(g.json.reqPayload.v); |
|
300
|
} |
|
301
|
if(!sContent) { |
|
302
|
json_set_err(FSL_JSON_E_MISSING_ARGS, |
|
303
|
"The 'payload' property must be either a string containing the " |
|
304
|
"Fossil wiki code to preview or an object with body + mimetype " |
|
305
|
"properties."); |
|
306
|
return NULL; |
|
307
|
} |
|
308
|
zContent = cson_string_cstr(sContent); |
|
309
|
blob_append( &contentOrig, zContent, (int)cson_string_length_bytes(sContent)); |
|
310
|
zMime = wiki_filter_mimetypes(zMime); |
|
311
|
if( 0==fossil_strcmp(zMime, "text/x-markdown") ){ |
|
312
|
markdown_to_html(&contentOrig, 0, &contentHtml); |
|
313
|
}else if( 0==fossil_strcmp(zMime, "text/plain") ){ |
|
314
|
blob_append(&contentHtml, "<pre class='textPlain'>", -1); |
|
315
|
blob_append(&contentHtml, blob_str(&contentOrig), blob_size(&contentOrig)); |
|
316
|
blob_append(&contentHtml, "</pre>", -1); |
|
317
|
}else{ |
|
318
|
wiki_convert( &contentOrig, &contentHtml, 0 ); |
|
319
|
} |
|
320
|
blob_reset( &contentOrig ); |
|
321
|
pay = cson_value_new_string( blob_str(&contentHtml), |
|
322
|
(unsigned int)blob_size(&contentHtml)); |
|
323
|
blob_reset( &contentHtml ); |
|
324
|
return pay; |
|
325
|
} |
|
326
|
|
|
327
|
|
|
328
|
/* |
|
329
|
** Internal impl of /wiki/save and /wiki/create. If createMode is 0 |
|
330
|
** and the page already exists then a |
|
331
|
** FSL_JSON_E_RESOURCE_ALREADY_EXISTS error is triggered. If |
|
332
|
** createMode is false then the FSL_JSON_E_RESOURCE_NOT_FOUND is |
|
333
|
** triggered if the page does not already exists. |
|
334
|
** |
|
335
|
** Note that the error triggered when createMode==0 and no such page |
|
336
|
** exists is rather arbitrary - we could just as well create the entry |
|
337
|
** here if it doesn't already exist. With that, save/create would |
|
338
|
** become one operation. That said, i expect there are people who |
|
339
|
** would categorize such behaviour as "being too clever" or "doing too |
|
340
|
** much automatically" (and i would likely agree with them). |
|
341
|
** |
|
342
|
** If allowCreateIfNotExists is true then this function will allow a new |
|
343
|
** page to be created even if createMode is false. |
|
344
|
*/ |
|
345
|
static cson_value * json_wiki_create_or_save(char createMode, |
|
346
|
char allowCreateIfNotExists){ |
|
347
|
Blob content = empty_blob; /* wiki page content */ |
|
348
|
cson_value * nameV; /* wiki page name */ |
|
349
|
char const * zPageName; /* cstr form of page name */ |
|
350
|
cson_value * contentV; /* passed-in content */ |
|
351
|
cson_value * emptyContent = NULL; /* placeholder for empty content. */ |
|
352
|
cson_value * payV = NULL; /* payload/return value */ |
|
353
|
cson_string const * jstr = NULL; /* temp for cson_value-to-cson_string |
|
354
|
conversions. */ |
|
355
|
char const * zMimeType = 0; |
|
356
|
unsigned int contentLen = 0; |
|
357
|
int rid; |
|
358
|
if( (createMode && !g.perm.NewWiki) |
|
359
|
|| (!createMode && !g.perm.WrWiki)){ |
|
360
|
json_set_err(FSL_JSON_E_DENIED, |
|
361
|
"Requires '%c' permissions.", |
|
362
|
(createMode ? 'f' : 'k')); |
|
363
|
return NULL; |
|
364
|
} |
|
365
|
nameV = json_req_payload_get("name"); |
|
366
|
if(!nameV){ |
|
367
|
json_set_err( FSL_JSON_E_MISSING_ARGS, |
|
368
|
"'name' parameter is missing."); |
|
369
|
return NULL; |
|
370
|
} |
|
371
|
zPageName = cson_string_cstr(cson_value_get_string(nameV)); |
|
372
|
if(!zPageName || !*zPageName){ |
|
373
|
json_set_err(FSL_JSON_E_INVALID_ARGS, |
|
374
|
"'name' parameter must be a non-empty string."); |
|
375
|
return NULL; |
|
376
|
} |
|
377
|
rid = db_int(0, |
|
378
|
"SELECT x.rid FROM tag t, tagxref x" |
|
379
|
" WHERE x.tagid=t.tagid AND t.tagname='wiki-%q'" |
|
380
|
" ORDER BY x.mtime DESC LIMIT 1", |
|
381
|
zPageName |
|
382
|
); |
|
383
|
|
|
384
|
if(rid){ |
|
385
|
if(createMode){ |
|
386
|
json_set_err(FSL_JSON_E_RESOURCE_ALREADY_EXISTS, |
|
387
|
"Wiki page '%s' already exists.", |
|
388
|
zPageName); |
|
389
|
goto error; |
|
390
|
} |
|
391
|
}else if(!createMode && !allowCreateIfNotExists){ |
|
392
|
json_set_err(FSL_JSON_E_RESOURCE_NOT_FOUND, |
|
393
|
"Wiki page '%s' not found.", |
|
394
|
zPageName); |
|
395
|
goto error; |
|
396
|
} |
|
397
|
|
|
398
|
contentV = json_req_payload_get("content"); |
|
399
|
if( !contentV ){ |
|
400
|
if( createMode || (!rid && allowCreateIfNotExists) ){ |
|
401
|
contentV = emptyContent = cson_value_new_string("",0); |
|
402
|
}else{ |
|
403
|
json_set_err(FSL_JSON_E_MISSING_ARGS, |
|
404
|
"'content' parameter is missing."); |
|
405
|
goto error; |
|
406
|
} |
|
407
|
} |
|
408
|
if( !cson_value_is_string(nameV) |
|
409
|
|| !cson_value_is_string(contentV)){ |
|
410
|
json_set_err(FSL_JSON_E_INVALID_ARGS, |
|
411
|
"'content' parameter must be a string."); |
|
412
|
goto error; |
|
413
|
} |
|
414
|
jstr = cson_value_get_string(contentV); |
|
415
|
contentLen = (int)cson_string_length_bytes(jstr); |
|
416
|
if(contentLen){ |
|
417
|
blob_append(&content, cson_string_cstr(jstr),contentLen); |
|
418
|
} |
|
419
|
|
|
420
|
zMimeType = json_find_option_cstr("mimetype","mimetype","M"); |
|
421
|
zMimeType = wiki_filter_mimetypes(zMimeType); |
|
422
|
|
|
423
|
wiki_cmd_commit(zPageName, rid, &content, zMimeType, 0); |
|
424
|
blob_reset(&content); |
|
425
|
/* |
|
426
|
Our return value here has a race condition: if this operation |
|
427
|
is called concurrently for the same wiki page via two requests, |
|
428
|
payV could reflect the results of the other save operation. |
|
429
|
*/ |
|
430
|
payV = json_get_wiki_page_by_name( |
|
431
|
cson_string_cstr( |
|
432
|
cson_value_get_string(nameV)), |
|
433
|
0); |
|
434
|
goto ok; |
|
435
|
error: |
|
436
|
assert( 0 != g.json.resultCode ); |
|
437
|
assert( NULL == payV ); |
|
438
|
ok: |
|
439
|
if( emptyContent ){ |
|
440
|
/* We have some potentially tricky memory ownership |
|
441
|
here, which is why we handle emptyContent separately. |
|
442
|
|
|
443
|
This is, in fact, overkill because cson_value_new_string("",0) |
|
444
|
actually returns a shared singleton instance (i.e. doesn't |
|
445
|
allocate), but that is a cson implementation detail which i |
|
446
|
don't want leaking into this code... |
|
447
|
*/ |
|
448
|
cson_value_free(emptyContent); |
|
449
|
} |
|
450
|
return payV; |
|
451
|
|
|
452
|
} |
|
453
|
|
|
454
|
/* |
|
455
|
** Implementation of /json/wiki/create. |
|
456
|
*/ |
|
457
|
static cson_value * json_wiki_create(void){ |
|
458
|
return json_wiki_create_or_save(1,0); |
|
459
|
} |
|
460
|
|
|
461
|
/* |
|
462
|
** Implementation of /json/wiki/save. |
|
463
|
*/ |
|
464
|
static cson_value * json_wiki_save(void){ |
|
465
|
char const createIfNotExists = json_getenv_bool("createIfNotExists",0); |
|
466
|
return json_wiki_create_or_save(0,createIfNotExists); |
|
467
|
} |
|
468
|
|
|
469
|
/* |
|
470
|
** Implementation of /json/wiki/list. |
|
471
|
*/ |
|
472
|
static cson_value * json_wiki_list(void){ |
|
473
|
cson_value * listV = NULL; |
|
474
|
cson_array * list = NULL; |
|
475
|
char const * zGlob = NULL; |
|
476
|
Stmt q = empty_Stmt; |
|
477
|
Blob sql = empty_blob; |
|
478
|
char const verbose = json_find_option_bool("verbose",NULL,"v",0); |
|
479
|
char fInvert = json_find_option_bool("invert",NULL,"i",0);; |
|
480
|
|
|
481
|
if( !g.perm.RdWiki && !g.perm.Read ){ |
|
482
|
json_set_err(FSL_JSON_E_DENIED, |
|
483
|
"Requires 'j' or 'o' permissions."); |
|
484
|
return NULL; |
|
485
|
} |
|
486
|
blob_append(&sql,"SELECT" |
|
487
|
" DISTINCT substr(tagname,6) as name" |
|
488
|
" FROM tag JOIN tagxref USING('tagid')" |
|
489
|
" WHERE tagname GLOB 'wiki-*'" |
|
490
|
" AND TYPEOF(tagxref.value+0)='integer'", |
|
491
|
/* ^^^ elide wiki- tags which are not wiki pages */ |
|
492
|
-1); |
|
493
|
zGlob = json_find_option_cstr("glob",NULL,"g"); |
|
494
|
if(zGlob && *zGlob){ |
|
495
|
blob_append_sql(&sql," AND name %s GLOB %Q", |
|
496
|
fInvert ? "NOT" : "", zGlob); |
|
497
|
}else{ |
|
498
|
zGlob = json_find_option_cstr("like",NULL,"l"); |
|
499
|
if(zGlob && *zGlob){ |
|
500
|
blob_append_sql(&sql," AND name %s LIKE %Q", |
|
501
|
fInvert ? "NOT" : "", zGlob); |
|
502
|
} |
|
503
|
} |
|
504
|
blob_append(&sql," ORDER BY lower(name)", -1); |
|
505
|
db_prepare(&q,"%s", blob_sql_text(&sql)); |
|
506
|
blob_reset(&sql); |
|
507
|
listV = cson_value_new_array(); |
|
508
|
list = cson_value_get_array(listV); |
|
509
|
while( SQLITE_ROW == db_step(&q) ){ |
|
510
|
cson_value * v; |
|
511
|
if( verbose ){ |
|
512
|
char const * name = db_column_text(&q,0); |
|
513
|
v = json_get_wiki_page_by_name(name,0); |
|
514
|
}else{ |
|
515
|
v = cson_sqlite3_column_to_value(q.pStmt,0); |
|
516
|
} |
|
517
|
if(!v){ |
|
518
|
json_set_err(FSL_JSON_E_UNKNOWN, |
|
519
|
"Could not convert wiki name column to JSON."); |
|
520
|
goto error; |
|
521
|
}else if( 0 != cson_array_append( list, v ) ){ |
|
522
|
cson_value_free(v); |
|
523
|
json_set_err(FSL_JSON_E_ALLOC,"Could not append wiki page name to array.") |
|
524
|
/* OOM (or maybe numeric overflow) are the only realistic |
|
525
|
error codes for that particular failure.*/; |
|
526
|
goto error; |
|
527
|
} |
|
528
|
} |
|
529
|
goto end; |
|
530
|
error: |
|
531
|
assert(0 != g.json.resultCode); |
|
532
|
cson_value_free(listV); |
|
533
|
listV = NULL; |
|
534
|
end: |
|
535
|
db_finalize(&q); |
|
536
|
return listV; |
|
537
|
} |
|
538
|
|
|
539
|
static cson_value * json_wiki_diff(void){ |
|
540
|
char const * zV1 = NULL; |
|
541
|
char const * zV2 = NULL; |
|
542
|
cson_object * pay = NULL; |
|
543
|
int argPos = g.json.dispatchDepth; |
|
544
|
int r1 = 0, r2 = 0; |
|
545
|
Manifest * pW1 = NULL, *pW2 = NULL; |
|
546
|
Blob w1 = empty_blob, w2 = empty_blob, d = empty_blob; |
|
547
|
char const * zErrTag = NULL; |
|
548
|
DiffConfig DCfg; |
|
549
|
char * zUuid = NULL; |
|
550
|
if( !g.perm.Hyperlink ){ |
|
551
|
json_set_err(FSL_JSON_E_DENIED, |
|
552
|
"Requires 'h' permissions."); |
|
553
|
return NULL; |
|
554
|
} |
|
555
|
|
|
556
|
|
|
557
|
zV1 = json_find_option_cstr2( "v1",NULL, NULL, ++argPos ); |
|
558
|
zV2 = json_find_option_cstr2( "v2",NULL, NULL, ++argPos ); |
|
559
|
if(!zV1 || !*zV1 || !zV2 || !*zV2) { |
|
560
|
json_set_err(FSL_JSON_E_INVALID_ARGS, |
|
561
|
"Requires both 'v1' and 'v2' arguments."); |
|
562
|
return NULL; |
|
563
|
} |
|
564
|
|
|
565
|
r1 = symbolic_name_to_rid( zV1, "w" ); |
|
566
|
zErrTag = zV1; |
|
567
|
if(r1<0){ |
|
568
|
goto ambiguous; |
|
569
|
}else if(0==r1){ |
|
570
|
goto invalid; |
|
571
|
} |
|
572
|
|
|
573
|
r2 = symbolic_name_to_rid( zV2, "w" ); |
|
574
|
zErrTag = zV2; |
|
575
|
if(r2<0){ |
|
576
|
goto ambiguous; |
|
577
|
}else if(0==r2){ |
|
578
|
goto invalid; |
|
579
|
} |
|
580
|
|
|
581
|
zErrTag = zV1; |
|
582
|
pW1 = manifest_get(r1, CFTYPE_WIKI, 0); |
|
583
|
if( pW1==0 ) { |
|
584
|
goto manifest; |
|
585
|
} |
|
586
|
zErrTag = zV2; |
|
587
|
pW2 = manifest_get(r2, CFTYPE_WIKI, 0); |
|
588
|
if( pW2==0 ) { |
|
589
|
goto manifest; |
|
590
|
} |
|
591
|
|
|
592
|
blob_init(&w1, pW1->zWiki, -1); |
|
593
|
blob_zero(&w2); |
|
594
|
blob_init(&w2, pW2->zWiki, -1); |
|
595
|
blob_zero(&d); |
|
596
|
diff_config_init(&DCfg, DIFF_IGNORE_EOLWS | DIFF_STRIP_EOLCR); |
|
597
|
text_diff(&w1, &w2, &d, &DCfg); |
|
598
|
blob_reset(&w1); |
|
599
|
blob_reset(&w2); |
|
600
|
|
|
601
|
pay = cson_new_object(); |
|
602
|
|
|
603
|
zUuid = json_wiki_get_uuid_for_rid( pW1->rid ); |
|
604
|
cson_object_set(pay, "v1", json_new_string(zUuid) ); |
|
605
|
free(zUuid); |
|
606
|
zUuid = json_wiki_get_uuid_for_rid( pW2->rid ); |
|
607
|
cson_object_set(pay, "v2", json_new_string(zUuid) ); |
|
608
|
free(zUuid); |
|
609
|
zUuid = NULL; |
|
610
|
|
|
611
|
manifest_destroy(pW1); |
|
612
|
manifest_destroy(pW2); |
|
613
|
|
|
614
|
cson_object_set(pay, "diff", |
|
615
|
cson_value_new_string( blob_str(&d), |
|
616
|
(unsigned int)blob_size(&d))); |
|
617
|
|
|
618
|
return cson_object_value(pay); |
|
619
|
|
|
620
|
manifest: |
|
621
|
json_set_err(FSL_JSON_E_UNKNOWN, |
|
622
|
"Could not load wiki manifest for UUID [%s].", |
|
623
|
zErrTag); |
|
624
|
goto end; |
|
625
|
|
|
626
|
ambiguous: |
|
627
|
json_set_err(FSL_JSON_E_AMBIGUOUS_UUID, |
|
628
|
"UUID [%s] is ambiguous.", zErrTag); |
|
629
|
goto end; |
|
630
|
|
|
631
|
invalid: |
|
632
|
json_set_err(FSL_JSON_E_RESOURCE_NOT_FOUND, |
|
633
|
"UUID [%s] not found.", zErrTag); |
|
634
|
goto end; |
|
635
|
|
|
636
|
end: |
|
637
|
cson_free_object(pay); |
|
638
|
return NULL; |
|
639
|
} |
|
640
|
|
|
641
|
#endif /* FOSSIL_ENABLE_JSON */ |
|
642
|
|