From 4f49b16079b27d7ce633619e4cddfdf2bffbc952 Mon Sep 17 00:00:00 2001 From: cashmere Date: Fri, 4 Sep 2026 09:05:24 +0200 Subject: initial commit --- src/doc.c | 1277 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1277 insertions(+) create mode 100644 src/doc.c (limited to 'src/doc.c') 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 + +/* +** 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=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; zwindow.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 %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"); + @

The Fossil /doc page uses filename + @ suffixes and the following tables to guess at the appropriate mimetype + @ for each document. Mimetypes may be customized and overridden using + @ the mimetypes config setting.

+ zCustomList = db_get("mimetypes",0); + if( zCustomList!=0 ){ + Blob list, entry, key, val; + @

Repository-specific mimetypes

+ @

The following extension-to-mimetype mappings are defined via + @ the mimetypes setting.

+ @ + @ + @ + @ + 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++; + @ + nCustomEntries++; + } + fossil_free(zCustomList); + if( nCustomEntries==0 ){ + /* This can happen if the option is set to an empty/space-only + ** value. */ + @ + } + @
SuffixMimetype + @
%h(zKey)%h(blob_str(&val))
none
+ } + @

Default built-in mimetypes

+ if(nCustomEntries>0){ + @

Entries starting with an exclamation mark ! + @ are overwritten by repository-specific settings.

+ } + @ + @ + @ + @ + for(i=0; i0 && + mimetype_from_name_custom(aMime[i].zSuffix)!=0){ + zFlag = "! "; + } + @ + } + @
SuffixMimetype + @
%s(zFlag)%h(aMime[i].zSuffix)%h(aMime[i].zMimetype)
+ 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
element. +** If that
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,"' ) 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=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); + @
+    @ %h(blob_str(pBody))
+    @ 
+ 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: +** +**
+** +** 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=0 && nMiss0 ){ + 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"); + @

Document %h(zOrigName) not found + if( fossil_strcmp(zCheckin,"ckout")!=0 ){ + @ in %z(href("%R/tree?ci=%T",zCheckin))%h(zCheckin) + } + 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: +** +** +** +*/ +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(); +} -- cgit v1.3.1