diff options
| author | cashmere <cashmere@autistici.org> | 2026-09-04 09:05:24 +0200 |
|---|---|---|
| committer | cashmere <cashmere@autistici.org> | 2026-09-04 09:05:24 +0200 |
| commit | 4f49b16079b27d7ce633619e4cddfdf2bffbc952 (patch) | |
| tree | c296c1e7f3c3e2f6859c6d3a1b1b208779b89679 /src/doc.c | |
| download | relic-master.tar.gz | |
Diffstat (limited to 'src/doc.c')
| -rw-r--r-- | src/doc.c | 1277 |
1 files changed, 1277 insertions, 0 deletions
diff --git a/src/doc.c b/src/doc.c new file mode 100644 index 0000000..35e0709 --- /dev/null +++ b/src/doc.c @@ -0,0 +1,1277 @@ +/* +** Copyright (c) 2007 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 code to implement the "/doc" web page and related +** pages. +*/ +#include "config.h" +#include "doc.h" +#include <assert.h> + +/* +** Try to guess the mimetype from content. +** +** If the content is pure text, return NULL. +** +** For image types, attempt to return an appropriate mimetype +** name like "image/gif" or "image/jpeg". +** +** For any other binary type, return "unknown/unknown". +*/ +const char *mimetype_from_content(Blob *pBlob){ + int i; + int n; + const unsigned char *x; + + /* A table of mimetypes based on file content prefixes + */ + static const struct { + const char *z; /* Identifying file text */ + const unsigned char sz1; /* Length of the prefix */ + const unsigned char of2; /* Offset to the second segment */ + const unsigned char sz2; /* Size of the second segment */ + const unsigned char mn; /* Minimum size of input */ + const char *zMimetype; /* The corresponding mimetype */ + } aMime[] = { + { "GIF87a", 6, 0, 0, 6, "image/gif" }, + { "GIF89a", 6, 0, 0, 6, "image/gif" }, + { "\211PNG\r\n\032\n", 8, 0, 0, 8, "image/png" }, + { "\377\332\377", 3, 0, 0, 3, "image/jpeg" }, + { "\377\330\377", 3, 0, 0, 3, "image/jpeg" }, + { "RIFFWAVEfmt", 4, 8, 7, 15, "sound/wav" }, + }; + + if( !looks_like_binary(pBlob) ) { + return 0; /* Plain text */ + } + x = (const unsigned char*)blob_buffer(pBlob); + n = blob_size(pBlob); + for(i=0; i<count(aMime); i++){ + if( n<aMime[i].mn ) continue; + if( memcmp(x, aMime[i].z, aMime[i].sz1)!=0 ) continue; + if( aMime[i].sz2 + && memcmp(x+aMime[i].of2, aMime[i].z+aMime[i].sz1, aMime[i].sz2)!=0 + ){ + continue; + } + return aMime[i].zMimetype; + } + return "unknown/unknown"; +} + +/* A table of mimetypes based on file suffixes. +** Suffixes must be in sorted order so that we can do a binary +** search to find the mimetype. +*/ +static const struct { + const char *zSuffix; /* The file suffix */ + int size; /* Length of the suffix */ + const char *zMimetype; /* The corresponding mimetype */ +} aMime[] = { + { "ai", 2, "application/postscript" }, + { "aif", 3, "audio/x-aiff" }, + { "aifc", 4, "audio/x-aiff" }, + { "aiff", 4, "audio/x-aiff" }, + { "arj", 3, "application/x-arj-compressed" }, + { "asc", 3, "text/plain" }, + { "asf", 3, "video/x-ms-asf" }, + { "asx", 3, "video/x-ms-asx" }, + { "au", 2, "audio/ulaw" }, + { "avi", 3, "video/x-msvideo" }, + { "bat", 3, "application/x-msdos-program" }, + { "bcpio", 5, "application/x-bcpio" }, + { "bin", 3, "application/octet-stream" }, + { "bmp", 3, "image/bmp" }, + { "bz2", 3, "application/x-bzip2" }, + { "bzip", 4, "application/x-bzip" }, + { "c", 1, "text/plain" }, + { "cc", 2, "text/plain" }, + { "ccad", 4, "application/clariscad" }, + { "cdf", 3, "application/x-netcdf" }, + { "class", 5, "application/octet-stream" }, + { "cod", 3, "application/vnd.rim.cod" }, + { "com", 3, "application/x-msdos-program" }, + { "cpio", 4, "application/x-cpio" }, + { "cpt", 3, "application/mac-compactpro" }, + { "cs", 2, "text/plain" }, + { "csh", 3, "application/x-csh" }, + { "css", 3, "text/css" }, + { "csv", 3, "text/csv" }, + { "dcr", 3, "application/x-director" }, + { "deb", 3, "application/x-debian-package" }, + { "dib", 3, "image/bmp" }, + { "dir", 3, "application/x-director" }, + { "dl", 2, "video/dl" }, + { "dms", 3, "application/octet-stream" }, + { "doc", 3, "application/msword" }, + { "docx", 4, "application/vnd.openxmlformats-" + "officedocument.wordprocessingml.document"}, + { "dot", 3, "application/msword" }, + { "dotx", 4, "application/vnd.openxmlformats-" + "officedocument.wordprocessingml.template"}, + { "drw", 3, "application/drafting" }, + { "dvi", 3, "application/x-dvi" }, + { "dwg", 3, "application/acad" }, + { "dxf", 3, "application/dxf" }, + { "dxr", 3, "application/x-director" }, + { "eps", 3, "application/postscript" }, + { "etx", 3, "text/x-setext" }, + { "exe", 3, "application/octet-stream" }, + { "ez", 2, "application/andrew-inset" }, + { "f", 1, "text/plain" }, + { "f90", 3, "text/plain" }, + { "fli", 3, "video/fli" }, + { "flv", 3, "video/flv" }, + { "gif", 3, "image/gif" }, + { "gl", 2, "video/gl" }, + { "gtar", 4, "application/x-gtar" }, + { "gz", 2, "application/x-gzip" }, + { "h", 1, "text/plain" }, + { "hdf", 3, "application/x-hdf" }, + { "hh", 2, "text/plain" }, + { "hqx", 3, "application/mac-binhex40" }, + { "htm", 3, "text/html" }, + { "html", 4, "text/html" }, + { "ice", 3, "x-conference/x-cooltalk" }, + { "ico", 3, "image/vnd.microsoft.icon" }, + { "ief", 3, "image/ief" }, + { "iges", 4, "model/iges" }, + { "igs", 3, "model/iges" }, + { "ips", 3, "application/x-ipscript" }, + { "ipx", 3, "application/x-ipix" }, + { "jad", 3, "text/vnd.sun.j2me.app-descriptor" }, + { "jar", 3, "application/java-archive" }, + { "jp2", 3, "image/jp2" }, + { "jpe", 3, "image/jpeg" }, + { "jpeg", 4, "image/jpeg" }, + { "jpg", 3, "image/jpeg" }, + { "js", 2, "text/javascript" }, + /* application/javascript is commonly used for JS, but the + ** spec says text/javascript is correct: + ** https://html.spec.whatwg.org/multipage/scripting.html + ** #scriptingLanguages:javascript-mime-type */ + { "json", 4, "application/json" }, + { "kar", 3, "audio/midi" }, + { "latex", 5, "application/x-latex" }, + { "lha", 3, "application/octet-stream" }, + { "lsp", 3, "application/x-lisp" }, + { "lzh", 3, "application/octet-stream" }, + { "m", 1, "text/plain" }, + { "m3u", 3, "audio/x-mpegurl" }, + { "m4a", 3, "audio/mp4" + /* Many sources call this audio/m4a, but that's + deprecated */ }, + { "man", 3, "text/plain" }, + { "markdown", 8, "text/x-markdown" }, + { "md", 2, "text/x-markdown" }, + { "me", 2, "application/x-troff-me" }, + { "mesh", 4, "model/mesh" }, + { "mid", 3, "audio/midi" }, + { "midi", 4, "audio/midi" }, + { "mif", 3, "application/x-mif" }, + { "mime", 4, "www/mime" }, + { "mjs", 3, "text/javascript" /*ES6 module*/ }, + { "mkd", 3, "text/x-markdown" }, + { "mov", 3, "video/quicktime" }, + { "movie", 5, "video/x-sgi-movie" }, + { "mp2", 3, "audio/mpeg" }, + { "mp3", 3, "audio/mpeg" }, + { "mp4", 3, "video/mp4" }, + { "mpe", 3, "video/mpeg" }, + { "mpeg", 4, "video/mpeg" }, + { "mpg", 3, "video/mpeg" }, + { "mpga", 4, "audio/mpeg" }, + { "ms", 2, "application/x-troff-ms" }, + { "msh", 3, "model/mesh" }, + { "n", 1, "text/plain" }, + { "nc", 2, "application/x-netcdf" }, + { "oda", 3, "application/oda" }, + { "odp", 3, "application/vnd.oasis.opendocument.presentation" }, + { "ods", 3, "application/vnd.oasis.opendocument.spreadsheet" }, + { "odt", 3, "application/vnd.oasis.opendocument.text" }, + { "ogg", 3, "application/ogg" }, + { "ogm", 3, "application/ogg" }, + { "org", 3, "text/x-org" }, + { "otf", 3, "font/otf" }, + { "pbm", 3, "image/x-portable-bitmap" }, + { "pdb", 3, "chemical/x-pdb" }, + { "pdf", 3, "application/pdf" }, + { "pgm", 3, "image/x-portable-graymap" }, + { "pgn", 3, "application/x-chess-pgn" }, + { "pgp", 3, "application/pgp" }, + { "pikchr", 6, "text/x-pikchr" }, + { "pl", 2, "application/x-perl" }, + { "pm", 2, "application/x-perl" }, + { "png", 3, "image/png" }, + { "pnm", 3, "image/x-portable-anymap" }, + { "pot", 3, "application/mspowerpoint" }, + { "potx", 4, "application/vnd.openxmlformats-" + "officedocument.presentationml.template"}, + { "ppm", 3, "image/x-portable-pixmap" }, + { "pps", 3, "application/mspowerpoint" }, + { "ppsx", 4, "application/vnd.openxmlformats-" + "officedocument.presentationml.slideshow"}, + { "ppt", 3, "application/mspowerpoint" }, + { "pptx", 4, "application/vnd.openxmlformats-" + "officedocument.presentationml.presentation"}, + { "ppz", 3, "application/mspowerpoint" }, + { "pre", 3, "application/x-freelance" }, + { "prt", 3, "application/pro_eng" }, + { "ps", 2, "application/postscript" }, + { "qt", 2, "video/quicktime" }, + { "ra", 2, "audio/x-realaudio" }, + { "ram", 3, "audio/x-pn-realaudio" }, + { "rar", 3, "application/x-rar-compressed" }, + { "ras", 3, "image/cmu-raster" }, + { "rgb", 3, "image/x-rgb" }, + { "rm", 2, "audio/x-pn-realaudio" }, + { "roff", 4, "application/x-troff" }, + { "rpm", 3, "audio/x-pn-realaudio-plugin" }, + { "rtf", 3, "text/rtf" }, + { "rtx", 3, "text/richtext" }, + { "scm", 3, "application/x-lotusscreencam" }, + { "set", 3, "application/set" }, + { "sgm", 3, "text/sgml" }, + { "sgml", 4, "text/sgml" }, + { "sh", 2, "application/x-sh" }, + { "shar", 4, "application/x-shar" }, + { "silo", 4, "model/mesh" }, + { "sit", 3, "application/x-stuffit" }, + { "skd", 3, "application/x-koan" }, + { "skm", 3, "application/x-koan" }, + { "skp", 3, "application/x-koan" }, + { "skt", 3, "application/x-koan" }, + { "smi", 3, "application/smil" }, + { "smil", 4, "application/smil" }, + { "snd", 3, "audio/basic" }, + { "sol", 3, "application/solids" }, + { "spl", 3, "application/x-futuresplash" }, + { "sql", 3, "application/sql" }, + { "src", 3, "application/x-wais-source" }, + { "step", 4, "application/STEP" }, + { "stl", 3, "application/SLA" }, + { "stp", 3, "application/STEP" }, + { "sv4cpio", 7, "application/x-sv4cpio" }, + { "sv4crc", 6, "application/x-sv4crc" }, + { "svg", 3, "image/svg+xml" }, + { "swf", 3, "application/x-shockwave-flash" }, + { "t", 1, "application/x-troff" }, + { "tar", 3, "application/x-tar" }, + { "tcl", 3, "application/x-tcl" }, + { "tex", 3, "application/x-tex" }, + { "texi", 4, "application/x-texinfo" }, + { "texinfo", 7, "application/x-texinfo" }, + { "tgz", 3, "application/x-tar-gz" }, + { "th1", 3, "application/x-th1" }, + { "tif", 3, "image/tiff" }, + { "tiff", 4, "image/tiff" }, + { "tr", 2, "application/x-troff" }, + { "tsi", 3, "audio/TSP-audio" }, + { "tsp", 3, "application/dsptype" }, + { "tsv", 3, "text/tab-separated-values" }, + { "txt", 3, "text/plain" }, + { "unv", 3, "application/i-deas" }, + { "ustar", 5, "application/x-ustar" }, + { "vb", 2, "text/plain" }, + { "vcd", 3, "application/x-cdlink" }, + { "vda", 3, "application/vda" }, + { "viv", 3, "video/vnd.vivo" }, + { "vivo", 4, "video/vnd.vivo" }, + { "vrml", 4, "model/vrml" }, + { "wasm", 4, "application/wasm" }, + { "wav", 3, "audio/x-wav" }, + { "wax", 3, "audio/x-ms-wax" }, + { "webp", 4, "image/webp" }, + { "wiki", 4, "text/x-fossil-wiki" }, + { "wma", 3, "audio/x-ms-wma" }, + { "wmv", 3, "video/x-ms-wmv" }, + { "wmx", 3, "video/x-ms-wmx" }, + { "wrl", 3, "model/vrml" }, + { "wvx", 3, "video/x-ms-wvx" }, + { "xbm", 3, "image/x-xbitmap" }, + { "xlc", 3, "application/vnd.ms-excel" }, + { "xll", 3, "application/vnd.ms-excel" }, + { "xlm", 3, "application/vnd.ms-excel" }, + { "xls", 3, "application/vnd.ms-excel" }, + { "xlsx", 4, "application/vnd.openxmlformats-" + "officedocument.spreadsheetml.sheet"}, + { "xlw", 3, "application/vnd.ms-excel" }, + { "xml", 3, "text/xml" }, + { "xpm", 3, "image/x-xpixmap" }, + { "xsl", 3, "text/xml" }, + { "xslt", 4, "text/xml" }, + { "xwd", 3, "image/x-xwindowdump" }, + { "xyz", 3, "chemical/x-pdb" }, + { "zip", 3, "application/zip" }, +}; + +/* +** Verify that all entries in the aMime[] table are in sorted order. +** Abort with a fatal error if any is out-of-order. +*/ +static void mimetype_verify(void){ + int i; + for(i=1; i<count(aMime); i++){ + if( fossil_strcmp(aMime[i-1].zSuffix,aMime[i].zSuffix)>=0 ){ + fossil_panic("mimetypes out of sequence: %s before %s", + aMime[i-1].zSuffix, aMime[i].zSuffix); + } + } +} + +/* +** Looks in the contents of the "mimetypes" setting for a suffix +** matching zSuffix. If found, it returns the configured value +** in memory owned by the app (i.e. do not free() it), else it +** returns 0. +** +** The mimetypes setting is expected to be a list of file extensions +** and mimetypes, with one such mapping per line. A leading '.' on +** extensions is permitted for compatibility with lists imported from +** other tools which require them. +*/ +static const char *mimetype_from_name_custom(const char *zSuffix){ + static char * zList = 0; + static char const * zEnd = 0; + static int once = 0; + char * z; + int tokenizerState /* 0=expecting a key, 1=skip next token, + ** 2=accept next token */; + if(once==0){ + once = 1; + zList = db_get("mimetypes",0); + if(zList==0){ + return 0; + } + /* Transform zList to simplify the main loop: + replace non-newline spaces with NUL bytes. */ + zEnd = zList + strlen(zList); + for(z = zList; z<zEnd; ++z){ + if('\n'==*z) continue; + else if(fossil_isspace(*z)){ + *z = 0; + } + } + }else if(zList==0){ + return 0; + } + tokenizerState = 0; + z = zList; + while( z<zEnd ){ + if(*z==0){ + ++z; + continue; + } + else if('\n'==*z){ + if(2==tokenizerState){ + /* We were expecting a value for a successful match + here, but got no value. Bail out. */ + break; + }else{ + /* May happen on malformed inputs. Skip this record. */ + tokenizerState = 0; + ++z; + continue; + } + } + switch(tokenizerState){ + case 0:{ /* This is a file extension */ + static char * zCase = 0; + if('.'==*z){ + /*ignore an optional leading dot, for compatibility + with some external mimetype lists*/; + if(++z==zEnd){ + break; + } + } + if(zCase<z){ + /*we have not yet case-folded this section: lower-case it*/ + for(zCase = z; zCase<zEnd && *zCase!=0; ++zCase){ + if(!(0x80 & *zCase)){ + *zCase = (char)fossil_tolower(*zCase); + } + } + } + if(strcmp(z,zSuffix)==0){ + tokenizerState = 2 /* Match: accept the next value. */; + }else{ + tokenizerState = 1 /* No match: skip the next value. */; + } + z += strlen(z); + break; + } + case 1: /* This is a value, but not a match. Skip it. */ + z += strlen(z); + break; + case 2: /* This is the value which matched the previous key. */; + return z; + default: + assert(!"cannot happen - invalid tokenizerState value."); + } + } + return 0; +} + +/* +** Emit Javascript which applies (or optionally can apply) to both the +** /doc and /wiki pages. None of this implements required +** functionality, just nice-to-haves. Any calls after the first are +** no-ops. +*/ +void document_emit_js(void){ + static int once = 0; + if(0==once++){ + builtin_fossil_js_bundle_or("pikchr", NULL); + style_script_begin(__FILE__,__LINE__); + CX("window.addEventListener('load', " + "()=>window.fossil.pikchr.addSrcView(), " + "false);\n"); + style_script_end(); + } +} + +/* +** Guess the mimetype of a document based on its name. +*/ +const char *mimetype_from_name(const char *zName){ + const char *z; + int i; + int first, last; + int len; + char zSuffix[20]; + + +#ifdef FOSSIL_DEBUG + /* This is test code to make sure the table above is in the correct + ** order + */ + if( fossil_strcmp(zName, "mimetype-test")==0 ){ + mimetype_verify(); + return "ok"; + } +#endif + + z = zName; + for(i=0; zName[i]; i++){ + if( zName[i]=='.' ) z = &zName[i+1]; + } + len = strlen(z); + if( len<(int)sizeof(zSuffix)-1 ){ + sqlite3_snprintf(sizeof(zSuffix), zSuffix, "%s", z); + for(i=0; zSuffix[i]; i++) zSuffix[i] = fossil_tolower(zSuffix[i]); + z = mimetype_from_name_custom(zSuffix); + if(z!=0){ + return z; + } + first = 0; + last = count(aMime) - 1; + while( first<=last ){ + int c; + i = (first+last)/2; + c = fossil_strcmp(zSuffix, aMime[i].zSuffix); + if( c==0 ) return aMime[i].zMimetype; + if( c<0 ){ + last = i-1; + }else{ + first = i+1; + } + } + } + return "application/x-fossil-artifact"; +} + +/* +** COMMAND: test-mimetype +** +** Usage: %fossil test-mimetype FILENAME... +** +** Return the deduced mimetype for each file listed. +** +** If Fossil is compiled with -DFOSSIL_DEBUG then the "mimetype-test" +** filename is special and verifies the integrity of the mimetype table. +** It should return "ok". +*/ +void mimetype_test_cmd(void){ + int i; + mimetype_verify(); + db_find_and_open_repository(0, 0); + for(i=2; i<g.argc; i++){ + fossil_print("%-20s -> %s\n", g.argv[i], mimetype_from_name(g.argv[i])); + } +} + +/* +** WEBPAGE: mimetype_list +** +** Show the built-in table used to guess embedded document mimetypes +** from file suffixes. +*/ +void mimetype_list_page(void){ + int i; + char *zCustomList = 0; /* value of the mimetypes setting */ + int nCustomEntries = 0; /* number of entries in the mimetypes + ** setting */ + mimetype_verify(); + style_header("Mimetype List"); + @ <p>The Fossil <a href="%R/help/www/doc">/doc</a> page uses filename + @ suffixes and the following tables to guess at the appropriate mimetype + @ for each document. Mimetypes may be customized and overridden using + @ <a href="%R/help/mimetypes">the mimetypes config setting</a>.</p> + zCustomList = db_get("mimetypes",0); + if( zCustomList!=0 ){ + Blob list, entry, key, val; + @ <h1>Repository-specific mimetypes</h1> + @ <p>The following extension-to-mimetype mappings are defined via + @ the <a href="%R/help/mimetypes">mimetypes setting</a>.</p> + @ <table class='sortable mimetypetable' border=1 cellpadding=0 \ + @ data-column-types='tt' data-init-sort='0'> + @ <thead> + @ <tr><th>Suffix<th>Mimetype + @ </thead> + @ <tbody> + blob_set(&list, zCustomList); + while( blob_line(&list, &entry)>0 ){ + const char *zKey; + if( blob_token(&entry, &key)==0 ) continue; + if( blob_token(&entry, &val)==0 ) continue; + zKey = blob_str(&key); + if( zKey[0]=='.' ) zKey++; + @ <tr><td>%h(zKey)<td>%h(blob_str(&val))</tr> + nCustomEntries++; + } + fossil_free(zCustomList); + if( nCustomEntries==0 ){ + /* This can happen if the option is set to an empty/space-only + ** value. */ + @ <tr><td colspan="2"><em>none</em></tr> + } + @ </tbody></table> + } + @ <h1>Default built-in mimetypes</h1> + if(nCustomEntries>0){ + @ <p>Entries starting with an exclamation mark <em><strong>!</strong></em> + @ are overwritten by repository-specific settings.</p> + } + @ <table class='sortable mimetypetable' border=1 cellpadding=0 \ + @ data-column-types='tt' data-init-sort='1'> + @ <thead> + @ <tr><th>Suffix<th>Mimetype + @ </thead> + @ <tbody> + for(i=0; i<count(aMime); i++){ + const char *zFlag = ""; + if(nCustomEntries>0 && + mimetype_from_name_custom(aMime[i].zSuffix)!=0){ + zFlag = "<em><strong>!</strong></em> "; + } + @ <tr><td>%s(zFlag)%h(aMime[i].zSuffix)<td>%h(aMime[i].zMimetype)</tr> + } + @ </tbody></table> + style_table_sorter(); + style_finish_page(); +} + +/* +** Check to see if the file in the pContent blob is "embedded HTML". Return +** true if it is, and fill pTitle with the document title. +** +** An "embedded HTML" file is HTML that lacks a header and a footer. The +** standard Fossil header is prepended and the standard Fossil footer is +** appended. Otherwise, the file is displayed without change. +** +** Embedded HTML must be contained in a <div class='fossil-doc'> element. +** If that <div> also contains a data-title attribute, then the +** value of that attribute is extracted into pTitle and becomes the title +** of the document. +*/ +int doc_is_embedded_html(Blob *pContent, Blob *pTitle){ + const char *zIn = blob_str(pContent); + const char *zAttr; + const char *zValue; + int nAttr, nValue; + int seenClass = 0; + int seenTitle = 0; + + while( fossil_isspace(zIn[0]) ) zIn++; + if( fossil_strnicmp(zIn,"<div",4)!=0 ) return 0; + zIn += 4; + while( zIn[0] ){ + if( fossil_isspace(zIn[0]) ) zIn++; + if( zIn[0]=='>' ) break; + zAttr = zIn; + while( fossil_isalnum(zIn[0]) || zIn[0]=='-' ) zIn++; + nAttr = (int)(zIn - zAttr); + while( fossil_isspace(zIn[0]) ) zIn++; + if( zIn[0]!='=' ) continue; + zIn++; + while( fossil_isspace(zIn[0]) ) zIn++; + if( zIn[0]=='"' || zIn[0]=='\'' ){ + char cDelim = zIn[0]; + zIn++; + zValue = zIn; + while( zIn[0] && zIn[0]!=cDelim ) zIn++; + if( zIn[0]==0 ) return 0; + nValue = (int)(zIn - zValue); + zIn++; + }else{ + zValue = zIn; + while( zIn[0]!=0 && zIn[0]!='>' && zIn[0]!='/' + && !fossil_isspace(zIn[0]) ) zIn++; + if( zIn[0]==0 ) return 0; + nValue = (int)(zIn - zValue); + } + if( nAttr==5 && fossil_strnicmp(zAttr,"class",5)==0 ){ + if( nValue!=10 || fossil_strnicmp(zValue,"fossil-doc",10)!=0 ) return 0; + seenClass = 1; + if( seenTitle ) return 1; + } + if( nAttr==10 && fossil_strnicmp(zAttr,"data-title",10)==0 ){ + /* The text argument to data-title="" will have had any characters that + ** are special to HTML encoded. We need to decode these before turning + ** the text into a title, as the title text will be reencoded later */ + char *zTitle = mprintf("%.*s", nValue, zValue); + int i; + for(i=0; fossil_isspace(zTitle[i]); i++){} + html_to_plaintext(zTitle+i, pTitle, 0); + fossil_free(zTitle); + seenTitle = 1; + if( seenClass ) return 1; + } + } + return seenClass; +} + +/* +** Look for a file named zName in the check-in with RID=vid. Load the content +** of that file into pContent and return the RID for the file. Or return 0 +** if the file is not found or could not be loaded. +*/ +int doc_load_content(int vid, const char *zName, Blob *pContent){ + int writable; + int rid; /* The RID of the file being loaded */ + if( db_is_protected(PROTECT_READONLY) + || !db_is_writeable("repository") + ){ + writable = 0; + }else{ + writable = 1; + } + if( writable ){ + db_end_transaction(0); + db_begin_write(); + } + if( !db_table_exists("repository", "vcache") || !writable ){ + db_multi_exec( + "CREATE %s TABLE IF NOT EXISTS vcache(\n" + " vid INTEGER, -- check-in ID\n" + " fname TEXT, -- filename\n" + " rid INTEGER, -- artifact ID\n" + " PRIMARY KEY(vid,fname)\n" + ") WITHOUT ROWID", writable ? "" : "TEMPORARY" + ); + } + if( !db_exists("SELECT 1 FROM vcache WHERE vid=%d", vid) ){ + db_multi_exec( + "DELETE FROM vcache;\n" + "CREATE VIRTUAL TABLE IF NOT EXISTS temp.foci USING files_of_checkin;\n" + "INSERT INTO vcache(vid,fname,rid)" + " SELECT checkinID, filename, blob.rid FROM foci, blob" + " WHERE blob.uuid=foci.uuid" + " AND foci.checkinID=%d;", + vid + ); + } + rid = db_int(0, "SELECT rid FROM vcache" + " WHERE vid=%d AND fname=%Q", vid, zName); + if( rid && content_get(rid, pContent)==0 ){ + rid = 0; + } + return rid; +} + +/* +** Check to verify that z[i] is contained within HTML markup. +** +** This works by looking backwards in the string for the most recent +** '<' or '>' character. If a '<' is found first, then we assume that +** z[i] is within markup. If a '>' is seen or neither character is seen, +** then z[i] is not within markup. +*/ +static int isWithinHtmlMarkup(const char *z, int i){ + while( i>=0 && z[i]!='>' && z[i]!='<' ){ i--; } + return z[i]=='<'; +} + +/* +** Check to see if z[i] is contained within an href='...' of markup. +*/ +static int isWithinHref(const char *z, int i){ + while( i>5 + && !fossil_isspace(z[i]) + && z[i]!='\'' && z[i]!='"' + && z[i]!='>' + ){ i--; } + if( i<=6 ) return 0; + if( z[i]!='\'' && z[i]!='\"' ) return 0; + if( strncmp(&z[i-5],"href=",5)!=0 ) return 0; + if( !fossil_isspace(z[i-6]) ) return 0; + return 1; +} + +/* +** Transfer content to the output. During the transfer, when text of +** the following form is seen: +** +** href="$ROOT/..." +** action="$ROOT/..." +** href=".../doc/$CURRENT/..." +** +** Convert $ROOT to the root URI of the repository, and $CURRENT to the +** version number of the /doc/ document currently being displayed (if any). +** Allow ' in place of " and any case for href or action. +** +** Efforts are made to limit this translation to cases where the text is +** fully contained with an HTML markup element. +*/ +void convert_href_and_output(Blob *pIn){ + int i, base; + int n = blob_size(pIn); + char *z = blob_buffer(pIn); + for(base=0, i=7; i<n; i++){ + if( z[i]=='$' + && strncmp(&z[i],"$ROOT/", 6)==0 + && (z[i-1]=='\'' || z[i-1]=='"') + && i-base>=9 + && ((fossil_strnicmp(&z[i-6],"href=",5)==0 && fossil_isspace(z[i-7])) || + (fossil_strnicmp(&z[i-8],"action=",7)==0 && fossil_isspace(z[i-9])) ) + && isWithinHtmlMarkup(z, i-6) + ){ + blob_append(cgi_output_blob(), &z[base], i-base); + blob_appendf(cgi_output_blob(), "%R"); + base = i+5; + }else + if( z[i]=='$' + && strncmp(&z[i-5],"/doc/$CURRENT/", 11)==0 + && isWithinHref(z,i-5) + && isWithinHtmlMarkup(z, i-5) + && strncmp(g.zPath, "doc/",4)==0 + ){ + int j; + for(j=7; g.zPath[j] && g.zPath[j]!='/'; j++){} + blob_append(cgi_output_blob(), &z[base], i-base); + blob_appendf(cgi_output_blob(), "%.*s", j-4, g.zPath+4); + base = i+8; + } + } + blob_append(cgi_output_blob(), &z[base], i-base); +} + +/* +** Render a document as the reply to the HTTP request. The body +** of the document is contained in pBody. The body might be binary. +** The mimetype is in zMimetype. +*/ +void document_render( + Blob *pBody, /* Document content */ + const char *zMime, /* MIME-type */ + const char *zDefaultTitle, /* Default title */ + const char *zFilename /* Name of the file being rendered */ +){ + Blob title; + int isPopup = P("popup")!=0; + blob_init(&title,0,0); + if( fossil_strcmp(zMime, "text/x-fossil-wiki")==0 ){ + Blob tail = BLOB_INITIALIZER; + style_adunit_config(ADUNIT_RIGHT_OK); + if( wiki_find_title(pBody, &title, &tail) ){ + if( !isPopup ) style_header("%s", blob_str(&title)); + wiki_convert(&tail, 0, WIKI_BUTTONS); + }else{ + if( !isPopup ) style_header("%s", zDefaultTitle); + wiki_convert(pBody, 0, WIKI_BUTTONS); + } + if( !isPopup ){ + document_emit_js(); + style_finish_page(); + } + blob_reset(&tail); + }else if( fossil_strcmp(zMime, "text/x-markdown")==0 ){ + Blob tail = BLOB_INITIALIZER; + markdown_to_html(pBody, &title, &tail); + if( !isPopup ){ + if( blob_size(&title)>0 ){ + markdown_dehtmlize_blob(&title); + style_header("%s", blob_str(&title)); + }else{ + style_header("%s", zDefaultTitle); + } + } + convert_href_and_output(&tail); + if( !isPopup ){ + document_emit_js(); + style_finish_page(); + } + blob_reset(&tail); + }else if( fossil_strcmp(zMime, "text/x-org")==0 ){ + Blob tail = BLOB_INITIALIZER; + org_to_html(pBody, &title, &tail); + if( !isPopup ){ + style_header("%s", blob_size(&title) ? blob_str(&title) : zDefaultTitle); + } + convert_href_and_output(&tail); + if( !isPopup ) style_finish_page(); + blob_reset(&tail); + }else if( fossil_strcmp(zMime, "text/plain")==0 ){ + style_header("%s", zDefaultTitle); + @ <blockquote><pre> + @ %h(blob_str(pBody)) + @ </pre></blockquote> + document_emit_js(); + style_finish_page(); + }else if( fossil_strcmp(zMime, "text/html")==0 + && doc_is_embedded_html(pBody, &title) ){ + if( blob_size(&title)==0 ) blob_append(&title,zFilename,-1); + if( !isPopup ) style_header("%s", blob_str(&title)); + convert_href_and_output(pBody); + if( !isPopup ){ + document_emit_js(); + style_finish_page(); + } + }else if( fossil_strcmp(zMime, "text/x-pikchr")==0 ){ + style_adunit_config(ADUNIT_RIGHT_OK); + if( !isPopup ) style_header("%s", zDefaultTitle); + wiki_render_by_mimetype(pBody, zMime); + if( !isPopup ) style_finish_page(); +#ifdef FOSSIL_ENABLE_TH1_DOCS + }else if( Th_AreDocsEnabled() && + fossil_strcmp(zMime, "application/x-th1")==0 ){ + int raw = P("raw")!=0; + if( !raw ){ + Blob tail; + blob_zero(&tail); + if( wiki_find_title(pBody, &title, &tail) ){ + style_header("%s", blob_str(&title)); + Th_Render(blob_str(&tail)); + blob_reset(&tail); + }else{ + style_header("%h", zFilename); + Th_Render(blob_str(pBody)); + } + }else{ + Th_Render(blob_str(pBody)); + } + if( !raw ){ + document_emit_js(); + style_finish_page(); + } +#endif + }else{ + fossil_free(style_csp(1)); + cgi_set_content_type(zMime); + cgi_set_content(pBody); + } +} + + +/* +** WEBPAGE: uv +** WEBPAGE: doc +** +** URLs can be either /doc/CHECKIN/FILE or /uv/FILE. +** +** CHECKIN can be either tag or hash prefix or timestamp identifying a +** particular check-in, or the name of a branch (meaning the most recent +** check-in on that branch) or one of various magic words: +** +** "tip" means the most recent check-in +** +** "ckout" means the current check-out, if the server is run from +** within a check-out, otherwise it is the same as "tip" +** +** "latest" means use the most recent check-in for the document +** regardless of what branch it occurs on. +** +** FILE is the name of a file to delivered up as a webpage. FILE is relative +** to the root of the source tree of the repository. The FILE must +** be a part of CHECKIN, except when CHECKIN=="ckout" when FILE is read +** directly from disk and need not be a managed file. For /uv, FILE +** can also be the hash of the unversioned file. +** +** The "ckout" CHECKIN is intended for development - to provide a mechanism +** for looking at what a file will look like using the /doc webpage after +** it gets checked in. Some commands like "fossil ui", "fossil server", +** and "fossil http" accept an argument "--ckout-alias NAME" when allows +** NAME to be understood as an alias for "ckout". On a site with many +** embedded hyperlinks to /doc/trunk/... one can run with "--ckout-alias trunk" +** to simulate what the pending changes will look like after they are +** checked in. The NAME alias is stored in g.zCkoutAlias. +** +** The file extension is used to decide how to render the file. +** +** If FILE ends in "/" then the names "FILE/index.html", "FILE/index.wiki", +** and "FILE/index.md" are tried in that order. If the binary was compiled +** with TH1 embedded documentation support and the "th1-docs" setting is +** enabled, the name "FILE/index.th1" is also tried. If none of those are +** found, then FILE is completely replaced by "404.md" and tried. If that +** is not found, then a default 404 screen is generated. +** +** If the file's mimetype is "text/x-fossil-wiki" or "text/x-markdown" +** then headers and footers are added. If the document has mimetype +** text/html then headers and footers are usually not added. However, +** if a "text/html" document begins with the following div: +** +** <div class='fossil-doc' data-title='TEXT'> +** +** then headers and footers are supplied. The optional data-title field +** specifies the title of the document in that case. +** +** For fossil-doc documents and for markdown documents, text of the +** form: "href='$ROOT/" or "action='$ROOT" has the $ROOT name expanded +** to the top-level of the repository. +*/ +void doc_page(void){ + const char *zName = 0; /* Argument to the /doc page */ + const char *zOrigName = "?"; /* Original document name */ + const char *zMime; /* Document MIME type */ + char *zCheckin = "tip"; /* The check-in holding the document */ + char *zPathSuffix = ""; /* Text to append to g.zPath */ + int vid = 0; /* Artifact of check-in */ + int rid = 0; /* Artifact of file */ + int i; /* Loop counter */ + Blob filebody; /* Content of the documentation file */ + Blob title; /* Document title */ + int nMiss = (-1); /* Failed attempts to find the document */ + int isUV = g.zPath[0]=='u'; /* True for /uv. False for /doc */ + const char *zDfltTitle; + static const char *const azSuffix[] = { + "index.html", "index.wiki", "index.md", "index.org" +#ifdef FOSSIL_ENABLE_TH1_DOCS + , "index.th1" +#endif + }; + + login_check_credentials(); + if( !g.perm.Read ){ login_needed(g.anon.Read); return; } + style_set_current_feature("doc"); + blob_init(&title, 0, 0); + blob_init(&filebody, 0, 0); + zDfltTitle = isUV ? "" : "Documentation"; + db_begin_transaction(); + while( rid==0 && (++nMiss)<=count(azSuffix) ){ + zName = P("name"); + if( isUV ){ + if( zName==0 ) zName = "index.wiki"; + i = 0; + }else{ + if( zName==0 || zName[0]==0 ) zName = "tip/index.wiki"; + for(i=0; zName[i] && zName[i]!='/'; i++){} + zCheckin = mprintf("%.*s", i, zName); + if( fossil_strcmp(zCheckin,"ckout")==0 && g.localOpen==0 ){ + zCheckin = "tip"; + }else if( fossil_strcmp(zCheckin,"latest")==0 ){ + char *zNewCkin = db_text(0, + "SELECT uuid FROM blob, mlink, event, filename" + " WHERE filename.name=%Q" + " AND mlink.fnid=filename.fnid" + " AND blob.rid=mlink.mid" + " AND event.objid=mlink.mid" + " ORDER BY event.mtime DESC LIMIT 1", + zName + i + 1); + if( zNewCkin ) zCheckin = zNewCkin; + } + } + if( nMiss==count(azSuffix) ){ + zName = "404.md"; + zDfltTitle = "Not Found"; + }else if( zName[i]==0 ){ + assert( nMiss>=0 && nMiss<count(azSuffix) ); + zName = azSuffix[nMiss]; + }else if( !isUV ){ + zName += i; + } + while( zName[0]=='/' ){ zName++; } + if( isUV ){ + zPathSuffix = fossil_strdup(zName); + }else{ + zPathSuffix = mprintf("%s/%s", zCheckin, zName); + } + if( nMiss==0 ) zOrigName = zName; + if( !file_is_simple_pathname(zName, 1) ){ + if( sqlite3_strglob("*/", zName)==0 ){ + assert( nMiss>=0 && nMiss<count(azSuffix) ); + zName = mprintf("%s%s", zName, azSuffix[nMiss]); + if( !file_is_simple_pathname(zName, 1) ){ + goto doc_not_found; + } + }else{ + goto doc_not_found; + } + } + if( isUV ){ + if( db_table_exists("repository","unversioned") ){ + rid = unversioned_content(zName, &filebody); + if( rid==1 ){ + Stmt q; + db_prepare(&q, "SELECT hash, mtime FROM unversioned" + " WHERE name=%Q", zName); + if( db_step(&q)==SQLITE_ROW ){ + etag_check(ETAG_HASH, db_column_text(&q,0)); + etag_last_modified(db_column_int64(&q,1)); + } + db_finalize(&q); + }else if( rid==2 ){ + zName = db_text(zName, + "SELECT name FROM unversioned WHERE hash=%Q", zName); + g.isConst = 1; + } + zDfltTitle = zName; + } + }else if( fossil_strcmp(zCheckin,"ckout")==0 + || fossil_strcmp(zCheckin,g.zCkoutAlias)==0 + ){ + /* Read from the local check-out */ + char *zFullpath; + db_must_be_within_tree(); + zFullpath = mprintf("%s/%s", g.zLocalRoot, zName); + if( file_isfile(zFullpath, RepoFILE) + && blob_read_from_file(&filebody, zFullpath, RepoFILE)>0 ){ + rid = 1; /* Fake RID just to get the loop to end */ + } + fossil_free(zFullpath); + }else{ + vid = symbolic_name_to_rid(zCheckin, "ci"); + rid = vid>0 ? doc_load_content(vid, zName, &filebody) : 0; + } + } + g.zPath = mprintf("%s/%s", g.zPath, zPathSuffix); + if( rid==0 ) goto doc_not_found; + blob_to_utf8_no_bom(&filebody, 0); + + /* The file is now contained in the filebody blob. Deliver the + ** file to the user + */ + zMime = nMiss==0 ? P("mimetype") : 0; + if( zMime==0 ){ + zMime = mimetype_from_name(zName); + } + Th_StoreUnsafe("doc_name", zName); + if( vid ){ + Th_Store("doc_version", db_text(0, "SELECT '[' || substr(uuid,1,10) || ']'" + " FROM blob WHERE rid=%d", vid)); + Th_Store("doc_date", db_text(0, "SELECT datetime(mtime) FROM event" + " WHERE objid=%d AND type='ci'", vid)); + } + cgi_check_for_malice(); + document_render(&filebody, zMime, zDfltTitle, zName); + if( nMiss>=count(azSuffix) ) cgi_set_status(404, "Not Found"); + db_end_transaction(0); + blob_reset(&title); + blob_reset(&filebody); + return; + + /* Jump here when unable to locate the document */ +doc_not_found: + db_end_transaction(0); + if( isUV && P("name")==0 ){ + uvlist_page(); + return; + } + cgi_set_status(404, "Not Found"); + style_header("Not Found"); + @ <p>Document %h(zOrigName) not found + if( fossil_strcmp(zCheckin,"ckout")!=0 ){ + @ in %z(href("%R/tree?ci=%T",zCheckin))%h(zCheckin)</a> + } + style_finish_page(); + blob_reset(&title); + blob_reset(&filebody); + return; +} + +/* +** The default logo. +*/ +static const unsigned char aLogo[] = { + 71, 73, 70, 56, 55, 97, 62, 0, 71, 0, 244, 0, 0, 85, + 129, 149, 95, 136, 155, 99, 139, 157, 106, 144, 162, 113, 150, 166, + 116, 152, 168, 127, 160, 175, 138, 168, 182, 148, 176, 188, 159, 184, + 195, 170, 192, 202, 180, 199, 208, 184, 202, 210, 191, 207, 215, 201, + 215, 221, 212, 223, 228, 223, 231, 235, 226, 227, 226, 226, 234, 237, + 233, 239, 241, 240, 244, 246, 244, 247, 248, 255, 255, 255, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 44, 0, 0, + 0, 0, 62, 0, 71, 0, 0, 5, 255, 96, 100, 141, 100, 105, + 158, 168, 37, 41, 132, 192, 164, 112, 44, 207, 102, 99, 0, 56, + 16, 84, 116, 239, 199, 141, 65, 110, 232, 248, 25, 141, 193, 161, + 82, 113, 108, 202, 32, 55, 229, 210, 73, 61, 41, 164, 88, 102, + 181, 10, 41, 96, 179, 91, 106, 35, 240, 5, 135, 143, 137, 242, + 87, 123, 246, 33, 190, 81, 108, 163, 237, 198, 14, 30, 113, 233, + 131, 78, 115, 72, 11, 115, 87, 101, 19, 124, 51, 66, 74, 8, + 19, 16, 67, 100, 74, 133, 50, 15, 101, 135, 56, 11, 74, 6, + 143, 49, 126, 106, 56, 8, 145, 67, 9, 152, 48, 139, 155, 5, + 22, 13, 74, 115, 161, 41, 147, 101, 13, 130, 57, 132, 170, 40, + 167, 155, 0, 94, 57, 3, 178, 48, 183, 181, 57, 160, 186, 40, + 19, 141, 189, 0, 69, 192, 40, 16, 195, 155, 185, 199, 41, 201, + 189, 191, 205, 193, 188, 131, 210, 49, 175, 88, 209, 214, 38, 19, + 3, 11, 19, 111, 127, 60, 219, 39, 55, 204, 19, 11, 6, 100, + 5, 10, 227, 228, 37, 163, 0, 239, 117, 56, 238, 243, 49, 195, + 177, 247, 48, 158, 56, 251, 50, 216, 254, 197, 56, 128, 107, 158, + 2, 125, 171, 114, 92, 218, 246, 96, 66, 3, 4, 50, 134, 176, + 145, 6, 97, 64, 144, 24, 19, 136, 108, 91, 177, 160, 0, 194, + 19, 253, 0, 216, 107, 214, 224, 192, 129, 5, 16, 83, 255, 244, + 43, 213, 195, 24, 159, 27, 169, 64, 230, 88, 208, 227, 129, 182, + 54, 4, 89, 158, 24, 181, 163, 199, 1, 155, 52, 233, 8, 130, + 176, 83, 24, 128, 137, 50, 18, 32, 48, 48, 114, 11, 173, 137, + 19, 110, 4, 64, 105, 1, 194, 30, 140, 68, 15, 24, 24, 224, + 50, 76, 70, 0, 11, 171, 54, 26, 160, 181, 194, 149, 148, 40, + 174, 148, 122, 64, 180, 208, 161, 17, 207, 112, 164, 1, 128, 96, + 148, 78, 18, 21, 194, 33, 229, 51, 247, 65, 133, 97, 5, 250, + 69, 229, 100, 34, 220, 128, 166, 116, 190, 62, 8, 167, 195, 170, + 47, 163, 0, 130, 90, 152, 11, 160, 173, 170, 27, 154, 26, 91, + 232, 151, 171, 18, 14, 162, 253, 98, 170, 18, 70, 171, 64, 219, + 10, 67, 136, 134, 187, 116, 75, 180, 46, 179, 174, 135, 4, 189, + 229, 231, 78, 40, 10, 62, 226, 164, 172, 64, 240, 167, 170, 10, + 18, 124, 188, 10, 107, 65, 193, 94, 11, 93, 171, 28, 248, 17, + 239, 46, 140, 78, 97, 34, 25, 153, 36, 99, 65, 130, 7, 203, + 183, 168, 51, 34, 136, 25, 140, 10, 6, 16, 28, 255, 145, 241, + 230, 140, 10, 66, 178, 167, 112, 48, 192, 128, 129, 9, 31, 141, + 84, 138, 63, 163, 162, 2, 203, 206, 240, 56, 55, 98, 192, 188, + 15, 185, 50, 160, 6, 0, 125, 62, 33, 214, 195, 33, 5, 24, + 184, 25, 231, 14, 201, 245, 144, 23, 126, 104, 228, 0, 145, 2, + 13, 140, 244, 212, 17, 21, 20, 176, 159, 17, 95, 225, 160, 128, + 16, 1, 32, 224, 142, 32, 227, 125, 87, 64, 0, 16, 54, 129, + 205, 2, 141, 76, 53, 130, 103, 37, 166, 64, 144, 107, 78, 196, + 5, 192, 0, 54, 50, 229, 9, 141, 49, 84, 194, 35, 12, 196, + 153, 48, 192, 137, 57, 84, 24, 7, 87, 159, 249, 240, 215, 143, + 105, 241, 118, 149, 9, 139, 4, 64, 203, 141, 35, 140, 129, 131, + 16, 222, 125, 231, 128, 2, 238, 17, 152, 66, 3, 5, 56, 224, + 159, 103, 16, 76, 25, 75, 5, 11, 164, 215, 96, 9, 14, 16, + 36, 225, 15, 11, 40, 144, 192, 156, 41, 10, 178, 199, 3, 66, + 64, 80, 193, 3, 124, 90, 48, 129, 129, 102, 177, 18, 192, 154, + 49, 84, 240, 208, 92, 22, 149, 96, 39, 9, 31, 74, 17, 94, + 3, 8, 177, 199, 72, 59, 85, 76, 25, 216, 8, 139, 194, 197, + 138, 163, 69, 96, 115, 0, 147, 72, 72, 84, 28, 14, 79, 86, + 233, 230, 23, 113, 26, 160, 128, 3, 10, 58, 129, 103, 14, 159, + 214, 163, 146, 117, 238, 213, 154, 128, 151, 109, 84, 64, 217, 13, + 27, 10, 228, 39, 2, 235, 164, 168, 74, 8, 0, 59, +}; + +/* +** WEBPAGE: logo +** +** Return the logo image. This image is available to anybody who can see +** the login page. It is designed for use in the upper left-hand corner +** of the header. +*/ +void logo_page(void){ + Blob logo; + char *zMime; + + etag_check(ETAG_CONFIG, 0); + zMime = db_get("logo-mimetype", "image/gif"); + blob_zero(&logo); + db_blob(&logo, "SELECT value FROM config WHERE name='logo-image'"); + if( blob_size(&logo)==0 ){ + blob_init(&logo, (char*)aLogo, sizeof(aLogo)); + } + cgi_set_content_type(zMime); + cgi_set_content(&logo); +} + +/* +** The default background image: a 16x16 white GIF +*/ +static const unsigned char aBackground[] = { + 71, 73, 70, 56, 57, 97, 16, 0, 16, 0, + 240, 0, 0, 255, 255, 255, 0, 0, 0, 33, + 254, 4, 119, 105, 115, 104, 0, 44, 0, 0, + 0, 0, 16, 0, 16, 0, 0, 2, 14, 132, + 143, 169, 203, 237, 15, 163, 156, 180, 218, 139, + 179, 62, 5, 0, 59, +}; + + +/* +** WEBPAGE: background +** +** Return the background image. If no background image is defined, a +** built-in 16x16 pixel white GIF is returned. +*/ +void background_page(void){ + Blob bgimg; + char *zMime; + + etag_check(ETAG_CONFIG, 0); + zMime = db_get("background-mimetype", "image/gif"); + blob_zero(&bgimg); + db_blob(&bgimg, "SELECT value FROM config WHERE name='background-image'"); + if( blob_size(&bgimg)==0 ){ + blob_init(&bgimg, (char*)aBackground, sizeof(aBackground)); + } + cgi_set_content_type(zMime); + cgi_set_content(&bgimg); +} + + +/* +** WEBPAGE: favicon.ico +** +** Return the configured "favicon.ico" image. If no "favicon.ico" image +** is defined, the returned image is for the Fossil lizard icon. +** +** The intended use case here is to supply an icon for the "fossil ui" +** command. For a permanent website, the recommended process is for +** the admin to set up a project-specific icon and reference that icon +** in the HTML header using a line like: +** +** <link rel="icon" href="URL-FOR-YOUR-ICON" type="MIMETYPE"/> +** +*/ +void favicon_page(void){ + Blob icon; + char *zMime; + + etag_check(ETAG_CONFIG, 0); + zMime = db_get("icon-mimetype", "image/gif"); + blob_zero(&icon); + db_blob(&icon, "SELECT value FROM config WHERE name='icon-image'"); + if( blob_size(&icon)==0 ){ + blob_init(&icon, (char*)aLogo, sizeof(aLogo)); + } + cgi_set_content_type(zMime); + cgi_set_content(&icon); +} + +/* +** WEBPAGE: docsrch +** +** Search for documents that match a user-supplied full-text search pattern. +** If no pattern is specified (by the s= query parameter) then the user +** is prompted to enter a search string. +** +** Query parameters: +** +** s=PATTERN Search for PATTERN +*/ +void doc_search_page(void){ + const int isSearch = P("s")!=0; + login_check_credentials(); + style_header("Document Search%s", isSearch ? " Results" : ""); + cgi_check_for_malice(); + search_screen(SRCH_DOC, 0); + style_finish_page(); +} |
