Fossil SCM

fossil-scm / src / json_artifact.c
Blame History Raw 515 lines
1
#ifdef FOSSIL_ENABLE_JSON
2
/*
3
** Copyright (c) 2011 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_artifact.h"
21
22
#if INTERFACE
23
#include "json_detail.h"
24
#endif
25
26
/*
27
** Internal callback for /json/artifact handlers. rid refers to
28
** the rid of a given type of artifact, and each callback is
29
** specialized to return a JSON form of one type of artifact.
30
**
31
** Implementations may assert() that rid refers to requested artifact
32
** type, since mismatches in the artifact types come from
33
** json_page_artifact() as opposed to client data.
34
**
35
** The pParent parameter points to the response payload object. It
36
** _may_ be used to populate "top-level" information in the response
37
** payload, but normally this is neither necessary nor desired.
38
*/
39
typedef cson_value * (*artifact_f)( cson_object * pParent, int rid );
40
41
/*
42
** Internal per-artifact-type dispatching helper.
43
*/
44
typedef struct ArtifactDispatchEntry {
45
/**
46
Artifact type name, e.g. "checkin", "ticket", "wiki".
47
*/
48
char const * name;
49
50
/**
51
JSON construction callback. Creates the contents for the
52
payload.artifact property of /json/artifact responses.
53
*/
54
artifact_f func;
55
} ArtifactDispatchEntry;
56
57
58
/*
59
** Generates a JSON Array reference holding the parent UUIDs (as strings).
60
** If it finds no matches then it returns NULL (OOM is a fatal error).
61
**
62
** Returned value is NULL or an Array owned by the caller.
63
*/
64
cson_value * json_parent_uuids_for_ci( int rid ){
65
Stmt q = empty_Stmt;
66
cson_array * pParents = NULL;
67
db_prepare( &q,
68
"SELECT uuid FROM plink, blob"
69
" WHERE plink.cid=%d AND blob.rid=plink.pid"
70
" ORDER BY plink.isprim DESC",
71
rid );
72
while( SQLITE_ROW==db_step(&q) ){
73
if(!pParents) {
74
pParents = cson_new_array();
75
}
76
cson_array_append( pParents, cson_sqlite3_column_to_value( q.pStmt, 0 ) );
77
}
78
db_finalize(&q);
79
return cson_array_value(pParents);
80
}
81
82
/*
83
** Generates an artifact Object for the given rid,
84
** which must refer to a Check-in.
85
**
86
** Returned value is NULL or an Object owned by the caller.
87
*/
88
cson_value * json_artifact_for_ci( int rid, char showFiles ){
89
cson_value * v = NULL;
90
Stmt q = empty_Stmt;
91
static cson_value * eventTypeLabel = NULL;
92
if(!eventTypeLabel){
93
eventTypeLabel = json_new_string("checkin");
94
json_gc_add("$EVENT_TYPE_LABEL(commit)", eventTypeLabel);
95
}
96
97
db_prepare(&q,
98
"SELECT b.uuid, "
99
" cast(strftime('%%s',e.mtime) as int), "
100
" strftime('%%s',e.omtime),"
101
" e.user, "
102
" e.comment"
103
" FROM blob b, event e"
104
" WHERE b.rid=%d"
105
" AND e.objid=%d",
106
rid, rid
107
);
108
if( db_step(&q)==SQLITE_ROW ){
109
cson_object * o;
110
cson_value * tmpV = NULL;
111
const char *zUuid = db_column_text(&q, 0);
112
const char *zUser;
113
const char *zComment;
114
char * zEUser, * zEComment;
115
i64 mtime, omtime;
116
v = cson_value_new_object();
117
o = cson_value_get_object(v);
118
#define SET(K,V) cson_object_set(o,(K), (V))
119
SET("type", eventTypeLabel );
120
SET("uuid",json_new_string(zUuid));
121
SET("isLeaf", cson_value_new_bool(is_a_leaf(rid)));
122
123
mtime = db_column_int64(&q,1);
124
SET("timestamp",json_new_int(mtime));
125
omtime = db_column_int64(&q,2);
126
if(omtime && (omtime!=mtime)){
127
SET("originTime",json_new_int(omtime));
128
}
129
130
zUser = db_column_text(&q,3);
131
zEUser = db_text(0,
132
"SELECT value FROM tagxref WHERE tagid=%d AND rid=%d",
133
TAG_USER, rid);
134
if(zEUser){
135
SET("user", json_new_string(zEUser));
136
if(0!=fossil_strcmp(zEUser,zUser)){
137
SET("originUser",json_new_string(zUser));
138
}
139
free(zEUser);
140
}else{
141
SET("user",json_new_string(zUser));
142
}
143
144
zComment = db_column_text(&q,4);
145
zEComment = db_text(0,
146
"SELECT value FROM tagxref WHERE tagid=%d AND rid=%d",
147
TAG_COMMENT, rid);
148
if(zEComment){
149
SET("comment",json_new_string(zEComment));
150
if(0 != fossil_strcmp(zEComment,zComment)){
151
SET("originComment", json_new_string(zComment));
152
}
153
free(zEComment);
154
}else{
155
SET("comment",json_new_string(zComment));
156
}
157
158
tmpV = json_parent_uuids_for_ci(rid);
159
if(tmpV){
160
SET("parents", tmpV);
161
}
162
163
tmpV = json_tags_for_checkin_rid(rid,0);
164
if(tmpV){
165
SET("tags",tmpV);
166
}
167
168
if( showFiles ){
169
tmpV = json_get_changed_files(rid, 1);
170
if(tmpV){
171
SET("files",tmpV);
172
}
173
}
174
175
#undef SET
176
}
177
db_finalize(&q);
178
return v;
179
}
180
181
/*
182
** Very incomplete/incorrect impl of /json/artifact/TICKET_ID.
183
*/
184
cson_value * json_artifact_ticket( cson_object * zParent, int rid ){
185
cson_object * pay = NULL;
186
Manifest *pTktChng = NULL;
187
static cson_value * eventTypeLabel = NULL;
188
if(! g.perm.RdTkt ){
189
g.json.resultCode = FSL_JSON_E_DENIED;
190
return NULL;
191
}
192
if(!eventTypeLabel){
193
eventTypeLabel = json_new_string("ticket");
194
json_gc_add("$EVENT_TYPE_LABEL(ticket)", eventTypeLabel);
195
}
196
197
pTktChng = manifest_get(rid, CFTYPE_TICKET, 0);
198
if( pTktChng==0 ){
199
g.json.resultCode = FSL_JSON_E_MANIFEST_READ_FAILED;
200
return NULL;
201
}
202
pay = cson_new_object();
203
cson_object_set(pay, "eventType", eventTypeLabel );
204
cson_object_set(pay, "uuid", json_new_string(pTktChng->zTicketUuid));
205
cson_object_set(pay, "user", json_new_string(pTktChng->zUser));
206
cson_object_set(pay, "timestamp", json_julian_to_timestamp(pTktChng->rDate));
207
manifest_destroy(pTktChng);
208
return cson_object_value(pay);
209
}
210
211
/*
212
** Sub-impl of /json/artifact for check-ins.
213
*/
214
static cson_value * json_artifact_ci( cson_object * zParent, int rid ){
215
if(!g.perm.Read){
216
json_set_err( FSL_JSON_E_DENIED,
217
"Viewing check-ins requires 'o' privileges." );
218
return NULL;
219
}else{
220
cson_value * artV = json_artifact_for_ci(rid, 1);
221
cson_object * art = cson_value_get_object(artV);
222
if(art){
223
cson_object_merge( zParent, art, CSON_MERGE_REPLACE );
224
cson_free_object(art);
225
}
226
return cson_object_value(zParent);
227
}
228
}
229
230
/*
231
** Internal mapping of /json/artifact/FOO commands/callbacks.
232
*/
233
static ArtifactDispatchEntry ArtifactDispatchList[] = {
234
{"checkin", json_artifact_ci},
235
{"file", json_artifact_file},
236
/*{"tag", NULL}, //impl missing */
237
/*{"technote", NULL}, //impl missing */
238
{"ticket", json_artifact_ticket},
239
{"wiki", json_artifact_wiki},
240
/* Final entry MUST have a NULL name. */
241
{NULL,NULL}
242
};
243
244
/*
245
** Internal helper which returns:
246
**
247
** If the "format" (CLI: -f) flag is set function returns the same as
248
** json_wiki_get_content_format_flag(), else it returns true (non-0)
249
** if either the includeContent (HTTP) or -content|-c boolean flags
250
** (CLI) are set.
251
*/
252
static int json_artifact_get_content_format_flag(void){
253
enum { MagicValue = -9 };
254
int contentFormat = json_wiki_get_content_format_flag(MagicValue);
255
if(MagicValue == contentFormat){
256
contentFormat = json_find_option_bool("includeContent",
257
"content","c",0) /* deprecated */ ? -1 : 0;
258
}
259
return contentFormat;
260
}
261
262
extern int json_wiki_get_content_format_flag(int defaultValue) /* json_wiki.c*/;
263
264
cson_value * json_artifact_wiki(cson_object * zParent, int rid){
265
if( ! g.perm.RdWiki ){
266
json_set_err(FSL_JSON_E_DENIED,
267
"Requires 'j' privileges.");
268
return NULL;
269
}else{
270
enum { MagicValue = -9 };
271
int const contentFormat = json_artifact_get_content_format_flag();
272
return json_get_wiki_page_by_rid(rid, contentFormat);
273
}
274
}
275
276
/*
277
** Internal helper for routines which add a "status" flag to file
278
** artifact data. isNew and isDel should be the "is this object new?"
279
** and "is this object removed?" flags of the underlying query. This
280
** function returns a static string from the set (added, removed,
281
** modified), depending on the combination of the two args.
282
**
283
** Reminder to self: (mlink.pid==0) AS isNew, (mlink.fid==0) AS isDel
284
*/
285
char const * json_artifact_status_to_string( char isNew, char isDel ){
286
return isNew
287
? "added"
288
: (isDel
289
? "removed"
290
: "modified");
291
}
292
293
cson_value * json_artifact_file(cson_object * zParent, int rid){
294
cson_object * pay = NULL;
295
Stmt q = empty_Stmt;
296
cson_array * checkin_arr = NULL;
297
int contentFormat;
298
i64 contentSize = -1;
299
char * parentUuid;
300
if( ! g.perm.Read ){
301
json_set_err(FSL_JSON_E_DENIED,
302
"Requires 'o' privileges.");
303
return NULL;
304
}
305
306
pay = zParent;
307
308
contentFormat = json_artifact_get_content_format_flag();
309
if( 0 != contentFormat ){
310
Blob content = empty_blob;
311
const char *zMime;
312
char const * zFormat = (contentFormat<1) ? "raw" : "html";
313
content_get(rid, &content);
314
zMime = mimetype_from_content(&content);
315
cson_object_set(zParent, "contentType",
316
json_new_string(zMime ? zMime : "text/plain"));
317
if(!zMime){/* text/plain */
318
if(0 < blob_size(&content)){
319
if( 0 < contentFormat ){/*HTML-size it*/
320
Blob html = empty_blob;
321
wiki_convert(&content, &html, 0);
322
assert( blob_size(&content) < blob_size(&html) );
323
blob_swap( &html, &content );
324
assert( blob_size(&content) > blob_size(&html) );
325
blob_reset( &html );
326
}/*else as-is*/
327
}
328
cson_object_set(zParent, "content",
329
cson_value_new_string(blob_str(&content),
330
(unsigned int)blob_size(&content)));
331
}/*else binary: ignore*/
332
contentSize = blob_size(&content);
333
cson_object_set(zParent, "contentSize", json_new_int(contentSize) );
334
cson_object_set(zParent, "contentFormat", json_new_string(zFormat) );
335
blob_reset(&content);
336
}
337
contentSize = db_int64(-1, "SELECT size FROM blob WHERE rid=%d", rid);
338
assert( -1 < contentSize );
339
cson_object_set(zParent, "size", json_new_int(contentSize) );
340
341
parentUuid = db_text(NULL,
342
"SELECT DISTINCT p.uuid "
343
"FROM blob p, blob f, mlink m "
344
"WHERE m.pid=p.rid "
345
"AND m.fid=f.rid "
346
"AND f.rid=%d",
347
rid
348
);
349
if(parentUuid){
350
cson_object_set( zParent, "parent", json_new_string(parentUuid) );
351
fossil_free(parentUuid);
352
}
353
354
/* Find check-ins associated with this file... */
355
db_prepare(&q,
356
"SELECT filename.name AS name, "
357
" (mlink.pid==0) AS isNew,"
358
" (mlink.fid==0) AS isDel,"
359
" cast(strftime('%%s',event.mtime) as int) AS timestamp,"
360
" coalesce(event.ecomment,event.comment) as comment,"
361
" coalesce(event.euser,event.user) as user,"
362
#if 0
363
" a.size AS size," /* same for all check-ins. */
364
#endif
365
" b.uuid as checkin, "
366
#if 0
367
" mlink.mperm as mperm,"
368
#endif
369
" coalesce((SELECT value FROM tagxref"
370
" WHERE tagid=%d AND tagtype>0 AND "
371
" rid=mlink.mid),'trunk') as branch"
372
" FROM mlink, filename, event, blob a, blob b"
373
" WHERE filename.fnid=mlink.fnid"
374
" AND event.objid=mlink.mid"
375
" AND a.rid=mlink.fid"
376
" AND b.rid=mlink.mid"
377
" AND mlink.fid=%d"
378
" ORDER BY filename.name, event.mtime",
379
TAG_BRANCH, rid
380
);
381
/* TODO: add a "state" flag for the file in each check-in,
382
e.g. "modified", "new", "deleted".
383
*/
384
checkin_arr = cson_new_array();
385
cson_object_set(pay, "checkins", cson_array_value(checkin_arr));
386
while( (SQLITE_ROW==db_step(&q) ) ){
387
cson_object * row = cson_value_get_object(
388
cson_sqlite3_row_to_object(q.pStmt));
389
/* FIXME: move this isNew/isDel stuff into an SQL CASE statement. */
390
char const isNew = cson_value_get_bool(cson_object_get(row,"isNew"));
391
char const isDel = cson_value_get_bool(cson_object_get(row,"isDel"));
392
cson_object_set(row, "isNew", NULL);
393
cson_object_set(row, "isDel", NULL);
394
cson_object_set(row, "state", json_new_string(
395
json_artifact_status_to_string(isNew, isDel)));
396
cson_array_append( checkin_arr, cson_object_value(row) );
397
}
398
db_finalize(&q);
399
return cson_object_value(pay);
400
}
401
402
/*
403
** Impl of /json/artifact. This basically just determines the type of
404
** an artifact and forwards the real work to another function.
405
*/
406
cson_value * json_page_artifact(void){
407
cson_object * pay = NULL;
408
char const * zName = NULL;
409
char const * zType = NULL;
410
char const * zUuid = NULL;
411
cson_value * entry = NULL;
412
Blob uuid = empty_blob;
413
int rc;
414
int rid = 0;
415
ArtifactDispatchEntry const * dispatcher = &ArtifactDispatchList[0];
416
zName = json_find_option_cstr2("name", NULL, NULL, g.json.dispatchDepth+1);
417
if(!zName || !*zName) {
418
json_set_err(FSL_JSON_E_MISSING_ARGS,
419
"Missing 'name' argument.");
420
return NULL;
421
}
422
423
if( validate16(zName, strlen(zName)) ){
424
if( db_exists("SELECT 1 FROM ticket WHERE tkt_uuid GLOB '%q*'", zName) ){
425
zType = "ticket";
426
goto handle_entry;
427
}
428
if( db_exists("SELECT 1 FROM tag WHERE tagname GLOB 'event-%q*'", zName) ){
429
zType = "tag";
430
goto handle_entry;
431
}
432
}
433
blob_set(&uuid,zName);
434
rc = name_to_uuid(&uuid,-1,"*");
435
/* FIXME: check for a filename if all else fails. */
436
if(1==rc){
437
g.json.resultCode = FSL_JSON_E_RESOURCE_NOT_FOUND;
438
goto error;
439
}else if(2==rc){
440
g.json.resultCode = FSL_JSON_E_AMBIGUOUS_UUID;
441
goto error;
442
}
443
zUuid = blob_str(&uuid);
444
rid = db_int(0, "SELECT rid FROM blob WHERE uuid=%Q", zUuid);
445
if(0==rid){
446
g.json.resultCode = FSL_JSON_E_RESOURCE_NOT_FOUND;
447
goto error;
448
}
449
450
if( db_exists("SELECT 1 FROM mlink WHERE mid=%d", rid)
451
|| db_exists("SELECT 1 FROM plink WHERE cid=%d", rid)
452
|| db_exists("SELECT 1 FROM plink WHERE pid=%d", rid)){
453
zType = "checkin";
454
goto handle_entry;
455
}else if( db_exists("SELECT 1 FROM tagxref JOIN tag USING(tagid)"
456
" WHERE rid=%d AND tagname LIKE 'wiki-%%'", rid) ){
457
zType = "wiki";
458
goto handle_entry;
459
}else if( db_exists("SELECT 1 FROM tagxref JOIN tag USING(tagid)"
460
" WHERE rid=%d AND tagname LIKE 'tkt-%%'", rid) ){
461
zType = "ticket";
462
goto handle_entry;
463
}else if ( db_exists("SELECT 1 FROM mlink WHERE fid = %d", rid) ){
464
zType = "file";
465
goto handle_entry;
466
}else{
467
g.json.resultCode = FSL_JSON_E_RESOURCE_NOT_FOUND;
468
goto error;
469
}
470
471
error:
472
assert( 0 != g.json.resultCode );
473
goto veryend;
474
475
handle_entry:
476
pay = cson_new_object();
477
assert( (NULL != zType) && "Internal dispatching error." );
478
for( ; dispatcher->name; ++dispatcher ){
479
if(0!=fossil_strcmp(dispatcher->name, zType)){
480
continue;
481
}else{
482
entry = (*dispatcher->func)(pay, rid);
483
break;
484
}
485
}
486
if(entry==0){
487
g.json.resultCode = FSL_JSON_E_RESOURCE_NOT_FOUND
488
/* This is not quite right. We need a new result code
489
for this case. */;
490
g.zErrMsg = mprintf("Missing implementation for "
491
"artifacts of this type.");
492
goto error;
493
}
494
if(!g.json.resultCode){
495
assert( NULL != entry );
496
assert( NULL != zType );
497
cson_object_set( pay, "type", json_new_string(zType) );
498
cson_object_set( pay, "uuid", json_new_string(zUuid) );
499
/*cson_object_set( pay, "name", json_new_string(zName ? zName : zUuid) );*/
500
/*cson_object_set( pay, "rid", cson_value_new_integer(rid) );*/
501
if(cson_value_is_object(entry) && (cson_value_get_object(entry) != pay)){
502
cson_object_set(pay, "artifact", entry);
503
}
504
}
505
veryend:
506
blob_reset(&uuid);
507
if(g.json.resultCode && pay){
508
cson_free_object(pay);
509
pay = NULL;
510
}
511
return cson_object_value(pay);
512
}
513
514
#endif /* FOSSIL_ENABLE_JSON */
515

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button