|
1
|
/* |
|
2
|
** Copyright (c) 2020 D. Richard Hipp |
|
3
|
** |
|
4
|
** This program is free software; you can redistribute it and/or |
|
5
|
** modify it under the terms of the Simplified BSD License (also |
|
6
|
** known as the "2-Clause License" or "FreeBSD License".) |
|
7
|
** |
|
8
|
** This program is distributed in the hope that it will be useful, |
|
9
|
** but without any warranty; without even the implied warranty of |
|
10
|
** merchantability or fitness for a particular purpose. |
|
11
|
** |
|
12
|
** Author contact information: |
|
13
|
** [email protected] |
|
14
|
** http://www.hwaci.com/drh/ |
|
15
|
** |
|
16
|
******************************************************************************* |
|
17
|
** |
|
18
|
** This file contains shared Ajax-related code for /fileedit, the wiki/forum |
|
19
|
** editors, and friends. |
|
20
|
*/ |
|
21
|
#include "config.h" |
|
22
|
#include "ajax.h" |
|
23
|
#include <assert.h> |
|
24
|
#include <stdarg.h> |
|
25
|
|
|
26
|
#if INTERFACE |
|
27
|
/* enum ajax_render_preview_flags: */ |
|
28
|
#define AJAX_PREVIEW_LINE_NUMBERS 1 |
|
29
|
/* enum ajax_render_modes: */ |
|
30
|
#define AJAX_RENDER_GUESS 0 /* Guess rendering mode based on mimetype. */ |
|
31
|
/* GUESS must be 0. All others have unspecified values. */ |
|
32
|
#define AJAX_RENDER_PLAIN_TEXT 1 /* Render as plain text. */ |
|
33
|
#define AJAX_RENDER_HTML_IFRAME 2 /* Render as HTML inside an IFRAME. */ |
|
34
|
#define AJAX_RENDER_HTML_INLINE 3 /* Render as HTML without an IFRAME. */ |
|
35
|
#define AJAX_RENDER_WIKI 4 /* Render as wiki/markdown. */ |
|
36
|
#endif |
|
37
|
|
|
38
|
/* |
|
39
|
** Emits JS code which initializes the |
|
40
|
** fossil.page.previewModes object to a map of AJAX_RENDER_xxx values |
|
41
|
** and symbolic names for use by client-side scripts. |
|
42
|
** |
|
43
|
** If addScriptTag is true then the output is wrapped in a SCRIPT tag |
|
44
|
** with the current nonce, else no SCRIPT tag is emitted. |
|
45
|
** |
|
46
|
** Requires that builtin_emit_script_fossil_bootstrap() has already been |
|
47
|
** called in order to initialize the window.fossil.page object. |
|
48
|
*/ |
|
49
|
void ajax_emit_js_preview_modes(int addScriptTag){ |
|
50
|
if(addScriptTag){ |
|
51
|
style_script_begin(__FILE__,__LINE__); |
|
52
|
} |
|
53
|
CX("fossil.page.previewModes={" |
|
54
|
"guess: %d, %d: 'guess', wiki: %d, %d: 'wiki'," |
|
55
|
"htmlIframe: %d, %d: 'htmlIframe', " |
|
56
|
"htmlInline: %d, %d: 'htmlInline', " |
|
57
|
"text: %d, %d: 'text'" |
|
58
|
"};\n", |
|
59
|
AJAX_RENDER_GUESS, AJAX_RENDER_GUESS, |
|
60
|
AJAX_RENDER_WIKI, AJAX_RENDER_WIKI, |
|
61
|
AJAX_RENDER_HTML_IFRAME, AJAX_RENDER_HTML_IFRAME, |
|
62
|
AJAX_RENDER_HTML_INLINE, AJAX_RENDER_HTML_INLINE, |
|
63
|
AJAX_RENDER_PLAIN_TEXT, AJAX_RENDER_PLAIN_TEXT); |
|
64
|
if(addScriptTag){ |
|
65
|
style_script_end(); |
|
66
|
} |
|
67
|
} |
|
68
|
|
|
69
|
/* |
|
70
|
** Returns a value from the ajax_render_modes enum, based on the |
|
71
|
** given mimetype string (which may be NULL), defaulting to |
|
72
|
** AJAX_RENDER_PLAIN_TEXT. |
|
73
|
*/ |
|
74
|
int ajax_render_mode_for_mimetype(const char * zMimetype){ |
|
75
|
int rc = AJAX_RENDER_PLAIN_TEXT; |
|
76
|
if( zMimetype ){ |
|
77
|
if( fossil_strcmp(zMimetype, "text/html")==0 ){ |
|
78
|
rc = AJAX_RENDER_HTML_IFRAME; |
|
79
|
}else if( fossil_strcmp(zMimetype, "text/x-fossil-wiki")==0 |
|
80
|
|| fossil_strcmp(zMimetype, "text/x-markdown")==0 ){ |
|
81
|
rc = AJAX_RENDER_WIKI; |
|
82
|
} |
|
83
|
} |
|
84
|
return rc; |
|
85
|
} |
|
86
|
|
|
87
|
/* |
|
88
|
** Renders text/wiki content preview for various /ajax routes. |
|
89
|
** |
|
90
|
** pContent is text/wiki content to preview. zName is the name of the |
|
91
|
** content, for purposes of determining the mimetype based on the |
|
92
|
** extension (if NULL, mimetype text/plain is assumed). flags may be a |
|
93
|
** bitmask of values from the ajax_render_preview_flags |
|
94
|
** enum. *renderMode must specify the render mode to use. If |
|
95
|
** *renderMode==AJAX_RENDER_GUESS then *renderMode gets set to the |
|
96
|
** mode which is guessed at for the rendering (based on the mimetype). |
|
97
|
** |
|
98
|
** nIframeHeightEm is only used for the AJAX_RENDER_HTML_IFRAME |
|
99
|
** renderMode, and specifies the height, in EM's, of the resulting |
|
100
|
** iframe. If passed 0, it defaults to "some sane value." |
|
101
|
*/ |
|
102
|
void ajax_render_preview(Blob * pContent, const char *zName, |
|
103
|
int flags, int * renderMode, |
|
104
|
int nIframeHeightEm){ |
|
105
|
const char * zMime; |
|
106
|
|
|
107
|
zMime = zName ? mimetype_from_name(zName) : "text/plain"; |
|
108
|
if(AJAX_RENDER_GUESS==*renderMode){ |
|
109
|
*renderMode = ajax_render_mode_for_mimetype(zMime); |
|
110
|
} |
|
111
|
switch(*renderMode){ |
|
112
|
case AJAX_RENDER_HTML_IFRAME:{ |
|
113
|
char * z64 = encode64(blob_str(pContent), blob_size(pContent)); |
|
114
|
CX("<iframe width='100%%' frameborder='0' " |
|
115
|
"marginwidth='0' style='height:%dem' " |
|
116
|
"marginheight='0' sandbox='allow-same-origin' " |
|
117
|
"src='data:text/html;base64,%z'" |
|
118
|
"></iframe>", |
|
119
|
nIframeHeightEm ? nIframeHeightEm : 40, |
|
120
|
z64); |
|
121
|
break; |
|
122
|
} |
|
123
|
case AJAX_RENDER_HTML_INLINE:{ |
|
124
|
CX("%b",pContent); |
|
125
|
break; |
|
126
|
} |
|
127
|
case AJAX_RENDER_WIKI: |
|
128
|
safe_html_context(DOCSRC_FILE); |
|
129
|
wiki_render_by_mimetype(pContent, zMime); |
|
130
|
break; |
|
131
|
default:{ |
|
132
|
const char *zContent = blob_str(pContent); |
|
133
|
if(AJAX_PREVIEW_LINE_NUMBERS & flags){ |
|
134
|
output_text_with_line_numbers(zContent, blob_size(pContent), |
|
135
|
zName, "on", 0); |
|
136
|
}else{ |
|
137
|
const char *zExt = strrchr(zName,'.'); |
|
138
|
if(zExt && zExt[1]){ |
|
139
|
CX("<pre><code class='language-%s'>%h</code></pre>", |
|
140
|
zExt+1, zContent); |
|
141
|
}else{ |
|
142
|
CX("<pre>%h</pre>", zContent); |
|
143
|
} |
|
144
|
} |
|
145
|
break; |
|
146
|
} |
|
147
|
} |
|
148
|
} |
|
149
|
|
|
150
|
/* |
|
151
|
** Renders diffs for ajax routes. pOrig is the "original" (v1) content |
|
152
|
** and pContent is the locally-edited (v2) content. diffFlags is any |
|
153
|
** set of flags suitable for passing to text_diff(). |
|
154
|
** |
|
155
|
** zOrigHash, if not NULL, must be the SCM-side hash of pOrig's |
|
156
|
** contents. If set, additional information may be built into |
|
157
|
** the diff output to enable dynamic loading of additional |
|
158
|
** diff context. |
|
159
|
*/ |
|
160
|
void ajax_render_diff(Blob * pOrig, const char * zOrigHash, |
|
161
|
Blob *pContent, u64 diffFlags){ |
|
162
|
Blob out = empty_blob; |
|
163
|
DiffConfig DCfg; |
|
164
|
|
|
165
|
diff_config_init(&DCfg, diffFlags); |
|
166
|
DCfg.zLeftHash = zOrigHash; |
|
167
|
text_diff(pOrig, pContent, &out, &DCfg); |
|
168
|
if(blob_size(&out)==0){ |
|
169
|
/* nothing to do */ |
|
170
|
}else{ |
|
171
|
CX("%b",&out); |
|
172
|
} |
|
173
|
blob_reset(&out); |
|
174
|
} |
|
175
|
|
|
176
|
/* |
|
177
|
** Uses P(zKey) to fetch a CGI environment variable. If that var is |
|
178
|
** NULL or starts with '0' or 'f' then this function returns false, |
|
179
|
** else it returns true. |
|
180
|
*/ |
|
181
|
int ajax_p_bool(char const *zKey){ |
|
182
|
const char * zVal = P(zKey); |
|
183
|
return (!zVal || '0'==*zVal || 'f'==*zVal) ? 0 : 1; |
|
184
|
} |
|
185
|
|
|
186
|
/* |
|
187
|
** Helper for /ajax routes. Clears the CGI content buffer, sets an |
|
188
|
** HTTP error status code, and queues up a JSON response in the form |
|
189
|
** of an object: |
|
190
|
** |
|
191
|
** {error: formatted message} |
|
192
|
** |
|
193
|
** If httpCode<=0 then it defaults to 500. |
|
194
|
** |
|
195
|
** After calling this, the caller should immediately return. |
|
196
|
*/ |
|
197
|
void ajax_route_error(int httpCode, const char * zFmt, ...){ |
|
198
|
Blob msg = empty_blob; |
|
199
|
Blob content = empty_blob; |
|
200
|
va_list vargs; |
|
201
|
va_start(vargs,zFmt); |
|
202
|
blob_vappendf(&msg, zFmt, vargs); |
|
203
|
va_end(vargs); |
|
204
|
blob_appendf(&content,"{\"error\":%!j}", blob_str(&msg)); |
|
205
|
blob_reset(&msg); |
|
206
|
cgi_set_content(&content); |
|
207
|
cgi_set_status(httpCode>0 ? httpCode : 500, "Error"); |
|
208
|
cgi_set_content_type("application/json"); |
|
209
|
} |
|
210
|
|
|
211
|
/* |
|
212
|
** Performs bootstrapping common to the /ajax/xyz AJAX routes, such as |
|
213
|
** logging in the user. |
|
214
|
** |
|
215
|
** Returns false (0) if bootstrapping fails, in which case it has |
|
216
|
** reported the error and the route should immediately return. Returns |
|
217
|
** true on success. |
|
218
|
** |
|
219
|
** If requireWrite is true then write permissions are required. |
|
220
|
** If requirePost is true then the request is assumed to be using |
|
221
|
** POST'ed data and CSRF validation is performed. |
|
222
|
** |
|
223
|
*/ |
|
224
|
int ajax_route_bootstrap(int requireWrite, int requirePost){ |
|
225
|
login_check_credentials(); |
|
226
|
if( requireWrite!=0 && g.perm.Write==0 ){ |
|
227
|
ajax_route_error(403,"Write permissions required."); |
|
228
|
return 0; |
|
229
|
}else if(0==cgi_csrf_safe(requirePost)){ |
|
230
|
ajax_route_error(403, |
|
231
|
"CSRF violation (make sure sending of HTTP " |
|
232
|
"Referer headers is enabled for XHR " |
|
233
|
"connections)."); |
|
234
|
return 0; |
|
235
|
} |
|
236
|
return 1; |
|
237
|
} |
|
238
|
|
|
239
|
/* |
|
240
|
** Helper for collecting filename/check-in request parameters. |
|
241
|
** |
|
242
|
** If zFn is not NULL, it is assigned the value of the first one of |
|
243
|
** the "filename" or "fn" CGI parameters which is set. |
|
244
|
** |
|
245
|
** If zCi is not NULL, it is assigned the value of the first one of |
|
246
|
** the "checkin" or "ci" CGI parameters which is set. |
|
247
|
** |
|
248
|
** If a parameter is not NULL, it will be assigned NULL if the |
|
249
|
** corresponding parameter is not set. |
|
250
|
** |
|
251
|
** Returns the number of non-NULL values it assigns to arguments. Thus |
|
252
|
** if passed (&x, NULL), it returns 1 if it assigns non-NULL to *x and |
|
253
|
** 0 if it assigns NULL to *x. |
|
254
|
*/ |
|
255
|
int ajax_get_fnci_args( const char **zFn, const char **zCi ){ |
|
256
|
int rc = 0; |
|
257
|
if(zCi!=0){ |
|
258
|
*zCi = PD("checkin",P("ci")); |
|
259
|
if( *zCi ) ++rc; |
|
260
|
} |
|
261
|
if(zFn!=0){ |
|
262
|
*zFn = PD("filename",P("fn")); |
|
263
|
if (*zFn) ++rc; |
|
264
|
} |
|
265
|
return rc; |
|
266
|
} |
|
267
|
|
|
268
|
/* |
|
269
|
** AJAX route /ajax/preview-text |
|
270
|
** |
|
271
|
** Required query parameters: |
|
272
|
** |
|
273
|
** filename=name of content, for use in determining the |
|
274
|
** mimetype/render mode. |
|
275
|
** |
|
276
|
** content=text |
|
277
|
** |
|
278
|
** Optional query parameters: |
|
279
|
** |
|
280
|
** render_mode=integer (AJAX_RENDER_xxx) (default=AJAX_RENDER_GUESS) |
|
281
|
** |
|
282
|
** ln=0 or 1 to disable/enable line number mode in |
|
283
|
** AJAX_RENDER_PLAIN_TEXT mode. |
|
284
|
** |
|
285
|
** iframe_height=integer (default=40) Height, in EMs of HTML preview |
|
286
|
** iframe. |
|
287
|
** |
|
288
|
** User must have Write access to use this page. |
|
289
|
** |
|
290
|
** Responds with the HTML content of the preview. On error it produces |
|
291
|
** a JSON response as documented for ajax_route_error(). |
|
292
|
** |
|
293
|
** Extra response headers: |
|
294
|
** |
|
295
|
** x-ajax-render-mode: string representing the rendering mode |
|
296
|
** which was really used (which will differ from the requested mode |
|
297
|
** only if mode 0 (guess) was requested). The names are documented |
|
298
|
** below in code and match those in the emitted JS object |
|
299
|
** fossil.page.previewModes. |
|
300
|
*/ |
|
301
|
void ajax_route_preview_text(void){ |
|
302
|
const char * zFilename = 0; |
|
303
|
const char * zContent = P("content"); |
|
304
|
int renderMode = atoi(PD("render_mode","0")); |
|
305
|
int ln = atoi(PD("ln","0")); |
|
306
|
int iframeHeight = atoi(PD("iframe_height","40")); |
|
307
|
Blob content = empty_blob; |
|
308
|
const char * zRenderMode = 0; |
|
309
|
|
|
310
|
ajax_get_fnci_args( &zFilename, 0 ); |
|
311
|
|
|
312
|
if(!ajax_route_bootstrap(0,1)){ |
|
313
|
return; |
|
314
|
} |
|
315
|
if(zFilename==0){ |
|
316
|
/* The filename is only used for mimetype determination, |
|
317
|
** so we can default it... */ |
|
318
|
zFilename = "foo.txt"; |
|
319
|
} |
|
320
|
cgi_set_content_type("text/html"); |
|
321
|
blob_init(&content, zContent, -1); |
|
322
|
ajax_render_preview(&content, zFilename, |
|
323
|
ln ? AJAX_PREVIEW_LINE_NUMBERS : 0, |
|
324
|
&renderMode, iframeHeight); |
|
325
|
/* |
|
326
|
** Now tell the caller if we did indeed use AJAX_RENDER_WIKI, so that |
|
327
|
** they can re-set the <base href> to an appropriate value (which |
|
328
|
** requires knowing the content's current check-in version, which we |
|
329
|
** don't have here). |
|
330
|
*/ |
|
331
|
switch(renderMode){ |
|
332
|
/* The strings used here MUST correspond to those used in the JS-side |
|
333
|
** fossil.page.previewModes map. |
|
334
|
*/ |
|
335
|
case AJAX_RENDER_WIKI: zRenderMode = "wiki"; break; |
|
336
|
case AJAX_RENDER_HTML_INLINE: zRenderMode = "htmlInline"; break; |
|
337
|
case AJAX_RENDER_HTML_IFRAME: zRenderMode = "htmlIframe"; break; |
|
338
|
case AJAX_RENDER_PLAIN_TEXT: zRenderMode = "text"; break; |
|
339
|
case AJAX_RENDER_GUESS: |
|
340
|
assert(!"cannot happen"); |
|
341
|
} |
|
342
|
if(zRenderMode!=0){ |
|
343
|
cgi_printf_header("x-ajax-render-mode: %s\r\n", zRenderMode); |
|
344
|
} |
|
345
|
} |
|
346
|
|
|
347
|
#if INTERFACE |
|
348
|
/* |
|
349
|
** Internal mapping of ajax sub-route names to various metadata. |
|
350
|
*/ |
|
351
|
struct AjaxRoute { |
|
352
|
const char *zName; /* Name part of the route after "ajax/" */ |
|
353
|
void (*xCallback)(); /* Impl function for the route. */ |
|
354
|
int bWriteMode; /* True if requires write mode */ |
|
355
|
int bPost; /* True if requires POST (i.e. CSRF |
|
356
|
** verification) */ |
|
357
|
}; |
|
358
|
typedef struct AjaxRoute AjaxRoute; |
|
359
|
#endif /*INTERFACE*/ |
|
360
|
|
|
361
|
/* |
|
362
|
** Comparison function for bsearch() for searching an AjaxRoute |
|
363
|
** list for a matching name. |
|
364
|
*/ |
|
365
|
int cmp_ajax_route_name(const void *a, const void *b){ |
|
366
|
const AjaxRoute * rA = (const AjaxRoute*)a; |
|
367
|
const AjaxRoute * rB = (const AjaxRoute*)b; |
|
368
|
return fossil_strcmp(rA->zName, rB->zName); |
|
369
|
} |
|
370
|
|
|
371
|
/* |
|
372
|
** WEBPAGE: ajax hidden |
|
373
|
** |
|
374
|
** The main dispatcher for shared ajax-served routes. Requires the |
|
375
|
** 'name' parameter be the main route's name (as defined in a list in |
|
376
|
** this function), noting that fossil automatically assigns all path |
|
377
|
** parts after "ajax" to "name", e.g. /ajax/foo/bar assigns |
|
378
|
** name=foo/bar. |
|
379
|
** |
|
380
|
** This "page" is only intended to be used by higher-level pages which |
|
381
|
** have certain Ajax-driven features in common. It is not intended to |
|
382
|
** be used by clients and NONE of its HTTP interfaces are considered |
|
383
|
** documented/stable/supported - they may change on any given build of |
|
384
|
** fossil. |
|
385
|
** |
|
386
|
** The exact response type depends on the route which gets called. In |
|
387
|
** the case of an initialization error it emits a JSON-format response |
|
388
|
** as documented for ajax_route_error(). Individual routes may emit |
|
389
|
** errors in different formats, e.g. HTML. |
|
390
|
*/ |
|
391
|
void ajax_route_dispatcher(void){ |
|
392
|
const char * zName = P("name"); |
|
393
|
AjaxRoute routeName = {0,0,0,0}; |
|
394
|
const AjaxRoute * pRoute = 0; |
|
395
|
const AjaxRoute routes[] = { |
|
396
|
/* Keep these sorted by zName (for bsearch()) */ |
|
397
|
{"preview-text", ajax_route_preview_text, 0, 1 |
|
398
|
/* Note that this does not require write permissions in the repo. |
|
399
|
** It should arguably require write permissions but doing means |
|
400
|
** that /chat does not work without check-in permissions: |
|
401
|
** |
|
402
|
** https://fossil-scm.org/forum/forumpost/ed4a762b3a557898 |
|
403
|
** |
|
404
|
** This particular route is used by /fileedit and /chat, whereas |
|
405
|
** /wikiedit uses a simpler wiki-specific route. |
|
406
|
*/ } |
|
407
|
}; |
|
408
|
|
|
409
|
if(zName==0 || zName[0]==0){ |
|
410
|
ajax_route_error(400,"Missing required [route] 'name' parameter."); |
|
411
|
return; |
|
412
|
} |
|
413
|
routeName.zName = zName; |
|
414
|
pRoute = (const AjaxRoute *)bsearch(&routeName, routes, |
|
415
|
count(routes), sizeof routes[0], |
|
416
|
cmp_ajax_route_name); |
|
417
|
if(pRoute==0){ |
|
418
|
ajax_route_error(404,"Ajax route not found."); |
|
419
|
return; |
|
420
|
}else if(0==ajax_route_bootstrap(pRoute->bWriteMode, pRoute->bPost)){ |
|
421
|
return; |
|
422
|
} |
|
423
|
pRoute->xCallback(); |
|
424
|
} |
|
425
|
|