/* ** Copyright (c) 2020 D. Richard Hipp ** ** This program is free software; you can redistribute it and/or ** modify it under the terms of the Simplified BSD License (also ** known as the "2-Clause License" or "FreeBSD License".) ** ** This program is distributed in the hope that it will be useful, ** but without any warranty; without even the implied warranty of ** merchantability or fitness for a particular purpose. ** ** Author contact information: ** drh@hwaci.com ** http://www.hwaci.com/drh/ ** ******************************************************************************* ** ** This file contains shared Ajax-related code for /fileedit, the wiki/forum ** editors, and friends. */ #include "config.h" #include "ajax.h" #include #include #if INTERFACE /* enum ajax_render_preview_flags: */ #define AJAX_PREVIEW_LINE_NUMBERS 1 /* enum ajax_render_modes: */ #define AJAX_RENDER_GUESS 0 /* Guess rendering mode based on mimetype. */ /* GUESS must be 0. All others have unspecified values. */ #define AJAX_RENDER_PLAIN_TEXT 1 /* Render as plain text. */ #define AJAX_RENDER_HTML_IFRAME 2 /* Render as HTML inside an IFRAME. */ #define AJAX_RENDER_HTML_INLINE 3 /* Render as HTML without an IFRAME. */ #define AJAX_RENDER_WIKI 4 /* Render as wiki/markdown. */ #endif /* ** Emits JS code which initializes the ** fossil.page.previewModes object to a map of AJAX_RENDER_xxx values ** and symbolic names for use by client-side scripts. ** ** If addScriptTag is true then the output is wrapped in a SCRIPT tag ** with the current nonce, else no SCRIPT tag is emitted. ** ** Requires that style_emit_script_fossil_bootstrap() has already been ** called in order to initialize the window.fossil.page object. */ void ajax_emit_js_preview_modes(int addScriptTag){ if(addScriptTag){ style_emit_script_tag(0,0); CX("\n"); } CX("fossil.page.previewModes={" "guess: %d, %d: 'guess', wiki: %d, %d: 'wiki'," "htmlIframe: %d, %d: 'htmlIframe', " "htmlInline: %d, %d: 'htmlInline', " "text: %d, %d: 'text'" "};\n", AJAX_RENDER_GUESS, AJAX_RENDER_GUESS, AJAX_RENDER_WIKI, AJAX_RENDER_WIKI, AJAX_RENDER_HTML_IFRAME, AJAX_RENDER_HTML_IFRAME, AJAX_RENDER_HTML_INLINE, AJAX_RENDER_HTML_INLINE, AJAX_RENDER_PLAIN_TEXT, AJAX_RENDER_PLAIN_TEXT); if(addScriptTag){ style_emit_script_tag(1,0); } } /* ** Returns a value from the ajax_render_modes enum, based on the ** given mime type string (which may be NULL), defaulting to ** AJAX_RENDER_PLAIN_TEXT. */ int ajax_render_mode_for_mimetype(const char * zMimetype){ int rc = AJAX_RENDER_PLAIN_TEXT; if( zMimetype ){ if( fossil_strcmp(zMimetype, "text/html")==0 ){ rc = AJAX_RENDER_HTML_IFRAME; }else if( fossil_strcmp(zMimetype, "text/x-fossil-wiki")==0 || fossil_strcmp(zMimetype, "text/x-markdown")==0 ){ rc = AJAX_RENDER_WIKI; } } return rc; } /* ** Renders text/wiki content preview for various /ajax routes. ** ** pContent is text/wiki content to preview. zName is the name of the ** content, for purposes of determining the mimetype based on the ** extension (if NULL, mimetype text/plain is assumed). flags may be a ** bitmask of values from the ajax_render_preview_flags ** enum. *renderMode must specify the render mode to use. If ** *renderMode==AJAX_RENDER_GUESS then *renderMode gets set to the ** mode which is guessed at for the rendering (based on the mimetype). ** ** nIframeHeightEm is only used for the AJAX_RENDER_HTML_IFRAME ** renderMode, and specifies the height, in EM's, of the resulting ** iframe. If passed 0, it defaults to "some sane value." */ void ajax_render_preview(Blob * pContent, const char *zName, int flags, int * renderMode, int nIframeHeightEm){ const char * zMime; zMime = zName ? mimetype_from_name(zName) : "text/plain"; if(AJAX_RENDER_GUESS==*renderMode){ *renderMode = ajax_render_mode_for_mimetype(zMime); } switch(*renderMode){ case AJAX_RENDER_HTML_IFRAME:{ char * z64 = encode64(blob_str(pContent), blob_size(pContent)); CX("", nIframeHeightEm ? nIframeHeightEm : 40, z64); break; } case AJAX_RENDER_HTML_INLINE:{ CX("%b",pContent); break; } case AJAX_RENDER_WIKI: safe_html_context(DOCSRC_FILE); wiki_render_by_mimetype(pContent, zMime); break; default:{ const char *zContent = blob_str(pContent); if(AJAX_PREVIEW_LINE_NUMBERS & flags){ output_text_with_line_numbers(zContent, "on"); }else{ const char *zExt = strrchr(zName,'.'); if(zExt && zExt[1]){ CX("
%h
", zExt+1, zContent); }else{ CX("
%h
", zContent); } } break; } } } /* ** Renders diffs for ajax routes. pOrig is the "original" (v1) content ** and pContent is the locally-edited (v2) content. diffFlags is any ** set of flags suitable for passing to text_diff(). */ void ajax_render_diff(Blob * pOrig, Blob *pContent, u64 diffFlags){ Blob out = empty_blob; text_diff(pOrig, pContent, &out, 0, diffFlags); if(blob_size(&out)==0){ /* nothing to do */ }else if(DIFF_SIDEBYSIDE & diffFlags){ CX("%b",&out); }else{ CX("
%b
",&out); } blob_reset(&out); } /* ** Uses P(zKey) to fetch a CGI environment variable. If that var is ** NULL or starts with '0' or 'f' then this function returns false, ** else it returns true. */ int ajax_p_bool(char const *zKey){ const char * zVal = P(zKey); return (!zVal || '0'==*zVal || 'f'==*zVal) ? 0 : 1; } /* ** Helper for /ajax routes. Clears the CGI content buffer, sets an ** HTTP error status code, and queues up a JSON response in the form ** of an object: ** ** {error: formatted message} ** ** If httpCode<=0 then it defaults to 500. ** ** After calling this, the caller should immediately return. */ void ajax_route_error(int httpCode, const char * zFmt, ...){ Blob msg = empty_blob; Blob content = empty_blob; va_list vargs; va_start(vargs,zFmt); blob_vappendf(&msg, zFmt, vargs); va_end(vargs); blob_appendf(&content,"{\"error\":%!j}", blob_str(&msg)); blob_reset(&msg); cgi_set_content(&content); cgi_set_status(httpCode>0 ? httpCode : 500, "Error"); cgi_set_content_type("application/json"); } /* ** Performs bootstrapping common to the /ajax/xyz AJAX routes, such as ** logging in the user. ** ** Returns false (0) if bootstrapping fails, in which case it has ** reported the error and the route should immediately return. Returns ** true on success. ** ** If requireWrite is true then write permissions are required. ** If requirePost is true then the request is assumed to be using ** POST'ed data and CSRF validation is performed. ** */ int ajax_route_bootstrap(int requireWrite, int requirePost){ login_check_credentials(); if( requireWrite!=0 && g.perm.Write==0 ){ ajax_route_error(403,"Write permissions required."); return 0; }else if(0==cgi_csrf_safe(requirePost)){ ajax_route_error(403, "CSRF violation (make sure sending of HTTP " "Referer headers is enabled for XHR " "connections)."); return 0; } return 1; } /* ** Helper for collecting filename/checkin request parameters. ** ** If zFn is not NULL, it is assigned the value of the first one of ** the "filename" or "fn" CGI parameters which is set. ** ** If zCi is not NULL, it is assigned the value of the first one of ** the "checkin" or "ci" CGI parameters which is set. ** ** If a parameter is not NULL, it will be assigned NULL if the ** corresponding parameter is not set. ** ** Returns the number of non-NULL values it assigns to arguments. Thus ** if passed (&x, NULL), it returns 1 if it assigns non-NULL to *x and ** 0 if it assigns NULL to *x. */ int ajax_get_fnci_args( const char **zFn, const char **zCi ){ int rc = 0; if(zCi!=0){ *zCi = PD("checkin",P("ci")); if( *zCi ) ++rc; } if(zFn!=0){ *zFn = PD("filename",P("fn")); if (*zFn) ++rc; } return rc; } /* ** AJAX route /ajax/preview-wiki ** ** Required query parameters: ** ** filename=name of content, for use in determining the ** mimetype/render mode. content=text ** ** Optional query parameters: ** ** render_mode=integer (AJAX_RENDER_xxx) (default=AJAX_RENDER_GUESS) ** ** ln=0 or 1 to disable/enable line number mode in ** AJAX_RENDER_PLAIN_TEXT mode. ** ** iframe_height=integer (default=40) Height, in EMs of HTML preview ** iframe. ** ** User must have Write access to use this page. ** ** Responds with the HTML content of the preview. On error it produces ** a JSON response as documented for ajax_route_error(). ** ** Extra response headers: ** ** x-ajax-render-mode: string representing the rendering mode ** which was really used (which will differ from the requested mode ** only if mode 0 (guess) was requested). The names are documented ** below in code and match those in the emitted JS object ** fossil.page.previewModes. */ void ajax_route_preview_text(void){ const char * zFilename = 0; const char * zContent = P("content"); int renderMode = atoi(PD("render_mode","0")); int ln = atoi(PD("ln","0")); int iframeHeight = atoi(PD("iframe_height","40")); Blob content = empty_blob; const char * zRenderMode = 0; ajax_get_fnci_args( &zFilename, 0 ); if(!ajax_route_bootstrap(1,1)){ return; } if(zFilename==0){ /* The filename is only used for mimetype determination, ** so we can default it... */ zFilename = "foo.txt"; } cgi_set_content_type("text/html"); blob_init(&content, zContent, -1); ajax_render_preview(&content, zFilename, ln ? AJAX_PREVIEW_LINE_NUMBERS : 0, &renderMode, iframeHeight); /* ** Now tell the caller if we did indeed use AJAX_RENDER_WIKI, so that ** they can re-set the to an appropriate value (which ** requires knowing the content's current checkin version, which we ** don't have here). */ switch(renderMode){ /* The strings used here MUST correspond to those used in the JS-side ** fossil.page.previewModes map. */ case AJAX_RENDER_WIKI: zRenderMode = "wiki"; break; case AJAX_RENDER_HTML_INLINE: zRenderMode = "htmlInline"; break; case AJAX_RENDER_HTML_IFRAME: zRenderMode = "htmlIframe"; break; case AJAX_RENDER_PLAIN_TEXT: zRenderMode = "text"; break; case AJAX_RENDER_GUESS: assert(!"cannot happen"); } if(zRenderMode!=0){ cgi_printf_header("x-ajax-render-mode: %s\r\n", zRenderMode); } } #if INTERFACE /* ** Internal mapping of ajax sub-route names to various metadata. */ struct AjaxRoute { const char *zName; /* Name part of the route after "ajax/" */ void (*xCallback)(); /* Impl function for the route. */ int bWriteMode; /* True if requires write mode */ int bPost; /* True if requires POST (i.e. CSRF ** verification) */ }; typedef struct AjaxRoute AjaxRoute; #endif /*INTERFACE*/ /* ** Comparison function for bsearch() for searching an AjaxRoute ** list for a matching name. */ int cmp_ajax_route_name(const void *a, const void *b){ const AjaxRoute * rA = (const AjaxRoute*)a; const AjaxRoute * rB = (const AjaxRoute*)b; return fossil_strcmp(rA->zName, rB->zName); } /* ** WEBPAGE: ajax ** ** The main dispatcher for shared ajax-served routes. Requires the ** 'name' parameter be the main route's name (as defined in a list in ** this function), noting that fossil automatically assigns all path ** parts after "ajax" to "name", e.g. /ajax/foo/bar assigns ** name=foo/bar. ** ** This "page" is only intended to be used by higher-level pages which ** have certain Ajax-driven features in common. It is not intended to ** be used by clients and NONE of its HTTP interfaces are considered ** documented/stable/supported - they may change on any given build of ** fossil. ** ** The exact response type depends on the route which gets called. In ** the case of an initialization error it emits a JSON-format response ** as documented for ajax_route_error(). Individual routes may emit ** errors in different formats, e.g. HTML. */ void ajax_route_dispatcher(void){ const char * zName = P("name"); AjaxRoute routeName = {0,0,0,0}; const AjaxRoute * pRoute = 0; const AjaxRoute routes[] = { /* Keep these sorted by zName (for bsearch()) */ {"preview-text", ajax_route_preview_text, 1, 1} }; if(zName==0 || zName[0]==0){ ajax_route_error(400,"Missing required [route] 'name' parameter."); return; } routeName.zName = zName; pRoute = (const AjaxRoute *)bsearch(&routeName, routes, count(routes), sizeof routes[0], cmp_ajax_route_name); if(pRoute==0){ ajax_route_error(404,"Ajax route not found."); return; }else if(0==ajax_route_bootstrap(pRoute->bWriteMode, pRoute->bPost)){ return; } pRoute->xCallback(); }