Fossil SCM

fossil-scm / src / ajax.c
Blame History Raw 487 lines
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
** Returns the resulting http code.
198
*/
199
int ajax_route_error(int httpCode, const char * zFmt, ...){
200
Blob msg = empty_blob;
201
Blob content = empty_blob;
202
va_list vargs;
203
204
if( httpCode<=0 ) httpCode=500;
205
va_start(vargs,zFmt);
206
blob_vappendf(&msg, zFmt, vargs);
207
va_end(vargs);
208
blob_appendf(&content,"{\"error\":%!j}", blob_str(&msg));
209
blob_reset(&msg);
210
cgi_set_content(&content);
211
cgi_set_status(httpCode, "Error");
212
cgi_set_content_type("application/json");
213
return httpCode;
214
}
215
216
void ajax_route_error_forbidden(){
217
ajax_route_error(403, "Permission denied.");
218
}
219
220
void ajax_route_error_captcha(){
221
ajax_route_error(400, "Invalid captcha response.");
222
}
223
224
void ajax_route_error_csrf(){
225
ajax_route_error(403, "Invalid CSRF signature.");
226
}
227
228
void ajax_route_error_404(const char *zMsg){
229
ajax_route_error(404, "%s", zMsg ? zMsg : "Resource not found.");
230
}
231
232
int ajax_check_csrf(int level){
233
if( 0==cgi_csrf_safe(level) ){
234
ajax_route_error_csrf();
235
return 0;
236
}
237
return 1;
238
}
239
240
/*
241
** Performs bootstrapping common to the /ajax/xyz AJAX routes, such as
242
** logging in the user.
243
**
244
** Returns false (0) if bootstrapping fails, in which case it has
245
** reported the error and the route should immediately return. Returns
246
** true on success.
247
**
248
** If requireWrite is true then write permissions are required.
249
** If requirePost is true then the request is assumed to be using
250
** POST'ed data and CSRF validation is performed.
251
**
252
*/
253
int ajax_route_bootstrap(int requireWrite, int requirePost){
254
login_check_credentials();
255
if( requireWrite!=0 && g.perm.Write==0 ){
256
ajax_route_error(403,"Write permissions required.");
257
return 0;
258
}else if(requirePost && 0==cgi_csrf_safe(requirePost)){
259
ajax_route_error(403,
260
"CSRF violation (make sure sending of HTTP "
261
"Referer headers is enabled for XHR "
262
"connections).");
263
return 0;
264
}
265
cgi_set_content_type("application/json");
266
return 1;
267
}
268
269
/*
270
** Helper for collecting filename/check-in request parameters.
271
**
272
** If zFn is not NULL, it is assigned the value of the first one of
273
** the "filename" or "fn" CGI parameters which is set.
274
**
275
** If zCi is not NULL, it is assigned the value of the first one of
276
** the "checkin" or "ci" CGI parameters which is set.
277
**
278
** If a parameter is not NULL, it will be assigned NULL if the
279
** corresponding parameter is not set.
280
**
281
** Returns the number of non-NULL values it assigns to arguments. Thus
282
** if passed (&x, NULL), it returns 1 if it assigns non-NULL to *x and
283
** 0 if it assigns NULL to *x.
284
*/
285
int ajax_get_fnci_args( const char **zFn, const char **zCi ){
286
int rc = 0;
287
if(zCi!=0){
288
*zCi = PD("checkin",P("ci"));
289
if( *zCi ) ++rc;
290
}
291
if(zFn!=0){
292
*zFn = PD("filename",P("fn"));
293
if (*zFn) ++rc;
294
}
295
return rc;
296
}
297
298
/*
299
** AJAX route /ajax/preview-text
300
**
301
** Required query parameters:
302
**
303
** filename=name of content, for use in determining the
304
** mimetype/render mode.
305
**
306
** content=text
307
**
308
** Optional query parameters:
309
**
310
** render_mode=integer (AJAX_RENDER_xxx) (default=AJAX_RENDER_GUESS)
311
**
312
** ln=0 or 1 to disable/enable line number mode in
313
** AJAX_RENDER_PLAIN_TEXT mode.
314
**
315
** iframe_height=integer (default=40) Height, in EMs of HTML preview
316
** iframe.
317
**
318
** Responds with the HTML content of the preview. On error it produces
319
** a JSON response as documented for ajax_route_error().
320
**
321
** Extra response headers:
322
**
323
** x-ajax-render-mode: string representing the rendering mode
324
** which was really used (which will differ from the requested mode
325
** only if mode 0 (guess) was requested). The names are documented
326
** below in code and match those in the emitted JS object
327
** fossil.page.previewModes.
328
*/
329
void ajax_route_preview_text(void){
330
const char * zFilename = 0;
331
const char * zContent = P("content");
332
int renderMode = atoi(PD("render_mode","0"));
333
int ln = atoi(PD("ln","0"));
334
int iframeHeight = atoi(PD("iframe_height","40"));
335
Blob content = empty_blob;
336
const char * zRenderMode = 0;
337
338
ajax_get_fnci_args( &zFilename, 0 );
339
340
if(!ajax_route_bootstrap(0,1)){
341
return;
342
}
343
if(zFilename==0){
344
/* The filename is only used for mimetype determination,
345
** so we can default it... */
346
zFilename = "foo.txt";
347
}
348
cgi_set_content_type("text/html");
349
blob_init(&content, zContent, -1);
350
ajax_render_preview(&content, zFilename,
351
ln ? AJAX_PREVIEW_LINE_NUMBERS : 0,
352
&renderMode, iframeHeight);
353
/*
354
** Now tell the caller if we did indeed use AJAX_RENDER_WIKI, so that
355
** they can re-set the <base href> to an appropriate value (which
356
** requires knowing the content's current check-in version, which we
357
** don't have here).
358
*/
359
switch(renderMode){
360
/* The strings used here MUST correspond to those used in the JS-side
361
** fossil.page.previewModes map.
362
*/
363
case AJAX_RENDER_WIKI: zRenderMode = "wiki"; break;
364
case AJAX_RENDER_HTML_INLINE: zRenderMode = "htmlInline"; break;
365
case AJAX_RENDER_HTML_IFRAME: zRenderMode = "htmlIframe"; break;
366
case AJAX_RENDER_PLAIN_TEXT: zRenderMode = "text"; break;
367
case AJAX_RENDER_GUESS:
368
assert(!"cannot happen");
369
}
370
if(zRenderMode!=0){
371
cgi_printf_header("x-ajax-render-mode: %s\r\n", zRenderMode);
372
}
373
}
374
375
/*
376
** AJAX route /ajax/artifact.json.
377
** URL arguments:
378
**
379
** uuid=ARTIFACT_ID REQUIRED
380
**
381
** and emits either:
382
**
383
** { error: "..." }
384
**
385
** with a non-200 response code or the artifact's manifest in JSON
386
** form with a 200 response code.
387
*/
388
void ajax_route_artifact_json(void){
389
const char *zUuid = P("uuid");
390
Blob json = BLOB_INITIALIZER;
391
login_check_credentials();
392
if( ! g.perm.Read ){
393
ajax_route_error_forbidden();
394
}else if( artifact_to_json_by_name(zUuid, &json) ){
395
@ %b(&json)
396
}else{
397
ajax_route_error_404("Cannot resolve artifact ID.");
398
}
399
blob_reset(&json);
400
}
401
402
#if INTERFACE
403
/*
404
** Internal mapping of ajax sub-route names to various metadata.
405
*/
406
struct AjaxRoute {
407
const char *zName; /* Name part of the route after "ajax/" */
408
void (*xCallback)(); /* Impl function for the route. */
409
int bWriteMode; /* True if requires write mode */
410
int bPost; /* True if requires POST (i.e. CSRF
411
** verification). Value is passed to
412
** cgi_csrf_safe(). */
413
};
414
typedef struct AjaxRoute AjaxRoute;
415
#endif /*INTERFACE*/
416
417
/*
418
** Comparison function for bsearch() for searching an AjaxRoute
419
** list for a matching name.
420
*/
421
int cmp_ajax_route_name(const void *a, const void *b){
422
const AjaxRoute * rA = (const AjaxRoute*)a;
423
const AjaxRoute * rB = (const AjaxRoute*)b;
424
return fossil_strcmp(rA->zName, rB->zName);
425
}
426
427
/*
428
** WEBPAGE: ajax hidden
429
**
430
** The main dispatcher for shared ajax-served routes. Requires the
431
** 'name' parameter be the main route's name (as defined in a list in
432
** this function), noting that fossil automatically assigns all path
433
** parts after "ajax" to "name", e.g. /ajax/foo/bar assigns
434
** name=foo/bar.
435
**
436
** This "page" is only intended to be used by higher-level pages which
437
** have certain Ajax-driven features in common. It is not intended to
438
** be used by clients and NONE of its HTTP interfaces are considered
439
** documented/stable/supported - they may change on any given build of
440
** fossil.
441
**
442
** The exact response type depends on the route which gets called. In
443
** the case of an initialization error it emits a JSON-format response
444
** as documented for ajax_route_error(). Individual routes may emit
445
** errors in different formats, e.g. HTML.
446
*/
447
void ajax_route_dispatcher(void){
448
const char * zName = P("name");
449
AjaxRoute routeName = {0,0,0,0};
450
const AjaxRoute * pRoute = 0;
451
const AjaxRoute routes[] = {
452
/* Keep these sorted by zName (for bsearch()) */
453
{"artifact.json", ajax_route_artifact_json, 0, 0},
454
{"preview-text", ajax_route_preview_text, 0, 1
455
/* Preview does not require write permissions in the repo. It
456
** should arguably require write permissions simply to limit abuse
457
** but doing means that /chat does not work without check-in
458
** permissions:
459
**
460
** https://fossil-scm.org/forum/forumpost/ed4a762b3a557898
461
**
462
** This particular route is used by /fileedit and /chat, whereas
463
** /wikiedit uses a simpler wiki-specific route.
464
*/
465
/* TODO (2026-06-09): preview.txt, preview.md, preview.wiki as
466
** shorthand for preview-text?filename=X.(txt|md|wiki), noting that
467
** the filename is only used for mimetype determination. */
468
}
469
};
470
471
if(zName==0 || zName[0]==0){
472
ajax_route_error(400,"Missing required [route] 'name' parameter.");
473
return;
474
}
475
routeName.zName = zName;
476
pRoute = (const AjaxRoute *)bsearch(&routeName, routes,
477
count(routes), sizeof routes[0],
478
cmp_ajax_route_name);
479
if(pRoute==0){
480
ajax_route_error(404,"Ajax route not found.");
481
return;
482
}else if(0==ajax_route_bootstrap(pRoute->bWriteMode, pRoute->bPost)){
483
return;
484
}
485
pRoute->xCallback();
486
}
487

Keyboard Shortcuts

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