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/th_main.c | |
| download | relic-master.tar.gz | |
Diffstat (limited to 'src/th_main.c')
| -rw-r--r-- | src/th_main.c | 3323 |
1 files changed, 3323 insertions, 0 deletions
diff --git a/src/th_main.c b/src/th_main.c new file mode 100644 index 0000000..b22054d --- /dev/null +++ b/src/th_main.c @@ -0,0 +1,3323 @@ +/* +** Copyright (c) 2008 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 an interface between the TH scripting language +** (an independent project) and fossil. +*/ +#include "config.h" +#include "th_main.h" +#include "sqlite3.h" + +#if INTERFACE +/* +** Flag parameters to the Th_FossilInit() routine used to control the +** interpreter creation and initialization process. +*/ +#define TH_INIT_NONE ((u32)0x00000000) /* No flags. */ +#define TH_INIT_NEED_CONFIG ((u32)0x00000001) /* Open configuration first? */ +#define TH_INIT_FORCE_TCL ((u32)0x00000002) /* Force Tcl to be enabled? */ +#define TH_INIT_FORCE_RESET ((u32)0x00000004) /* Force TH1 commands re-added? */ +#define TH_INIT_FORCE_SETUP ((u32)0x00000008) /* Force eval of setup script? */ +#define TH_INIT_NO_REPO ((u32)0x00000010) /* Skip opening repository. */ +#define TH_INIT_MASK ((u32)0x0000001F) /* All possible init flags. */ + +/* +** Useful and/or "well-known" combinations of flag values. +*/ +#define TH_INIT_DEFAULT (TH_INIT_NONE) /* Default flags. */ +#define TH_INIT_HOOK (TH_INIT_NEED_CONFIG | TH_INIT_FORCE_SETUP) +#define TH_INIT_FORBID_MASK (TH_INIT_FORCE_TCL) /* Illegal from a script. */ +#endif + +/* +** Flags set by functions in this file to keep track of integration state +** information. These flags should not be used outside of this file. +*/ +#define TH_STATE_CONFIG ((u32)0x00000200) /* We opened the config. */ +#define TH_STATE_REPOSITORY ((u32)0x00000400) /* We opened the repository. */ +#define TH_STATE_MASK ((u32)0x00000600) /* All possible state flags. */ + +#ifdef FOSSIL_ENABLE_TH1_HOOKS +/* +** These are the "well-known" TH1 error messages that occur when no hook is +** registered to be called prior to executing a command or processing a web +** page, respectively. If one of these errors is seen, it will not be sent +** or displayed to the remote user or local interactive user, respectively. +*/ +#define NO_COMMAND_HOOK_ERROR "no such command: command_hook" +#define NO_WEBPAGE_HOOK_ERROR "no such command: webpage_hook" +#endif + +/* +** These macros are used within this file to detect if the repository and +** configuration ("user") database are currently open. +*/ +#define Th_IsRepositoryOpen() (g.repositoryOpen) +#define Th_IsConfigOpen() (g.zConfigDbName!=0) + +/* +** When memory debugging is enabled, use our custom memory allocator. +*/ +#if defined(TH_MEMDEBUG) +/* +** Global variable counting the number of outstanding calls to malloc() +** made by the th1 implementation. This is used to catch memory leaks +** in the interpreter. Obviously, it also means th1 is not thread-safe. +*/ +static int nOutstandingMalloc = 0; + +/* +** Implementations of malloc() and free() to pass to the interpreter. +*/ +static void *xMalloc(unsigned int n){ + void *p = fossil_malloc(n); + if( p ){ + nOutstandingMalloc++; + } + return p; +} +static void xFree(void *p){ + if( p ){ + nOutstandingMalloc--; + } + free(p); +} +static Th_Vtab vtab = { xMalloc, xFree }; + +/* +** Returns the number of outstanding TH1 memory allocations. +*/ +int Th_GetOutstandingMalloc(){ + return nOutstandingMalloc; +} +#endif + +/* +** Generate a TH1 trace message if debugging is enabled. +*/ +void Th_Trace(const char *zFormat, ...){ + va_list ap; + va_start(ap, zFormat); + blob_vappendf(&g.thLog, zFormat, ap); + va_end(ap); +} + +/* +** Forces input and output to be done via the CGI subsystem. +*/ +void Th_ForceCgi(int fullHttpReply){ + g.httpOut = stdout; + g.httpIn = stdin; + fossil_binary_mode(g.httpOut); + fossil_binary_mode(g.httpIn); + g.cgiOutput = 1; + g.fullHttpReply = fullHttpReply; +} + +/* +** Checks if the TH1 trace log needs to be enabled. If so, prepares +** it for use. +*/ +void Th_InitTraceLog(){ + g.thTrace = find_option("th-trace", 0, 0)!=0; + if( g.thTrace ){ + g.fAnyTrace = 1; + blob_zero(&g.thLog); + } +} + +/* +** Prints the entire contents of the TH1 trace log to the standard +** output channel. +*/ +void Th_PrintTraceLog(){ + if( g.thTrace ){ + fossil_print("\n------------------ BEGIN TRACE LOG ------------------\n"); + fossil_print("%s", blob_str(&g.thLog)); + fossil_print("\n------------------- END TRACE LOG -------------------\n"); + } +} + +/* +** - adapted from ls_cmd_rev in checkin.c +** - adapted commands/error handling for usage within th1 +** - interface adapted to allow result creation as TH1 List +** +** Takes a check-in identifier in zRev and an optiona glob pattern in zGLOB +** as parameter returns a TH list in pzList,pnList with filenames matching +** glob pattern with the checking +*/ +static void dir_cmd_rev( + Th_Interp *interp, + char **pzList, + int *pnList, + const char *zRev, /* Revision string given */ + const char *zGlob, /* Glob pattern given */ + int bDetails +){ + Stmt q; + char *zOrderBy = "pathname COLLATE nocase"; + int rid; + + rid = th1_name_to_typed_rid(interp, zRev, "ci"); + compute_fileage(rid, zGlob); + db_prepare(&q, + "SELECT datetime(fileage.mtime, toLocal()), fileage.pathname,\n" + " blob.size\n" + " FROM fileage, blob\n" + " WHERE blob.rid=fileage.fid \n" + " ORDER BY %s;", zOrderBy /*safe-for-%s*/ + ); + while( db_step(&q)==SQLITE_ROW ){ + const char *zFile = db_column_text(&q, 1); + if( bDetails ){ + const char *zTime = db_column_text(&q, 0); + int size = db_column_int(&q, 2); + char zSize[50]; + char *zSubList = 0; + int nSubList = 0; + sqlite3_snprintf(sizeof(zSize), zSize, "%d", size); + Th_ListAppend(interp, &zSubList, &nSubList, zFile, -1); + Th_ListAppend(interp, &zSubList, &nSubList, zSize, -1); + Th_ListAppend(interp, &zSubList, &nSubList, zTime, -1); + Th_ListAppend(interp, pzList, pnList, zSubList, -1); + Th_Free(interp, zSubList); + }else{ + Th_ListAppend(interp, pzList, pnList, zFile, -1); + } + } + db_finalize(&q); +} + +/* +** TH1 command: dir CHECKIN ?GLOB? ?DETAILS? +** +** Returns a list containing all files in CHECKIN. If GLOB is given only +** the files matching the pattern GLOB within CHECKIN will be returned. +** If DETAILS is non-zero, the result will be a list-of-lists, with each +** element containing at least three elements: the file name, the file +** size (in bytes), and the file last modification time (relative to the +** time zone configured for the repository). +*/ +static int dirCmd( + Th_Interp *interp, + void *ctx, + int argc, + const char **argv, + int *argl +){ + const char *zGlob = 0; + int bDetails = 0; + + if( argc<2 || argc>4 ){ + return Th_WrongNumArgs(interp, "dir CHECKIN ?GLOB? ?DETAILS?"); + } + if( argc>=3 ){ + zGlob = argv[2]; + } + if( argc>=4 && Th_ToInt(interp, argv[3], argl[3], &bDetails) ){ + return TH_ERROR; + } + if( Th_IsRepositoryOpen() ){ + char *zList = 0; + int nList = 0; + dir_cmd_rev(interp, &zList, &nList, argv[1], zGlob, bDetails); + Th_SetResult(interp, zList, nList); + Th_Free(interp, zList); + return TH_OK; + }else{ + Th_SetResult(interp, "repository unavailable", -1); + return TH_ERROR; + } +} + +/* +** TH1 command: httpize STRING +** +** Escape all characters of STRING which have special meaning in URI +** components. Return a new string result. +*/ +static int httpizeCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + char *zOut; + if( argc!=2 ){ + return Th_WrongNumArgs(interp, "httpize STRING"); + } + zOut = httpize((char*)argv[1], TH1_LEN(argl[1])); + Th_SetResult(interp, zOut, -1); + free(zOut); + return TH_OK; +} + +/* +** True if output is enabled. False if disabled. +*/ +static int enableOutput = 1; + +/* +** TH1 command: enable_output BOOLEAN +** +** Enable or disable the puts, wiki, combobox and copybtn commands. +*/ +static int enableOutputCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + int rc; + if( argc<2 || argc>3 ){ + return Th_WrongNumArgs(interp, "enable_output [LABEL] BOOLEAN"); + } + rc = Th_ToInt(interp, argv[argc-1], argl[argc-1], &enableOutput); + if( g.thTrace ){ + Th_Trace("enable_output {%.*s} -> %d<br>\n", + TH1_LEN(argl[1]),argv[1],enableOutput); + } + return rc; +} + +/* +** Returns a name for a TH1 return code. +*/ +const char *Th_ReturnCodeName(int rc, int nullIfOk){ + static char zRc[32]; + + switch( rc ){ + case TH_OK: return nullIfOk ? 0 : "TH_OK"; + case TH_ERROR: return "TH_ERROR"; + case TH_BREAK: return "TH_BREAK"; + case TH_RETURN: return "TH_RETURN"; + case TH_CONTINUE: return "TH_CONTINUE"; + case TH_RETURN2: return "TH_RETURN2"; + default: { + sqlite3_snprintf(sizeof(zRc), zRc, "TH1 return code %d", rc); + } + } + return zRc; +} + +/* See Th_SetOutputBlob() */ +static Blob * pThOut = 0; +/* +** Sets the th1-internal output-redirection blob and returns the +** previous value. That blob is used by certain output-generation +** routines to emit its output. It returns the previous value so that +** a routine can temporarily replace the buffer with its own and +** restore it when it's done. +*/ +Blob * Th_SetOutputBlob(Blob * pOut){ + Blob * tmp = pThOut; + pThOut = pOut; + return tmp; +} + +/* +** Send text to the appropriate output: If pOut is not NULL, it is +** appended there, else to the console or to the CGI reply buffer. +** Escape all characters with special meaning to HTML if the encode +** parameter is true. +** +** If pOut is NULL and the global pThOut is not then that blob +** is used for output. +*/ +static void sendText(Blob *pOut, const char *z, int n, int encode){ + if(0==pOut && pThOut!=0){ + pOut = pThOut; + } + if( enableOutput && n ){ + if( n<0 ){ + n = strlen(z); + }else{ + n = TH1_LEN(n); + } + if( encode ){ + z = htmlize(z, n); + n = strlen(z); + } + if(pOut!=0){ + blob_append(pOut, z, n); + }else if( g.cgiOutput ){ + cgi_append_content(z, n); + }else{ + fwrite(z, 1, n, stdout); + fflush(stdout); + } + if( encode ) free((char*)z); + } +} + +/* +** error-reporting counterpart of sendText(). +*/ +static void sendError(Blob * pOut, const char *z, int n, int forceCgi){ + int savedEnable = enableOutput; + enableOutput = 1; + if( forceCgi || g.cgiOutput ){ + sendText(pOut, "<hr><p class=\"thmainError\">", -1, 0); + } + sendText(pOut,"ERROR: ", -1, 0); + sendText(pOut,(char*)z, n, 1); + sendText(pOut,forceCgi || g.cgiOutput ? "</p>" : "\n", -1, 0); + enableOutput = savedEnable; +} + +/* +** Convert name to an rid. This function was copied from name_to_typed_rid() +** in name.c; however, it has been modified to report TH1 script errors instead +** of "fatal errors". +*/ +int th1_name_to_typed_rid( + Th_Interp *interp, + const char *zName, + const char *zType +){ + int rid; + + if( zName==0 || zName[0]==0 ) return 0; + rid = symbolic_name_to_rid(zName, zType); + if( rid<0 ){ + Th_SetResult(interp, "ambiguous name", -1); + }else if( rid==0 ){ + Th_SetResult(interp, "name not found", -1); + } + return rid; +} + +/* +** Attempt to lookup the specified check-in and file name into an rid. +** This function was copied from artifact_from_ci_and_filename() in +** info.c; however, it has been modified to report TH1 script errors +** instead of "fatal errors". +*/ +int th1_artifact_from_ci_and_filename( + Th_Interp *interp, + const char *zCI, + const char *zFilename +){ + int cirid; + Blob err; + Manifest *pManifest; + ManifestFile *pFile; + + if( zCI==0 ){ + Th_SetResult(interp, "invalid check-in", -1); + return 0; + } + if( zFilename==0 ){ + Th_SetResult(interp, "invalid file name", -1); + return 0; + } + cirid = th1_name_to_typed_rid(interp, zCI, "*"); + blob_zero(&err); + pManifest = manifest_get(cirid, CFTYPE_MANIFEST, &err); + if( pManifest==0 ){ + if( blob_size(&err)>0 ){ + Th_SetResult(interp, blob_str(&err), blob_size(&err)); + }else{ + Th_SetResult(interp, "manifest not found", -1); + } + blob_reset(&err); + return 0; + } + blob_reset(&err); + manifest_file_rewind(pManifest); + while( (pFile = manifest_file_next(pManifest,0))!=0 ){ + if( fossil_strcmp(zFilename, pFile->zName)==0 ){ + int rid = db_int(0, "SELECT rid FROM blob WHERE uuid=%Q", pFile->zUuid); + manifest_destroy(pManifest); + return rid; + } + } + Th_SetResult(interp, "file name not found in manifest", -1); + return 0; +} + +/* +** TH1 command: nonce +** +** Returns the value of the cryptographic nonce for the request being +** processed. +*/ +static int nonceCmd( + Th_Interp *interp, + void *pConvert, + int argc, + const char **argv, + int *argl +){ + if( argc!=1 ){ + return Th_WrongNumArgs(interp, "nonce"); + } + Th_SetResult(interp, style_nonce(), -1); + return TH_OK; +} + +/* +** TH1 command: puts STRING +** TH1 command: html STRING +** +** Output STRING escaped for HTML (puts) or unchanged (html). +*/ +static int putsCmd( + Th_Interp *interp, + void *pConvert, + int argc, + const char **argv, + int *argl +){ + int encode = *(unsigned int*)pConvert; + int n; + if( argc!=2 ){ + return Th_WrongNumArgs(interp, "puts STRING"); + } + n = argl[1]; + if( encode==0 && n>0 && TH1_TAINTED(n) ){ + if( Th_ReportTaint(interp, "output string", argv[1], n) ){ + return TH_ERROR; + } + } + sendText(0,(char*)argv[1], TH1_LEN(n), encode); + return TH_OK; +} + +/* +** TH1 command: redirect URL ?withMethod? +** +** Issues an HTTP redirect to the specified URL and then exits the process. +** By default, an HTTP status code of 302 is used. If the optional withMethod +** argument is present and non-zero, an HTTP status code of 307 is used, which +** should force the user agent to preserve the original method for the request +** (e.g. GET, POST) instead of (possibly) forcing the user agent to change the +** method to GET. +*/ +static int redirectCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + int withMethod = 0; + if( argc!=2 && argc!=3 ){ + return Th_WrongNumArgs(interp, "redirect URL ?withMethod?"); + } + if( argc==3 ){ + if( Th_ToInt(interp, argv[2], argl[2], &withMethod) ){ + return TH_ERROR; + } + } + if( TH1_TAINTED(argl[1]) + && Th_ReportTaint(interp,"redirect URL",argv[1],argl[1]) + ){ + return TH_ERROR; + } + if( withMethod ){ + cgi_redirect_with_method(argv[1]); + }else{ + cgi_redirect(argv[1]); + } + Th_SetResult(interp, argv[1], argl[1]); /* NOT REACHED */ + return TH_OK; +} + +/* +** TH1 command: insertCsrf +** +** While rendering a form, call this command to add the Anti-CSRF token +** as a hidden element of the form. +*/ +static int insertCsrfCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + if( argc!=1 ){ + return Th_WrongNumArgs(interp, "insertCsrf"); + } + login_insert_csrf_secret(); + return TH_OK; +} + +/* +** TH1 command: verifyCsrf +** +** Before using the results of a form, first call this command to verify +** that this Anti-CSRF token is present and is valid. If the Anti-CSRF token +** is missing or is incorrect, that indicates a cross-site scripting attack. +** If the event of an attack is detected, an error message is generated and +** all further processing is aborted. +*/ +static int verifyCsrfCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + if( argc!=1 ){ + return Th_WrongNumArgs(interp, "verifyCsrf"); + } + if( !cgi_csrf_safe(2) ){ + fossil_fatal("possible CSRF attack"); + } + return TH_OK; +} + +/* +** TH1 command: verifyLogin +** +** Returns non-zero if the specified user name and password represent a +** valid login for the repository. +*/ +static int verifyLoginCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + const char *zUser; + const char *zPass; + int uid; + if( argc!=3 ){ + return Th_WrongNumArgs(interp, "verifyLogin userName password"); + } + zUser = argv[1]; + zPass = argv[2]; + uid = login_search_uid(&zUser, zPass); + Th_SetResultInt(interp, uid!=0); + if( uid==0 ) sqlite3_sleep(100); + return TH_OK; +} + +/* +** TH1 command: markdown STRING +** +** Renders the input string as markdown. The result is a two-element list. +** The first element is the text-only title string. The second element +** contains the body, rendered as HTML. +*/ +static int markdownCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + Blob src, title, body; + char *zValue = 0; + int nValue = 0; + if( argc!=2 ){ + return Th_WrongNumArgs(interp, "markdown STRING"); + } + blob_zero(&src); + blob_init(&src, (char*)argv[1], TH1_LEN(argl[1])); + blob_zero(&title); blob_zero(&body); + markdown_to_html(&src, &title, &body); + Th_ListAppend(interp, &zValue, &nValue, blob_str(&title), blob_size(&title)); + Th_ListAppend(interp, &zValue, &nValue, blob_str(&body), blob_size(&body)); + Th_SetResult(interp, zValue, nValue); + Th_Free(interp, zValue); + return TH_OK; +} + +/* +** TH1 command: decorate STRING +** TH1 command: wiki STRING +** +** Render the input string as wiki. For the decorate command, only links +** are handled. +*/ +static int wikiCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + int flags = WIKI_INLINE | WIKI_NOBADLINKS | *(unsigned int*)p; + if( argc!=2 ){ + return Th_WrongNumArgs(interp, "wiki STRING"); + } + if( enableOutput ){ + Blob src; + blob_init(&src, (char*)argv[1], TH1_LEN(argl[1])); + wiki_convert(&src, 0, flags); + blob_reset(&src); + } + return TH_OK; +} + +/* +** TH1 command: wiki_assoc STRING STRING +** +** Render an associated wiki page. The first string is the namespace +** (e.g. "checkin", "branch", "ticket"). The second is the ID of the +** associated object. See wiki_render_associated(). +*/ +static int wikiAssocCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + if( argc!=3 ){ + return Th_WrongNumArgs(interp, "wiki_assoc STRING STRING"); + } + wiki_render_associated((char*)argv[1], (char*)argv[2], WIKIASSOC_FULL_TITLE); + return TH_OK; +} + +/* +** TH1 command: htmlize STRING +** +** Escape all characters of STRING which have special meaning in HTML. +** Return a new string result. +*/ +static int htmlizeCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + char *zOut; + if( argc!=2 ){ + return Th_WrongNumArgs(interp, "htmlize STRING"); + } + zOut = htmlize((char*)argv[1], TH1_LEN(argl[1])); + Th_SetResult(interp, zOut, -1); + free(zOut); + return TH_OK; +} + +/* +** TH1 command: encode64 STRING +** +** Encode the specified string using Base64 and return the result. +*/ +static int encode64Cmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + char *zOut; + if( argc!=2 ){ + return Th_WrongNumArgs(interp, "encode64 STRING"); + } + zOut = encode64((char*)argv[1], TH1_LEN(argl[1])); + Th_SetResult(interp, zOut, -1); + free(zOut); + return TH_OK; +} + +/* +** TH1 command: date +** +** Return a string which is the current time and date. If the +** -local option is used, the date appears using localtime instead +** of UTC. +*/ +static int dateCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + char *zOut; + if( argc>=2 && TH1_LEN(argl[1])==6 && memcmp(argv[1],"-local",6)==0 ){ + zOut = db_text("??", "SELECT datetime('now',toLocal())"); + }else{ + zOut = db_text("??", "SELECT datetime('now')"); + } + Th_SetResult(interp, zOut, -1); + free(zOut); + return TH_OK; +} + +/* +** TH1 command: hascap STRING... +** TH1 command: anoncap STRING... +** +** Return true if the current user (hascap) or if the anonymous user +** (anoncap) has all of the capabilities listed in STRING. +*/ +static int hascapCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + int rc = 1, i; + char *zCapList = 0; + int nCapList = 0; + if( argc<2 ){ + return Th_WrongNumArgs(interp, "hascap STRING ..."); + } + for(i=1; rc==1 && i<argc; i++){ + if( g.thTrace ){ + Th_ListAppend(interp, &zCapList, &nCapList, argv[i], TH1_LEN(argl[i])); + } + rc = login_has_capability((char*)argv[i],TH1_LEN(argl[i]),*(int*)p); + } + if( g.thTrace ){ + Th_Trace("[%s %#h] => %d<br>\n", argv[0], nCapList, zCapList, rc); + Th_Free(interp, zCapList); + } + Th_SetResultInt(interp, rc); + return TH_OK; +} + +/* +** TH1 command: capexpr CAPABILITY-EXPR +** +** Nmemonic: "CAPability EXPRression" +** +** The capability expression is a list. Each term of the list is a cluster +** of capability letters. The overall expression is true if any one term +** is true. A single term is true if all letters within that term are true. +** Or, if the term begins with "!", then the term is true if none of the +** terms or true. Or, if the term begins with "@" then the term is true +** if all of the capability letters in that term are available to the +** "anonymous" user. Or, if the term is "*" then it is always true. +** +** Examples: +** +** capexpr {j o r} True if any one of j, o, or r are available +** capexpr {oh} True if both o and h are available +** capexpr {@2 @3 4 5 6} 2 or 3 available for anonymous or one of +** 4, 5 or 6 is available for the user +*/ +int capexprCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + char **azCap; + int *anCap; + int nCap; + int rc; + int i; + + if( argc!=2 ){ + return Th_WrongNumArgs(interp, "capexpr EXPR"); + } + rc = Th_SplitList(interp, argv[1], TH1_LEN(argl[1]), &azCap, &anCap, &nCap); + if( rc ) return rc; + rc = 0; + for(i=0; i<nCap; i++){ + if( azCap[i][0]=='!' ){ + rc = !login_has_capability(azCap[i]+1, anCap[i]-1, 0); + }else if( azCap[i][0]=='@' ){ + rc = login_has_capability(azCap[i]+1, anCap[i]-1, LOGIN_ANON); + }else if( azCap[i][0]=='*' ){ + rc = 1; + }else{ + rc = login_has_capability(azCap[i], anCap[i], 0); + } + if( rc ) break; + } + Th_Free(interp, azCap); + Th_SetResultInt(interp, rc); + return TH_OK; +} + + +/* +** TH1 command: searchable STRING... +** +** Return true if searching in any of the document classes identified +** by STRING is enabled for the repository and user has the necessary +** capabilities to perform the search. +** +** Document classes: +** +** c Check-in comments +** d Embedded documentation +** t Tickets +** w Wiki +** +** To be clear, only one of the document classes identified by each STRING +** needs to be searchable in order for that argument to be true. But +** all arguments must be true for this routine to return true. Hence, to +** see if ALL document classes are searchable: +** +** if {[searchable c d t w]} {...} +** +** But to see if ANY document class is searchable: +** +** if {[searchable cdtw]} {...} +** +** This command is useful for enabling or disabling a "Search" entry +** on the menu bar. +*/ +static int searchableCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + int rc = 1, i, j; + unsigned int searchCap = search_restrict(SRCH_ALL); + if( argc<2 ){ + return Th_WrongNumArgs(interp, "hascap STRING ..."); + } + for(i=1; i<argc && rc; i++){ + int match = 0; + int nn = TH1_LEN(argl[i]); + for(j=0; j<nn; j++){ + switch( argv[i][j] ){ + case 'c': match |= searchCap & SRCH_CKIN; break; + case 'd': match |= searchCap & SRCH_DOC; break; + case 't': match |= searchCap & SRCH_TKT; break; + case 'w': match |= searchCap & SRCH_WIKI; break; + } + } + if( !match ) rc = 0; + } + if( g.thTrace ){ + Th_Trace("[searchable %#h] => %d<br>\n", TH1_LEN(argl[1]), argv[1], rc); + } + Th_SetResultInt(interp, rc); + return TH_OK; +} + +/* +** TH1 command: hasfeature STRING +** +** Return true if the fossil binary has the given compile-time feature +** enabled. The set of features includes: +** +** "ssl" = FOSSIL_ENABLE_SSL +** "legacyMvRm" = FOSSIL_ENABLE_LEGACY_MV_RM +** "execRelPaths" = FOSSIL_ENABLE_EXEC_REL_PATHS +** "th1Docs" = FOSSIL_ENABLE_TH1_DOCS +** "th1Hooks" = FOSSIL_ENABLE_TH1_HOOKS +** "tcl" = FOSSIL_ENABLE_TCL +** "useTclStubs" = USE_TCL_STUBS +** "tclStubs" = FOSSIL_ENABLE_TCL_STUBS +** "tclPrivateStubs" = FOSSIL_ENABLE_TCL_PRIVATE_STUBS +** "json" = FOSSIL_ENABLE_JSON +** "markdown" = FOSSIL_ENABLE_MARKDOWN +** "unicodeCmdLine" = !BROKEN_MINGW_CMDLINE +** "dynamicBuild" = FOSSIL_DYNAMIC_BUILD +** "mman" = USE_MMAN_H +** "see" = USE_SEE +** +** Specifying an unknown feature will return a value of false, it will not +** raise a script error. +*/ +static int hasfeatureCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + int rc = 0; + const char *zArg; + if( argc!=2 ){ + return Th_WrongNumArgs(interp, "hasfeature STRING"); + } + zArg = (const char *)argv[1]; + if(NULL==zArg){ + /* placeholder for following ifdefs... */ + } +#if defined(FOSSIL_ENABLE_SSL) + else if( 0 == fossil_strnicmp( zArg, "ssl\0", 4 ) ){ + rc = 1; + } +#endif + else if( 0 == fossil_strnicmp( zArg, "legacyMvRm\0", 11 ) ){ + rc = 1; + } +#if defined(FOSSIL_ENABLE_EXEC_REL_PATHS) + else if( 0 == fossil_strnicmp( zArg, "execRelPaths\0", 13 ) ){ + rc = 1; + } +#endif +#if defined(FOSSIL_ENABLE_TH1_DOCS) + else if( 0 == fossil_strnicmp( zArg, "th1Docs\0", 8 ) ){ + rc = 1; + } +#endif +#if defined(FOSSIL_ENABLE_TH1_HOOKS) + else if( 0 == fossil_strnicmp( zArg, "th1Hooks\0", 9 ) ){ + rc = 1; + } +#endif +#if defined(FOSSIL_ENABLE_TCL) + else if( 0 == fossil_strnicmp( zArg, "tcl\0", 4 ) ){ + rc = 1; + } +#endif +#if defined(USE_TCL_STUBS) + else if( 0 == fossil_strnicmp( zArg, "useTclStubs\0", 12 ) ){ + rc = 1; + } +#endif +#if defined(FOSSIL_ENABLE_TCL_STUBS) + else if( 0 == fossil_strnicmp( zArg, "tclStubs\0", 9 ) ){ + rc = 1; + } +#endif +#if defined(FOSSIL_ENABLE_TCL_PRIVATE_STUBS) + else if( 0 == fossil_strnicmp( zArg, "tclPrivateStubs\0", 16 ) ){ + rc = 1; + } +#endif +#if defined(FOSSIL_ENABLE_JSON) + else if( 0 == fossil_strnicmp( zArg, "json\0", 5 ) ){ + rc = 1; + } +#endif +#if !defined(BROKEN_MINGW_CMDLINE) + else if( 0 == fossil_strnicmp( zArg, "unicodeCmdLine\0", 15 ) ){ + rc = 1; + } +#endif +#if defined(FOSSIL_DYNAMIC_BUILD) + else if( 0 == fossil_strnicmp( zArg, "dynamicBuild\0", 13 ) ){ + rc = 1; + } +#endif +#if defined(USE_MMAN_H) + else if( 0 == fossil_strnicmp( zArg, "mman\0", 5 ) ){ + rc = 1; + } +#endif +#if defined(USE_SEE) + else if( 0 == fossil_strnicmp( zArg, "see\0", 4 ) ){ + rc = 1; + } +#endif + else if( 0 == fossil_strnicmp( zArg, "markdown\0", 9 ) ){ + rc = 1; + } + if( g.thTrace ){ + Th_Trace("[hasfeature %#h] => %d<br>\n", TH1_LEN(argl[1]), zArg, rc); + } + Th_SetResultInt(interp, rc); + return TH_OK; +} + + +/* +** TH1 command: tclReady +** +** Return true if the fossil binary has the Tcl integration feature +** enabled and it is currently available for use by TH1 scripts. +** +*/ +static int tclReadyCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + int rc = 0; + if( argc!=1 ){ + return Th_WrongNumArgs(interp, "tclReady"); + } +#if defined(FOSSIL_ENABLE_TCL) + if( g.tcl.interp ){ + rc = 1; + } +#endif + if( g.thTrace ){ + Th_Trace("[tclReady] => %d<br>\n", rc); + } + Th_SetResultInt(interp, rc); + return TH_OK; +} + + +/* +** TH1 command: anycap STRING +** +** Return true if the current user +** has any one of the capabilities listed in STRING. +*/ +static int anycapCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + int rc = 0; + int i; + int nn; + if( argc!=2 ){ + return Th_WrongNumArgs(interp, "anycap STRING"); + } + nn = TH1_LEN(argl[1]); + for(i=0; rc==0 && i<nn; i++){ + rc = login_has_capability((char*)&argv[1][i],1,0); + } + if( g.thTrace ){ + Th_Trace("[anycap %#h] => %d<br>\n", TH1_LEN(argl[1]), argv[1], rc); + } + Th_SetResultInt(interp, rc); + return TH_OK; +} + +/* +** TH1 command: combobox NAME TEXT-LIST NUMLINES +** +** Generate an HTML combobox. NAME is both the name of the +** CGI parameter and the name of a variable that contains the +** currently selected value. TEXT-LIST is a list of possible +** values for the combobox. NUMLINES is 1 for a true combobox. +** If NUMLINES is greater than one then the display is a listbox +** with the number of lines given. +*/ +static int comboboxCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + if( argc!=4 ){ + return Th_WrongNumArgs(interp, "combobox NAME TEXT-LIST NUMLINES"); + } + if( enableOutput ){ + int height; + Blob name; + int nValue = 0; + const char *zValue; + char *z, *zH; + int nElem; + int *aszElem; + char **azElem; + int i; + + if( Th_ToInt(interp, argv[3], argl[3], &height) ) return TH_ERROR; + Th_SplitList(interp, argv[2], TH1_LEN(argl[2]), &azElem, &aszElem, &nElem); + blob_init(&name, (char*)argv[1], TH1_LEN(argl[1])); + zValue = Th_Fetch(blob_str(&name), &nValue); + nValue = TH1_LEN(nValue); + zH = htmlize(blob_buffer(&name), blob_size(&name)); + z = mprintf("<select id=\"%s\" name=\"%s\" size=\"%d\">", zH, zH, height); + free(zH); + sendText(0,z, -1, 0); + free(z); + blob_reset(&name); + for(i=0; i<nElem; i++){ + zH = htmlize((char*)azElem[i], aszElem[i]); + if( zValue && aszElem[i]==nValue + && memcmp(zValue, azElem[i], nValue)==0 ){ + z = mprintf("<option value=\"%s\" selected=\"selected\">%s</option>", + zH, zH); + }else{ + z = mprintf("<option value=\"%s\">%s</option>", zH, zH); + } + free(zH); + sendText(0,z, -1, 0); + free(z); + } + sendText(0,"</select>", -1, 0); + Th_Free(interp, azElem); + } + return TH_OK; +} + +/* +** TH1 command: copybtn TARGETID FLIPPED TEXT ?COPYLENGTH? +** +** Output TEXT with a click-to-copy button next to it. Loads the copybtn.js +** Javascript module, and generates HTML elements with the following IDs: +** +** TARGETID: The <span> wrapper around TEXT. +** copy-TARGETID: The <span> for the copy button. +** +** If the FLIPPED argument is non-zero, the copy button is displayed after TEXT. +** +** The optional COPYLENGTH argument defines the length of the substring of TEXT +** copied to clipboard: +** +** <= 0: No limit (default if the argument is omitted). +** >= 3: Truncate TEXT after COPYLENGTH (single-byte) characters. +** 1: Use the "hash-digits" setting as the limit. +** 2: Use the length appropriate for URLs as the limit (defined at +** compile-time by FOSSIL_HASH_DIGITS_URL, defaults to 16). +*/ +static int copybtnCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + if( argc!=4 && argc!=5 ){ + return Th_WrongNumArgs(interp, + "copybtn TARGETID FLIPPED TEXT ?COPYLENGTH?"); + } + if( enableOutput ){ + int flipped = 0; + int copylength = 0; + char *zResult; + if( Th_ToInt(interp, argv[2], argl[2], &flipped) ) return TH_ERROR; + if( argc==5 ){ + if( Th_ToInt(interp, argv[4], argl[4], ©length) ) return TH_ERROR; + } + zResult = style_copy_button( + /*bOutputCGI==*/0, /*TARGETID==*/(char*)argv[1], + flipped, copylength, 0, "%h", /*TEXT==*/(char*)argv[3]); + sendText(0,zResult, -1, 0); + free(zResult); + } + return TH_OK; +} + +/* +** TH1 command: linecount STRING MAX MIN +** +** Return one more than the number of \n characters in STRING. But +** never return less than MIN or more than MAX. +*/ +static int linecntCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + const char *z; + int size, n, i; + int iMin, iMax; + if( argc!=4 ){ + return Th_WrongNumArgs(interp, "linecount STRING MAX MIN"); + } + if( Th_ToInt(interp, argv[2], argl[2], &iMax) ) return TH_ERROR; + if( Th_ToInt(interp, argv[3], argl[3], &iMin) ) return TH_ERROR; + z = argv[1]; + size = TH1_LEN(argl[1]); + for(n=1, i=0; i<size; i++){ + if( z[i]=='\n' ){ + n++; + if( n>=iMax ) break; + } + } + if( n<iMin ) n = iMin; + if( n>iMax ) n = iMax; + Th_SetResultInt(interp, n); + return TH_OK; +} + +/* +** TH1 command: repository ?BOOLEAN? +** +** Return the fully qualified file name of the open repository or an empty +** string if one is not currently open. Optionally, it will attempt to open +** the repository if the boolean argument is non-zero. +*/ +static int repositoryCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + if( argc!=1 && argc!=2 ){ + return Th_WrongNumArgs(interp, "repository ?BOOLEAN?"); + } + if( argc==2 ){ + int openRepository = 0; + if( Th_ToInt(interp, argv[1], argl[1], &openRepository) ){ + return TH_ERROR; + } + if( openRepository ) db_find_and_open_repository(OPEN_OK_NOT_FOUND, 0); + } + Th_SetResult(interp, g.zRepositoryName, -1); + return TH_OK; +} + +/* +** TH1 command: checkout ?BOOLEAN? +** +** Return the fully qualified directory name of the current check-out or an +** empty string if it is not available. Optionally, it will attempt to find +** the current check-out, opening the configuration ("user") database and the +** repository as necessary, if the boolean argument is non-zero. +*/ +static int checkoutCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + if( argc!=1 && argc!=2 ){ + return Th_WrongNumArgs(interp, "checkout ?BOOLEAN?"); + } + if( argc==2 ){ + int openCheckout = 0; + if( Th_ToInt(interp, argv[1], argl[1], &openCheckout) ){ + return TH_ERROR; + } + if( openCheckout ) db_open_local(0); + } + Th_SetResult(interp, g.zLocalRoot, -1); + return TH_OK; +} + +/* +** TH1 command: trace STRING +** +** Generate a TH1 trace message if debugging is enabled. +*/ +static int traceCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + if( argc!=2 ){ + return Th_WrongNumArgs(interp, "trace STRING"); + } + if( g.thTrace ){ + Th_Trace("%s", argv[1]); + } + Th_SetResult(interp, 0, 0); + return TH_OK; +} + +/* +** TH1 command: globalState NAME ?DEFAULT? +** +** Returns a string containing the value of the specified global state +** variable -OR- the specified default value. Currently, the supported +** items are: +** +** "checkout" = The active local check-out directory, if any. +** "configuration" = The active configuration database file name, +** if any. +** "executable" = The fully qualified executable file name. +** "flags" = The TH1 initialization flags. +** "log" = The error log file name, if any. +** "repository" = The active local repository file name, if +** any. +** "top" = The base path for the active server instance, +** if applicable. +** "user" = The active user name, if any. +** "vfs" = The SQLite VFS in use, if overridden. +** +** Attempts to query for unsupported global state variables will result +** in a script error. Additional global state variables may be exposed +** in the future. +** +** See also: checkout, repository, setting +*/ +static int globalStateCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + const char *zDefault = 0; + if( argc!=2 && argc!=3 ){ + return Th_WrongNumArgs(interp, "globalState NAME ?DEFAULT?"); + } + if( argc==3 ){ + zDefault = argv[2]; + } + if( fossil_strnicmp(argv[1], "checkout\0", 9)==0 ){ + Th_SetResult(interp, g.zLocalRoot ? g.zLocalRoot : zDefault, -1); + return TH_OK; + }else if( fossil_strnicmp(argv[1], "configuration\0", 14)==0 ){ + Th_SetResult(interp, g.zConfigDbName ? g.zConfigDbName : zDefault, -1); + return TH_OK; + }else if( fossil_strnicmp(argv[1], "executable\0", 11)==0 ){ + Th_SetResult(interp, g.nameOfExe ? g.nameOfExe : zDefault, -1); + return TH_OK; + }else if( fossil_strnicmp(argv[1], "flags\0", 6)==0 ){ + Th_SetResultInt(interp, g.th1Flags); + return TH_OK; + }else if( fossil_strnicmp(argv[1], "log\0", 4)==0 ){ + Th_SetResult(interp, g.zErrlog ? g.zErrlog : zDefault, -1); + return TH_OK; + }else if( fossil_strnicmp(argv[1], "repository\0", 11)==0 ){ + Th_SetResult(interp, g.zRepositoryName ? g.zRepositoryName : zDefault, -1); + return TH_OK; + }else if( fossil_strnicmp(argv[1], "top\0", 4)==0 ){ + Th_SetResult(interp, g.zTop ? g.zTop : zDefault, -1); + return TH_OK; + }else if( fossil_strnicmp(argv[1], "user\0", 5)==0 ){ + Th_SetResult(interp, g.zLogin ? g.zLogin : zDefault, -1); + return TH_OK; + }else if( fossil_strnicmp(argv[1], "vfs\0", 4)==0 ){ + Th_SetResult(interp, g.zVfsName ? g.zVfsName : zDefault, -1); + return TH_OK; + }else{ + Th_ErrorMessage(interp, "unsupported global state:", + argv[1], TH1_LEN(argl[1])); + return TH_ERROR; + } +} + +/* +** TH1 command: getParameter NAME ?DEFAULT? +** +** Return the value of the specified query parameter or the specified default +** value when there is no matching query parameter. +*/ +static int getParameterCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + const char *zDefault = 0; + const char *zVal; + int sz; + if( argc!=2 && argc!=3 ){ + return Th_WrongNumArgs(interp, "getParameter NAME ?DEFAULT?"); + } + if( argc==3 ){ + zDefault = argv[2]; + } + zVal = cgi_parameter(argv[1], zDefault); + sz = th_strlen(zVal); + Th_SetResult(interp, zVal, TH1_ADD_TAINT(sz)); + return TH_OK; +} + +/* +** TH1 command: setParameter NAME VALUE +** +** Sets the value of the specified query parameter. +*/ +static int setParameterCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + if( argc!=3 ){ + return Th_WrongNumArgs(interp, "setParameter NAME VALUE"); + } + cgi_replace_parameter(fossil_strdup(argv[1]), fossil_strdup(argv[2])); + return TH_OK; +} + +/* +** TH1 command: reinitialize ?FLAGS? +** +** Reinitializes the TH1 interpreter using the specified flags. +*/ +static int reinitializeCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + u32 flags = TH_INIT_DEFAULT; + if( argc!=1 && argc!=2 ){ + return Th_WrongNumArgs(interp, "reinitialize ?FLAGS?"); + } + if( argc==2 ){ + int iFlags; + if( Th_ToInt(interp, argv[1], argl[1], &iFlags) ){ + return TH_ERROR; + }else{ + flags = (u32)iFlags; + } + } + Th_FossilInit(flags & ~TH_INIT_FORBID_MASK); + Th_SetResult(interp, 0, 0); + return TH_OK; +} + +/* +** TH1 command: render STRING +** +** Renders the template and writes the results. +*/ +static int renderCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + int rc; + if( argc!=2 ){ + return Th_WrongNumArgs(interp, "render STRING"); + } + rc = Th_Render(argv[1]); + Th_SetResult(interp, 0, 0); + return rc; +} + +/* +** TH1 command: defHeader TITLE +** +** Returns the default page header. +*/ +static int defHeaderCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + if( argc!=1 ){ + return Th_WrongNumArgs(interp, "defHeader"); + } + Th_SetResult(interp, get_default_header(), -1); + return TH_OK; +} + +/* +** TH1 command: styleHeader TITLE +** +** Render the configured style header for the selected skin. +*/ +static int styleHeaderCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + if( argc!=2 ){ + return Th_WrongNumArgs(interp, "styleHeader TITLE"); + } + if( Th_IsRepositoryOpen() ){ + style_header("%s", argv[1]); + Th_SetResult(interp, 0, 0); + return TH_OK; + }else{ + Th_SetResult(interp, "repository unavailable", -1); + return TH_ERROR; + } +} + +/* +** TH1 command: styleFooter +** +** Render the configured style footer for the selected skin. +*/ +static int styleFooterCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + if( argc!=1 ){ + return Th_WrongNumArgs(interp, "styleFooter"); + } + if( Th_IsRepositoryOpen() ){ + style_finish_page(); + Th_SetResult(interp, 0, 0); + return TH_OK; + }else{ + Th_SetResult(interp, "repository unavailable", -1); + return TH_ERROR; + } +} + +/* +** TH1 command: styleScript ?BUILTIN-FILENAME? +** +** Render the js.txt file from the current skin. Or, if an argument +** is supplied, render the built-in filename given. +** +** By "rendering" we mean that the script is loaded and run through +** TH1 to expand variables and process <th1>...</th1> script. Contrast +** with the "builtin_request_js BUILTIN-FILENAME" command which just +** loads the file as-is without interpretation. +*/ +static int styleScriptCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + if( argc!=1 && argc!=2 ){ + return Th_WrongNumArgs(interp, "styleScript ?BUILTIN_NAME?"); + } + if( Th_IsRepositoryOpen() ){ + const char *zScript; + if( argc==2 ){ + zScript = (const char*)builtin_file(argv[1], 0); + }else{ + zScript = skin_get("js"); + } + if( zScript==0 ) zScript = ""; + Th_Render(zScript); + Th_SetResult(interp, 0, 0); + return TH_OK; + }else{ + Th_SetResult(interp, "repository unavailable", -1); + return TH_ERROR; + } +} + +/* +** TH1 command: submenu link LABEL URL +** +** Add a hyperlink to the submenu. +*/ +static int submenuCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + if( argc!=4 || memcmp(argv[1],"link",5)!=0 ){ + return Th_WrongNumArgs(interp, "submenu link LABEL URL"); + } + if( argl[2]==0 ){ + Th_SetResult(interp, "link's LABEL is empty", -1); + return TH_ERROR; + } + if( argl[3]==0 ){ + Th_SetResult(interp, "link's URL is empty", -1); + return TH_ERROR; + } + /* + ** Label and URL are unescaped because it is expected that + ** style_finish_page() provides propper escaping via %h format. + */ + style_submenu_element( fossil_strdup(argv[2]), "%s", argv[3] ); + Th_SetResult(interp, 0, 0); + return TH_OK; +} + +/* +** TH1 command: builtin_request_js NAME +** +** Request that the built-in javascript file called NAME be added to the +** end of the generated page. +** +** See also: styleScript +*/ +static int builtinRequestJsCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + if( argc!=2 ){ + return Th_WrongNumArgs(interp, "builtin_request_js NAME"); + } + builtin_request_js(argv[1]); + return TH_OK; +} + +/* +** TH1 command: artifact ID ?FILENAME? +** +** Attempts to locate the specified artifact and return its contents. An +** error is generated if the repository is not open or the artifact cannot +** be found. +*/ +static int artifactCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + if( argc!=2 && argc!=3 ){ + return Th_WrongNumArgs(interp, "artifact ID ?FILENAME?"); + } + if( Th_IsRepositoryOpen() ){ + int rid; + Blob content; + if( argc==3 ){ + rid = th1_artifact_from_ci_and_filename(interp, argv[1], argv[2]); + }else{ + rid = th1_name_to_typed_rid(interp, argv[1], "*"); + } + if( rid!=0 && content_get(rid, &content) ){ + Th_SetResult(interp, blob_str(&content), blob_size(&content)); + blob_reset(&content); + return TH_OK; + }else{ + return TH_ERROR; + } + }else{ + Th_SetResult(interp, "repository unavailable", -1); + return TH_ERROR; + } +} + +/* +** TH1 command: cgiHeaderLine line +** +** Adds the specified line to the CGI header. +*/ +static int cgiHeaderLineCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + if( argc!=2 ){ + return Th_WrongNumArgs(interp, "cgiHeaderLine line"); + } + cgi_append_header(argv[1]); + return TH_OK; +} + +/* +** TH1 command: unversioned content FILENAME +** +** Attempts to locate the specified unversioned file and return its contents. +** An error is generated if the repository is not open or the unversioned file +** cannot be found. +*/ +static int unversionedContentCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + if( argc!=3 ){ + return Th_WrongNumArgs(interp, "unversioned content FILENAME"); + } + if( Th_IsRepositoryOpen() ){ + Blob content; + if( unversioned_content(argv[2], &content)!=0 ){ + Th_SetResult(interp, blob_str(&content), blob_size(&content)); + blob_reset(&content); + return TH_OK; + }else{ + return TH_ERROR; + } + }else{ + Th_SetResult(interp, "repository unavailable", -1); + return TH_ERROR; + } +} + +/* +** TH1 command: unversioned list +** +** Returns a list of the names of all unversioned files held in the local +** repository. An error is generated if the repository is not open. +*/ +static int unversionedListCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + if( argc!=2 ){ + return Th_WrongNumArgs(interp, "unversioned list"); + } + if( Th_IsRepositoryOpen() ){ + Stmt q; + char *zList = 0; + int nList = 0; + db_prepare(&q, "SELECT name FROM unversioned WHERE hash IS NOT NULL" + " ORDER BY name"); + while( db_step(&q)==SQLITE_ROW ){ + Th_ListAppend(interp, &zList, &nList, db_column_text(&q,0), -1); + } + db_finalize(&q); + Th_SetResult(interp, zList, nList); + Th_Free(interp, zList); + return TH_OK; + }else{ + Th_SetResult(interp, "repository unavailable", -1); + return TH_ERROR; + } +} + +static int unversionedCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + static const Th_SubCommand aSub[] = { + { "content", unversionedContentCmd }, + { "list", unversionedListCmd }, + { 0, 0 } + }; + return Th_CallSubCommand(interp, p, argc, argv, argl, aSub); +} + + +/* +** TH1 command: utime +** +** Return the number of microseconds of CPU time consumed by the current +** process in user space. +*/ +static int utimeCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + sqlite3_uint64 x; + char zUTime[50]; + fossil_cpu_times(&x, 0); + sqlite3_snprintf(sizeof(zUTime), zUTime, "%llu", x); + Th_SetResult(interp, zUTime, -1); + return TH_OK; +} + +/* +** TH1 command: stime +** +** Return the number of microseconds of CPU time consumed by the current +** process in system space. +*/ +static int stimeCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + sqlite3_uint64 x; + char zUTime[50]; + fossil_cpu_times(0, &x); + sqlite3_snprintf(sizeof(zUTime), zUTime, "%llu", x); + Th_SetResult(interp, zUTime, -1); + return TH_OK; +} + +/* +** TH1 command: taint STRING +** +** Return a copy of STRING that is marked as tainted. +*/ +static int taintCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + if( argc!=2 ){ + return Th_WrongNumArgs(interp, "STRING"); + } + Th_SetResult(interp, argv[1], TH1_ADD_TAINT(argl[1])); + return TH_OK; +} + +/* +** TH1 command: untaint STRING +** +** Return a copy of STRING that is marked as untainted. +*/ +static int untaintCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + if( argc!=2 ){ + return Th_WrongNumArgs(interp, "STRING"); + } + Th_SetResult(interp, argv[1], TH1_LEN(argl[1])); + return TH_OK; +} + +/* +** TH1 command: randhex N +** +** Return N*2 random hexadecimal digits with N<50. If N is omitted, +** use a value of 10. +*/ +static int randhexCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + int n; + unsigned char aRand[50]; + unsigned char zOut[100]; + if( argc!=1 && argc!=2 ){ + return Th_WrongNumArgs(interp, "repository ?BOOLEAN?"); + } + if( argc==2 ){ + if( Th_ToInt(interp, argv[1], argl[1], &n) ){ + return TH_ERROR; + } + if( n<1 ) n = 1; + if( n>(int)sizeof(aRand) ) n = sizeof(aRand); + }else{ + n = 10; + } + sqlite3_randomness(n, aRand); + encode16(aRand, zOut, n); + Th_SetResult(interp, (const char *)zOut, -1); + return TH_OK; +} + +/* +** Run sqlite3_step() while suppressing error messages sent to the +** rendered webpage or to the console. +*/ +static int ignore_errors_step(sqlite3_stmt *pStmt){ + int rc; + g.dbIgnoreErrors++; + rc = sqlite3_step(pStmt); + g.dbIgnoreErrors--; + return rc; +} + +/* +** TH1 command: query [-nocomplain] SQL CODE +** +** Run the SQL query given by the SQL argument. For each row in the result +** set, run CODE. +** +** In SQL, parameters such as $var are filled in using the value of variable +** "var". Result values are stored in variables with the column name prior +** to each invocation of CODE. +*/ +static int queryCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + sqlite3_stmt *pStmt; + int rc; + const char *zSql; + int nSql; + const char *zTail; + int n, i; + int res = TH_OK; + int nVar; + char *zErr = 0; + int noComplain = 0; + + if( argc>3 && TH1_LEN(argl[1])==11 + && strncmp(argv[1], "-nocomplain", 11)==0 + ){ + argc--; + argv++; + argl++; + noComplain = 1; + } + if( argc!=3 ){ + return Th_WrongNumArgs(interp, "query SQL CODE"); + } + if( g.db==0 ){ + if( noComplain ) return TH_OK; + Th_ErrorMessage(interp, "database is not open", 0, 0); + return TH_ERROR; + } + zSql = argv[1]; + nSql = argl[1]; + if( TH1_TAINTED(nSql) ){ + if( Th_ReportTaint(interp,"query SQL",zSql,nSql) ){ + return TH_ERROR; + } + nSql = TH1_LEN(nSql); + } + + while( res==TH_OK && nSql>0 ){ + zErr = 0; + report_restrict_sql(&zErr); + g.dbIgnoreErrors++; + rc = sqlite3_prepare_v2(g.db, argv[1], TH1_LEN(argl[1]), &pStmt, &zTail); + g.dbIgnoreErrors--; + report_unrestrict_sql(); + if( rc!=0 || zErr!=0 ){ + if( noComplain ) return TH_OK; + Th_ErrorMessage(interp, "SQL error: ", + zErr ? zErr : sqlite3_errmsg(g.db), -1); + return TH_ERROR; + } + n = (int)(zTail - zSql); + zSql += n; + nSql -= n; + if( pStmt==0 ) continue; + nVar = sqlite3_bind_parameter_count(pStmt); + for(i=1; i<=nVar; i++){ + const char *zVar = sqlite3_bind_parameter_name(pStmt, i); + int szVar = zVar ? th_strlen(zVar) : 0; + if( szVar>1 && zVar[0]=='$' + && Th_GetVar(interp, zVar+1, szVar-1)==TH_OK ){ + int nVal; + const char *zVal = Th_GetResult(interp, &nVal); + sqlite3_bind_text(pStmt, i, zVal, TH1_LEN(nVal), SQLITE_TRANSIENT); + } + } + while( res==TH_OK && ignore_errors_step(pStmt)==SQLITE_ROW ){ + int nCol = sqlite3_column_count(pStmt); + for(i=0; i<nCol; i++){ + const char *zCol = sqlite3_column_name(pStmt, i); + int szCol = th_strlen(zCol); + const char *zVal = (const char*)sqlite3_column_text(pStmt, i); + int szVal = sqlite3_column_bytes(pStmt, i); + Th_SetVar(interp, zCol, szCol, zVal, TH1_ADD_TAINT(szVal)); + } + if( g.thTrace ){ + Th_Trace("query_eval {<pre>%#h</pre>}<br>\n",TH1_LEN(argl[2]),argv[2]); + } + res = Th_Eval(interp, 0, argv[2], TH1_LEN(argl[2])); + if( g.thTrace ){ + int nTrRes; + char *zTrRes = (char*)Th_GetResult(g.interp, &nTrRes); + Th_Trace("[query_eval] => %h {%#h}<br>\n", + Th_ReturnCodeName(res, 0), TH1_LEN(nTrRes), zTrRes); + } + if( res==TH_BREAK || res==TH_CONTINUE ) res = TH_OK; + } + rc = sqlite3_finalize(pStmt); + if( rc!=SQLITE_OK ){ + if( noComplain ) return TH_OK; + Th_ErrorMessage(interp, "SQL error: ", sqlite3_errmsg(g.db), -1); + return TH_ERROR; + } + } + return res; +} + +/* +** TH1 command: setting name +** +** Gets and returns the value of the specified Fossil setting. +*/ +#define SETTING_WRONGNUMARGS "setting ?-strict? ?--? name" +static int settingCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + int rc; + int strict = 0; + int nArg = 1; + char *zValue; + if( argc<2 || argc>4 ){ + return Th_WrongNumArgs(interp, SETTING_WRONGNUMARGS); + } + if( fossil_strcmp(argv[nArg], "-strict")==0 ){ + strict = 1; nArg++; + } + if( fossil_strcmp(argv[nArg], "--")==0 ) nArg++; + if( nArg+1!=argc ){ + return Th_WrongNumArgs(interp, SETTING_WRONGNUMARGS); + } + zValue = db_get(argv[nArg], 0); + if( zValue!=0 ){ + Th_SetResult(interp, zValue, -1); + rc = TH_OK; + }else if( strict ){ + Th_ErrorMessage(interp, "no value for setting \"", argv[nArg], -1); + rc = TH_ERROR; + }else{ + Th_SetResult(interp, 0, 0); + rc = TH_OK; + } + if( g.thTrace ){ + Th_Trace("[setting %s%#h] => %d<br>\n", strict ? "strict " : "", + TH1_LEN(argl[nArg]), argv[nArg], rc); + } + return rc; +} + +/* +** TH1 command: glob_match ?-one? ?--? patternList string +** +** Checks the string against the specified glob pattern -OR- list of glob +** patterns and returns non-zero if there is a match. +*/ +#define GLOB_MATCH_WRONGNUMARGS "glob_match ?-one? ?--? patternList string" +static int globMatchCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + int rc; + int one = 0; + int nArg = 1; + Glob *pGlob = 0; + if( argc<3 || argc>5 ){ + return Th_WrongNumArgs(interp, GLOB_MATCH_WRONGNUMARGS); + } + if( fossil_strcmp(argv[nArg], "-one")==0 ){ + one = 1; nArg++; + } + if( fossil_strcmp(argv[nArg], "--")==0 ) nArg++; + if( nArg+2!=argc ){ + return Th_WrongNumArgs(interp, GLOB_MATCH_WRONGNUMARGS); + } + if( one ){ + Th_SetResultInt(interp, sqlite3_strglob(argv[nArg], argv[nArg+1])==0); + rc = TH_OK; + }else{ + pGlob = glob_create(argv[nArg]); + if( pGlob ){ + Th_SetResultInt(interp, glob_match(pGlob, argv[nArg+1])); + rc = TH_OK; + }else{ + Th_SetResult(interp, "unable to create glob from pattern list", -1); + rc = TH_ERROR; + } + glob_free(pGlob); + } + return rc; +} + +/* +** TH1 command: regexp ?-nocase? ?--? exp string +** +** Checks the string against the specified regular expression and returns +** non-zero if it matches. If the regular expression is invalid or cannot +** be compiled, an error will be generated. +*/ +#define REGEXP_WRONGNUMARGS "regexp ?-nocase? ?--? exp string" +static int regexpCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + int rc; + int noCase = 0; + int nArg = 1; + ReCompiled *pRe = 0; + const char *zErr; + if( argc<3 || argc>5 ){ + return Th_WrongNumArgs(interp, REGEXP_WRONGNUMARGS); + } + if( fossil_strcmp(argv[nArg], "-nocase")==0 ){ + noCase = 1; nArg++; + } + if( fossil_strcmp(argv[nArg], "--")==0 ) nArg++; + if( nArg+2!=argc ){ + return Th_WrongNumArgs(interp, REGEXP_WRONGNUMARGS); + } + zErr = fossil_re_compile(&pRe, argv[nArg], noCase); + if( !zErr ){ + Th_SetResultInt(interp, re_match(pRe, + (const unsigned char *)argv[nArg+1], TH1_LEN(argl[nArg+1]))); + rc = TH_OK; + }else{ + Th_SetResult(interp, zErr, -1); + rc = TH_ERROR; + } + re_free(pRe); + return rc; +} + +/* +** TH1 command: http ?-asynchronous? ?--? url ?payload? +** +** Perform an HTTP or HTTPS request for the specified URL. If a +** payload is present, it will be interpreted as text/plain and +** the POST method will be used; otherwise, the GET method will +** be used. Upon success, if the -asynchronous option is used, an +** empty string is returned as the result; otherwise, the response +** from the server is returned as the result. Synchronous requests +** are not currently implemented. +*/ +#define HTTP_WRONGNUMARGS "http ?-asynchronous? ?--? url ?payload?" +static int httpCmd( + Th_Interp *interp, + void *p, + int argc, + const char **argv, + int *argl +){ + int nArg = 1; + int fAsynchronous = 0; + const char *zType, *zRegexp; + Blob payload; + ReCompiled *pRe = 0; + UrlData urlData; + + if( argc<2 || argc>5 ){ + return Th_WrongNumArgs(interp, HTTP_WRONGNUMARGS); + } + if( fossil_strnicmp(argv[nArg], "-asynchronous", TH1_LEN(argl[nArg]))==0 ){ + fAsynchronous = 1; nArg++; + } + if( fossil_strcmp(argv[nArg], "--")==0 ) nArg++; + if( nArg+1!=argc && nArg+2!=argc ){ + return Th_WrongNumArgs(interp, REGEXP_WRONGNUMARGS); + } + memset(&urlData, '\0', sizeof(urlData)); + url_parse_local(argv[nArg], 0, &urlData); + if( urlData.isSsh || urlData.isFile ){ + Th_ErrorMessage(interp, "url must be http:// or https://", 0, 0); + return TH_ERROR; + } + zRegexp = db_get("th1-uri-regexp", 0); + if( zRegexp && zRegexp[0] ){ + const char *zErr = fossil_re_compile(&pRe, zRegexp, 0); + if( zErr ){ + Th_SetResult(interp, zErr, -1); + return TH_ERROR; + } + } + if( !pRe || !re_match(pRe, (const unsigned char *)urlData.canonical, -1) ){ + Th_SetResult(interp, "url not allowed", -1); + re_free(pRe); + return TH_ERROR; + } + re_free(pRe); + blob_zero(&payload); + if( nArg+2==argc ){ + blob_append(&payload, argv[nArg+1], TH1_LEN(argl[nArg+1])); + zType = "POST"; + }else{ + zType = "GET"; + } + if( fAsynchronous ){ + const char *zSep, *zParams; + Blob hdr; + zParams = strrchr(argv[nArg], '?'); + if( strlen(urlData.path)>0 && zParams!=argv[nArg] ){ + zSep = ""; + }else{ + zSep = "/"; + } + blob_zero(&hdr); + blob_appendf(&hdr, "%s %s%s%s HTTP/1.0\r\n", + zType, zSep, urlData.path, zParams ? zParams : ""); + if( urlData.proxyAuth ){ + blob_appendf(&hdr, "Proxy-Authorization: %s\r\n", urlData.proxyAuth); + } + if( urlData.passwd && urlData.user && urlData.passwd[0]=='#' ){ + char *zCredentials = mprintf("%s:%s", urlData.user, &urlData.passwd[1]); + char *zEncoded = encode64(zCredentials, -1); + blob_appendf(&hdr, "Authorization: Basic %s\r\n", zEncoded); + fossil_free(zEncoded); + fossil_free(zCredentials); + } + blob_appendf(&hdr, "Host: %s\r\n" + "User-Agent: %s\r\n", urlData.hostname, get_user_agent()); + if( zType[0]=='P' ){ + blob_appendf(&hdr, "Content-Type: application/x-www-form-urlencoded\r\n" + "Content-Length: %d\r\n\r\n", blob_size(&payload)); + }else{ + blob_appendf(&hdr, "\r\n"); + } + if( transport_open(&urlData) ){ + Th_ErrorMessage(interp, transport_errmsg(&urlData), 0, 0); + blob_reset(&hdr); + blob_reset(&payload); + return TH_ERROR; + } + transport_send(&urlData, &hdr); + transport_send(&urlData, &payload); + blob_reset(&hdr); + blob_reset(&payload); + transport_close(&urlData); + Th_SetResult(interp, 0, 0); /* NOTE: Asynchronous, no results. */ + return TH_OK; + }else{ + Th_ErrorMessage(interp, + "synchronous requests are not yet implemented", 0, 0); + blob_reset(&payload); + return TH_ERROR; + } +} + +/* +** TH1 command: captureTh1 STRING +** +** Evaluates the given string as TH1 code and captures any of its +** TH1-generated output as a string (instead of it being output), +** which becomes the result of the function. +*/ +static int captureTh1Cmd( + Th_Interp *interp, + void *pConvert, + int argc, + const char **argv, + int *argl +){ + Blob out = empty_blob; + Blob * pOrig; + const char * zStr; + int nStr, rc; + if( argc!=2 ){ + return Th_WrongNumArgs(interp, "captureTh1 STRING"); + } + pOrig = Th_SetOutputBlob(&out); + zStr = argv[1]; + nStr = TH1_LEN(argl[1]); + rc = Th_Eval(g.interp, 0, zStr, nStr); + Th_SetOutputBlob(pOrig); + if(0==rc){ + Th_SetResult(g.interp, blob_str(&out), blob_size(&out)); + } + blob_reset(&out); + return rc; +} + + +/* +** Attempts to open the configuration ("user") database. Optionally, also +** attempts to try to find the repository and open it. +*/ +void Th_OpenConfig( + int openRepository +){ + if( openRepository && !Th_IsRepositoryOpen() ){ + db_find_and_open_repository(OPEN_ANY_SCHEMA | OPEN_OK_NOT_FOUND, 0); + if( Th_IsRepositoryOpen() ){ + g.th1Flags |= TH_STATE_REPOSITORY; + }else{ + g.th1Flags &= ~TH_STATE_REPOSITORY; + } + } + if( !Th_IsConfigOpen() ){ + db_open_config(0, 1); + if( Th_IsConfigOpen() ){ + g.th1Flags |= TH_STATE_CONFIG; + }else{ + g.th1Flags &= ~TH_STATE_CONFIG; + } + } +} + +/* +** Attempts to close the configuration ("user") database. Optionally, also +** attempts to close the repository. +*/ +void Th_CloseConfig( + int closeRepository +){ + if( g.th1Flags & TH_STATE_CONFIG ){ + db_close_config(); + g.th1Flags &= ~TH_STATE_CONFIG; + } + if( closeRepository && (g.th1Flags & TH_STATE_REPOSITORY) ){ + db_close(1); + g.th1Flags &= ~TH_STATE_REPOSITORY; + } +} + +/* +** Make sure the interpreter has been initialized. Initialize it if +** it has not been already. +** +** The interpreter is stored in the g.interp global variable. +*/ +void Th_FossilInit(u32 flags){ + int wasInit = 0; + int needConfig = flags & TH_INIT_NEED_CONFIG; + int forceReset = flags & TH_INIT_FORCE_RESET; + int forceTcl = flags & TH_INIT_FORCE_TCL; + int forceSetup = flags & TH_INIT_FORCE_SETUP; + int noRepo = flags & TH_INIT_NO_REPO; + static unsigned int aFlags[] = {0, 1, WIKI_LINKSONLY}; + static int anonFlag = LOGIN_ANON; + static int zeroInt = 0; + static struct _Command { + const char *zName; + Th_CommandProc xProc; + void *pContext; + } aCommand[] = { + {"anoncap", hascapCmd, (void*)&anonFlag}, + {"anycap", anycapCmd, 0}, + {"artifact", artifactCmd, 0}, + {"builtin_request_js", builtinRequestJsCmd, 0}, + {"capexpr", capexprCmd, 0}, + {"captureTh1", captureTh1Cmd, 0}, + {"cgiHeaderLine", cgiHeaderLineCmd, 0}, + {"checkout", checkoutCmd, 0}, + {"combobox", comboboxCmd, 0}, + {"copybtn", copybtnCmd, 0}, + {"date", dateCmd, 0}, + {"decorate", wikiCmd, (void*)&aFlags[2]}, + {"defHeader", defHeaderCmd, 0}, + {"dir", dirCmd, 0}, + {"enable_output", enableOutputCmd, 0}, + {"encode64", encode64Cmd, 0}, + {"getParameter", getParameterCmd, 0}, + {"glob_match", globMatchCmd, 0}, + {"globalState", globalStateCmd, 0}, + {"httpize", httpizeCmd, 0}, + {"hascap", hascapCmd, (void*)&zeroInt}, + {"hasfeature", hasfeatureCmd, 0}, + {"html", putsCmd, (void*)&aFlags[0]}, + {"htmlize", htmlizeCmd, 0}, + {"http", httpCmd, 0}, + {"insertCsrf", insertCsrfCmd, 0}, + {"linecount", linecntCmd, 0}, + {"markdown", markdownCmd, 0}, + {"nonce", nonceCmd, 0}, + {"puts", putsCmd, (void*)&aFlags[1]}, + {"query", queryCmd, 0}, + {"randhex", randhexCmd, 0}, + {"redirect", redirectCmd, 0}, + {"regexp", regexpCmd, 0}, + {"reinitialize", reinitializeCmd, 0}, + {"render", renderCmd, 0}, + {"repository", repositoryCmd, 0}, + {"searchable", searchableCmd, 0}, + {"setParameter", setParameterCmd, 0}, + {"setting", settingCmd, 0}, + {"styleFooter", styleFooterCmd, 0}, + {"styleHeader", styleHeaderCmd, 0}, + {"styleScript", styleScriptCmd, 0}, + {"submenu", submenuCmd, 0}, + {"taint", taintCmd, 0}, + {"tclReady", tclReadyCmd, 0}, + {"trace", traceCmd, 0}, + {"stime", stimeCmd, 0}, + {"untaint", untaintCmd, 0}, + {"unversioned", unversionedCmd, 0}, + {"utime", utimeCmd, 0}, + {"verifyCsrf", verifyCsrfCmd, 0}, + {"verifyLogin", verifyLoginCmd, 0}, + {"wiki", wikiCmd, (void*)&aFlags[0]}, + {"wiki_assoc", wikiAssocCmd, 0}, + {0, 0, 0} + }; + if( g.thTrace ){ + Th_Trace("th1-init 0x%x => 0x%x<br>\n", g.th1Flags, flags); + } + if( needConfig ){ + /* + ** This function uses several settings which may be defined in the + ** repository and/or the global configuration. Since the caller + ** passed a non-zero value for the needConfig parameter, make sure + ** the necessary database connections are open prior to continuing. + */ + Th_OpenConfig(!noRepo); + } + if( forceReset || forceTcl || g.interp==0 ){ + int created = 0; + int i; + if( g.interp==0 ){ + Th_Vtab *pVtab = 0; +#if defined(TH_MEMDEBUG) + if( fossil_getenv("TH1_DELETE_INTERP")!=0 ){ + pVtab = &vtab; + if( g.thTrace ){ + Th_Trace("th1-init MEMDEBUG ENABLED<br>\n"); + } + } +#endif + g.interp = Th_CreateInterp(pVtab); + created = 1; + } + if( forceReset || created ){ + th_register_language(g.interp); /* Basic scripting commands. */ + } +#ifdef FOSSIL_ENABLE_TCL + if( forceTcl || fossil_getenv("TH1_ENABLE_TCL")!=0 || + db_get_boolean("tcl", 0) ){ + if( !g.tcl.setup ){ + g.tcl.setup = db_get("tcl-setup", 0); /* Grab Tcl setup script. */ + } + th_register_tcl(g.interp, &g.tcl); /* Tcl integration commands. */ + } +#endif + for(i=0; i<count(aCommand); i++){ + if ( !aCommand[i].zName || !aCommand[i].xProc ) continue; + Th_CreateCommand(g.interp, aCommand[i].zName, aCommand[i].xProc, + aCommand[i].pContext, 0); + } + }else{ + wasInit = 1; + } + if( forceSetup || !wasInit ){ + int rc = TH_OK; + if( !g.th1Setup ){ + g.th1Setup = db_get("th1-setup", 0); /* Grab TH1 setup script. */ + } + if( g.th1Setup ){ + rc = Th_Eval(g.interp, 0, g.th1Setup, -1); + if( rc==TH_ERROR ){ + int nResult = 0; + char *zResult = (char*)Th_GetResult(g.interp, &nResult); + sendError(0,zResult, nResult, 0); + } + } + if( g.thTrace ){ + Th_Trace("th1-setup {%h} => %h<br>\n", g.th1Setup, + Th_ReturnCodeName(rc, 0)); + } + } + g.th1Flags &= ~TH_INIT_MASK; + g.th1Flags |= (flags & TH_INIT_MASK); +} + +/* +** Store a string value in a variable in the interpreter if the variable +** does not already exist. +*/ +void Th_MaybeStore(const char *zName, const char *zValue){ + Th_FossilInit(TH_INIT_DEFAULT); + if( zValue && !Th_ExistsVar(g.interp, zName, -1) ){ + if( g.thTrace ){ + Th_Trace("maybe_set %h {%h}<br>\n", zName, zValue); + } + Th_SetVar(g.interp, zName, -1, zValue, strlen(zValue)); + } +} + +/* +** Store a string value in a variable in the interpreter. +*/ +void Th_Store(const char *zName, const char *zValue){ + Th_FossilInit(TH_INIT_DEFAULT); + if( zValue ){ + if( g.thTrace ){ + Th_Trace("set %h {%h}<br>\n", zName, zValue); + } + Th_SetVar(g.interp, zName, -1, zValue, strlen(zValue)); + } +} + +/* +** Store a string value in a variable in the interpreter +** with the "taint" marking, so that TH1 knows that this +** variable contains content under the control of the remote +** user and presents a risk of XSS or SQL-injection attacks. +*/ +void Th_StoreUnsafe(const char *zName, const char *zValue){ + Th_FossilInit(TH_INIT_DEFAULT); + if( zValue ){ + if( g.thTrace ){ + Th_Trace("set %h [taint {%h}]<br>\n", zName, zValue); + } + Th_SetVar(g.interp, zName, -1, zValue, TH1_ADD_TAINT(strlen(zValue))); + } +} + +/* +** Appends an element to a TH1 list value. This function is called by the +** transfer subsystem; therefore, it must be very careful to avoid doing +** any unnecessary work. To that end, the TH1 subsystem will not be called +** or initialized if the list pointer is zero (i.e. which will be the case +** when TH1 transfer hooks are disabled). +*/ +void Th_AppendToList( + char **pzList, + int *pnList, + const char *zElem, + int nElem +){ + if( pzList && zElem ){ + Th_FossilInit(TH_INIT_DEFAULT); + Th_ListAppend(g.interp, pzList, pnList, zElem, nElem); + } +} + +/* +** Stores a list value in the specified TH1 variable using the specified +** array of strings as the source of the element values. +*/ +void Th_StoreList( + const char *zName, + char **pzList, + int nList +){ + Th_FossilInit(TH_INIT_DEFAULT); + if( pzList ){ + char *zValue = 0; + int nValue = 0; + int i; + for(i=0; i<nList; i++){ + Th_ListAppend(g.interp, &zValue, &nValue, pzList[i], -1); + } + if( g.thTrace ){ + Th_Trace("set %h {%h}<br>\n", zName, zValue); + } + Th_SetVar(g.interp, zName, -1, zValue, nValue); + Th_Free(g.interp, zValue); + } +} + +/* +** Store an integer value in a variable in the interpreter. +*/ +void Th_StoreInt(const char *zName, int iValue){ + Blob value; + char *zValue; + Th_FossilInit(TH_INIT_DEFAULT); + blob_zero(&value); + blob_appendf(&value, "%d", iValue); + zValue = blob_str(&value); + if( g.thTrace ){ + Th_Trace("set %h {%h}<br>\n", zName, zValue); + } + Th_SetVar(g.interp, zName, -1, zValue, strlen(zValue)); + blob_reset(&value); +} + +/* +** Unset a variable. +*/ +void Th_Unstore(const char *zName){ + if( g.interp ){ + Th_UnsetVar(g.interp, (char*)zName, -1); + } +} + +/* +** Retrieve a string value from the interpreter. If no such +** variable exists, return NULL. +*/ +char *Th_Fetch(const char *zName, int *pSize){ + int rc; + Th_FossilInit(TH_INIT_DEFAULT); + rc = Th_GetVar(g.interp, (char*)zName, -1); + if( rc==TH_OK ){ + return (char*)Th_GetResult(g.interp, pSize); + }else{ + return 0; + } +} + +/* +** Return true if the string begins with the TH1 begin-script +** tag: <th1>. +*/ +static int isBeginScriptTag(const char *z){ + return z[0]=='<' + && (z[1]=='t' || z[1]=='T') + && (z[2]=='h' || z[2]=='H') + && z[3]=='1' + && z[4]=='>'; +} + +/* +** Return true if the string begins with the TH1 end-script +** tag: </th1>. +*/ +static int isEndScriptTag(const char *z){ + return z[0]=='<' + && z[1]=='/' + && (z[2]=='t' || z[2]=='T') + && (z[3]=='h' || z[3]=='H') + && z[4]=='1' + && z[5]=='>'; +} + +/* +** If string z[0...] contains a valid variable name, return +** the number of characters in that name. Otherwise, return 0. +*/ +static int validVarName(const char *z){ + int i = 0; + int inBracket = 0; + if( z[0]=='<' ){ + inBracket = 1; + z++; + } + if( z[0]==':' && z[1]==':' && fossil_isalpha(z[2]) ){ + z += 3; + i += 3; + }else if( fossil_isalpha(z[0]) ){ + z ++; + i += 1; + }else{ + return 0; + } + while( fossil_isalnum(z[0]) || z[0]=='_' ){ + z++; + i++; + } + if( inBracket ){ + if( z[0]!='>' ) return 0; + i += 2; + } + return i; +} + +#ifdef FOSSIL_ENABLE_TH1_HOOKS +/* +** This function determines if TH1 hooks are enabled for the repository. It +** may be necessary to open the repository and/or the configuration ("user") +** database from within this function. Before this function returns, any +** database opened will be closed again. This is very important because some +** commands do not expect the repository and/or the configuration ("user") +** database to be open prior to their own code doing so. +*/ +int Th_AreHooksEnabled(void){ + int rc; + if( fossil_getenv("TH1_ENABLE_HOOKS")!=0 ){ + return 1; + } + Th_OpenConfig(1); + rc = db_get_boolean("th1-hooks", 0); + Th_CloseConfig(1); + return rc; +} + +/* +** This function is called by Fossil just prior to dispatching a command. +** Returning a value other than TH_OK from this function (i.e. via an +** evaluated script raising an error or calling [break]/[continue]) will +** cause the actual command execution to be skipped. +*/ +int Th_CommandHook( + const char *zName, + unsigned int cmdFlags +){ + int rc = TH_OK; + if( !Th_AreHooksEnabled() ) return rc; + Th_FossilInit(TH_INIT_HOOK); + Th_Store("cmd_name", zName); + Th_StoreList("cmd_args", g.argv, g.argc); + Th_StoreInt("cmd_flags", cmdFlags); + rc = Th_Eval(g.interp, 0, "command_hook", -1); + if( rc==TH_ERROR ){ + int nResult = 0; + char *zResult = (char*)Th_GetResult(g.interp, &nResult); + /* + ** Make sure that the TH1 script error was not caused by a "missing" + ** command hook handler as that is not actually an error condition. + */ + nResult = TH1_LEN(nResult); + if( memcmp(zResult, NO_COMMAND_HOOK_ERROR, nResult)!=0 ){ + sendError(0,zResult, nResult, 0); + }else{ + /* + ** There is no command hook handler "installed". This situation + ** is NOT actually an error. + */ + rc = TH_OK; + } + } + /* + ** If the script returned TH_ERROR (e.g. the "command_hook" TH1 command does + ** not exist because commands are not being hooked), return TH_OK because we + ** do not want to skip executing essential commands unless the called command + ** (i.e. "command_hook") explicitly forbids this by successfully returning + ** TH_BREAK or TH_CONTINUE. + */ + if( g.thTrace ){ + Th_Trace("[command_hook {%h}] => %h<br>\n", zName, + Th_ReturnCodeName(rc, 0)); + } + /* + ** Does our call to Th_FossilInit() result in opening a database? If so, + ** clean it up now. This is very important because some commands do not + ** expect the repository and/or the configuration ("user") database to be + ** open prior to their own code doing so. + */ + if( TH_INIT_HOOK & TH_INIT_NEED_CONFIG ) Th_CloseConfig(1); + return rc; +} + +/* +** This function is called by Fossil just after dispatching a command. +** Returning a value other than TH_OK from this function (i.e. via an +** evaluated script raising an error or calling [break]/[continue]) may +** cause an error message to be displayed to the local interactive user. +** Currently, TH1 error messages generated by this function are ignored. +*/ +int Th_CommandNotify( + const char *zName, + unsigned int cmdFlags +){ + int rc = TH_OK; + if( !Th_AreHooksEnabled() ) return rc; + Th_FossilInit(TH_INIT_HOOK); + Th_Store("cmd_name", zName); + Th_StoreList("cmd_args", g.argv, g.argc); + Th_StoreInt("cmd_flags", cmdFlags); + rc = Th_Eval(g.interp, 0, "command_notify", -1); + if( g.thTrace ){ + Th_Trace("[command_notify {%h}] => %h<br>\n", zName, + Th_ReturnCodeName(rc, 0)); + } + /* + ** Does our call to Th_FossilInit() result in opening a database? If so, + ** clean it up now. This is very important because some commands do not + ** expect the repository and/or the configuration ("user") database to be + ** open prior to their own code doing so. + */ + if( TH_INIT_HOOK & TH_INIT_NEED_CONFIG ) Th_CloseConfig(1); + return rc; +} + +/* +** This function is called by Fossil just prior to processing a web page. +** Returning a value other than TH_OK from this function (i.e. via an +** evaluated script raising an error or calling [break]/[continue]) will +** cause the actual web page processing to be skipped. +*/ +int Th_WebpageHook( + const char *zName, + unsigned int cmdFlags +){ + int rc = TH_OK; + if( !Th_AreHooksEnabled() ) return rc; + Th_FossilInit(TH_INIT_HOOK); + Th_Store("web_name", zName); + Th_StoreList("web_args", g.argv, g.argc); + Th_StoreInt("web_flags", cmdFlags); + rc = Th_Eval(g.interp, 0, "webpage_hook", -1); + if( rc==TH_ERROR ){ + int nResult = 0; + char *zResult = (char*)Th_GetResult(g.interp, &nResult); + /* + ** Make sure that the TH1 script error was not caused by a "missing" + ** webpage hook handler as that is not actually an error condition. + */ + nResult = TH1_LEN(nResult); + if( memcmp(zResult, NO_WEBPAGE_HOOK_ERROR, nResult)!=0 ){ + sendError(0,zResult, nResult, 1); + }else{ + /* + ** There is no webpage hook handler "installed". This situation + ** is NOT actually an error. + */ + rc = TH_OK; + } + } + /* + ** If the script returned TH_ERROR (e.g. the "webpage_hook" TH1 command does + ** not exist because commands are not being hooked), return TH_OK because we + ** do not want to skip processing essential web pages unless the called + ** command (i.e. "webpage_hook") explicitly forbids this by successfully + ** returning TH_BREAK or TH_CONTINUE. + */ + if( g.thTrace ){ + Th_Trace("[webpage_hook {%h}] => %h<br>\n", zName, + Th_ReturnCodeName(rc, 0)); + } + /* + ** Does our call to Th_FossilInit() result in opening a database? If so, + ** clean it up now. This is very important because some commands do not + ** expect the repository and/or the configuration ("user") database to be + ** open prior to their own code doing so. + */ + if( TH_INIT_HOOK & TH_INIT_NEED_CONFIG ) Th_CloseConfig(1); + return rc; +} + +/* +** This function is called by Fossil just after processing a web page. +** Returning a value other than TH_OK from this function (i.e. via an +** evaluated script raising an error or calling [break]/[continue]) may +** cause an error message to be displayed to the remote user. +** Currently, TH1 error messages generated by this function are ignored. +*/ +int Th_WebpageNotify( + const char *zName, + unsigned int cmdFlags +){ + int rc = TH_OK; + if( !Th_AreHooksEnabled() ) return rc; + Th_FossilInit(TH_INIT_HOOK); + Th_Store("web_name", zName); + Th_StoreList("web_args", g.argv, g.argc); + Th_StoreInt("web_flags", cmdFlags); + rc = Th_Eval(g.interp, 0, "webpage_notify", -1); + if( g.thTrace ){ + Th_Trace("[webpage_notify {%h}] => %h<br>\n", zName, + Th_ReturnCodeName(rc, 0)); + } + /* + ** Does our call to Th_FossilInit() result in opening a database? If so, + ** clean it up now. This is very important because some commands do not + ** expect the repository and/or the configuration ("user") database to be + ** open prior to their own code doing so. + */ + if( TH_INIT_HOOK & TH_INIT_NEED_CONFIG ) Th_CloseConfig(1); + return rc; +} +#endif + + +#ifdef FOSSIL_ENABLE_TH1_DOCS +/* +** This function determines if TH1 docs are enabled for the repository. +*/ +int Th_AreDocsEnabled(void){ + if( fossil_getenv("TH1_ENABLE_DOCS")!=0 ){ + return 1; + } + return db_get_boolean("th1-docs", 0); +} +#endif + + +#if INTERFACE +/* +** Flags for use with Th_RenderToBlob. These must not overlap with +** TH_INIT_MASK. +*/ +#define TH_R2B_MASK ((u32)0x0f000) +#define TH_R2B_NO_VARS ((u32)0x01000) /* Disables eval of $vars and $<vars> */ +#endif + +/* +** If pOut is NULL, this works identically to Th_Render() and sends +** any TH1-generated output to stdin (in CLI mode) or the CGI buffer +** (in CGI mode), else it works just like that function but appends +** any TH1-generated output to the given blob. A bitmask of TH_R2B_xxx +** and/or TH_INIT_xxx flags may be passed as the 3rd argument, or 0 +** for default options. Note that this function necessarily calls +** Th_FossilInit(), which may unset flags used on previous calls +** unless mFlags is explicitly passed in. +*/ +int Th_RenderToBlob(const char *z, Blob * pOut, u32 mFlags){ + int i = 0; + int n; + int rc = TH_OK; + char *zResult; + Blob * const origOut = Th_SetOutputBlob(pOut); + + assert(0==(TH_R2B_MASK & TH_INIT_MASK) && "init/r2b mask conflict"); + Th_FossilInit(mFlags & TH_INIT_MASK); + while( z[i] ){ + if( 0==(TH_R2B_NO_VARS & mFlags) + && z[i]=='$' && (n = validVarName(&z[i+1]))>0 ){ + const char *zVar; + int nVar; + int encode = 1; + sendText(pOut,z, i, 0); + if( z[i+1]=='<' ){ + /* Variables of the form $<aaa> are html escaped */ + zVar = &z[i+2]; + nVar = n-2; + }else{ + /* Variables of the form $aaa are output raw */ + zVar = &z[i+1]; + nVar = n; + encode = 0; + } + rc = Th_GetVar(g.interp, (char*)zVar, nVar); + z += i+1+n; + i = 0; + zResult = (char*)Th_GetResult(g.interp, &n); + if( !TH1_TAINTED(n) + || encode + || Th_ReportTaint(g.interp, "inline variable", zVar, nVar)==TH_OK + ){ + sendText(pOut,(char*)zResult, n, encode); + } + }else if( z[i]=='<' && isBeginScriptTag(&z[i]) ){ + sendText(pOut,z, i, 0); + z += i+5; + for(i=0; z[i] && (z[i]!='<' || !isEndScriptTag(&z[i])); i++){} + if( g.thTrace ){ + Th_Trace("render_eval {<pre>%#h</pre>}<br>\n", i, z); + } + rc = Th_Eval(g.interp, 0, (const char*)z, i); + if( g.thTrace ){ + int nTrRes; + char *zTrRes = (char*)Th_GetResult(g.interp, &nTrRes); + Th_Trace("[render_eval] => %h {%#h}<br>\n", + Th_ReturnCodeName(rc, 0), TH1_LEN(nTrRes), zTrRes); + } + if( rc!=TH_OK ) break; + z += i; + if( z[0] ){ z += 6; } + i = 0; + }else{ + i += strcspn(&z[i+1], "<$") + 1; + } + } + if( rc==TH_ERROR ){ + zResult = (char*)Th_GetResult(g.interp, &n); + sendError(pOut,zResult, n, 1); + }else{ + sendText(pOut,z, i, 0); + } + Th_SetOutputBlob(origOut); + return rc; +} + +/* +** The z[] input contains text mixed with TH1 scripts. +** The TH1 scripts are contained within <th1>...</th1>. +** TH1 variables are $aaa or $<aaa>. The first form of +** variable is literal. The second is run through htmlize +** before being inserted. +** +** This routine processes the template and writes the results to one +** of stdout, CGI, or an internal blob which was set up via a prior +** call to Th_SetOutputBlob(). +*/ +int Th_Render(const char *z){ + return Th_RenderToBlob(z, pThOut, g.th1Flags) + /* Maintenance reminder: on most calls to Th_Render(), e.g. for + ** outputing the site skin, pThOut will be 0, which means that + ** Th_RenderToBlob() will output directly to the CGI buffer (in + ** CGI mode) or stdout (in CLI mode). Recursive calls, however, + ** e.g. via the "render" script function binding, need to use the + ** pThOut blob in order to avoid out-of-order output if + ** Th_SetOutputBlob() has been called. If it has not been called, + ** pThOut will be 0, which will redirect the output to CGI/stdout, + ** as appropriate. We need to pass on g.th1Flags for the case of + ** recursive calls. + */; +} + +/* +** SETTING: vuln-report width=8 default=block +** +** This setting controls Fossil's behavior when it encounters a potential +** XSS or SQL-injection vulnerability due to misuse of TH1 configuration +** scripts. Choices are: +** +** off Do nothing. Ignore the vulnerability. +** +** log Write a report of the problem into the error log. +** +** block Like "log" but also prevent the offending TH1 command +** from running. +** +** fatal Render an error message page instead of the requested +** page. +*/ + +/* +** Report misuse of a tainted string in TH1. +** +** The behavior depends on the vuln-report setting. If "off", this routine +** is a no-op. Otherwise, right a message into the error log. If +** vuln-report is "log", that is all that happens. But for any other +** value of vuln-report, a fatal error is raised. +*/ +int Th_ReportTaint( + Th_Interp *interp, /* Report error here, if an error is reported */ + const char *zWhere, /* Where the tainted string appears */ + const char *zStr, /* The tainted string */ + int nStr /* Length of the tainted string */ +){ + static const char *zDisp = 0; /* Dispensation; what to do with the error */ + const char *zVulnType; /* Type of vulnerability */ + + if( zDisp==0 ) zDisp = db_get("vuln-report","block"); + if( is_false(zDisp) ) return 0; + if( strstr(zWhere,"SQL")!=0 ){ + zVulnType = "SQL-injection"; + }else{ + zVulnType = "XSS"; + } + nStr = TH1_LEN(nStr); + fossil_errorlog("possible TH1 %s vulnerability due to tainted %s: \"%.*s\"", + zVulnType, zWhere, nStr, zStr); + if( strcmp(zDisp,"log")==0 ){ + return 0; + } + if( strcmp(zDisp,"block")==0 ){ + char *z = mprintf("tainted %s: \"", zWhere); + Th_ErrorMessage(interp, z, zStr, nStr); + fossil_free(z); + }else{ + char *z = mprintf("%#h", nStr, zStr); + zDisp = "off"; + cgi_reset_content(); + style_submenu_enable(0); + style_set_current_feature("error"); + style_header("Configuration Error"); + @ <p>Error in a TH1 configuration script: + @ tainted %h(zWhere): "%z(z)" + style_finish_page(); + cgi_reply(); + fossil_exit(1); + } + return 1; +} + +/* +** COMMAND: test-th-render +** +** Usage: %fossil test-th-render FILE +** +** Read the content of the file named "FILE" as if it were a header or +** footer or ticket rendering script, evaluate it, and show the results +** on standard output. +** +** Options: +** --cgi Include a CGI response header in the output +** --http Include an HTTP response header in the output +** --open-config Open the configuration database +** --set-anon-caps Set anonymous login capabilities +** --set-user-caps Set user login capabilities +** --th-trace Trace TH1 execution (for debugging purposes) +*/ +void test_th_render(void){ + int forceCgi, fullHttpReply; + Blob in; + Th_InitTraceLog(); + forceCgi = find_option("cgi", 0, 0)!=0; + fullHttpReply = find_option("http", 0, 0)!=0; + if( fullHttpReply ) forceCgi = 1; + if( forceCgi ) Th_ForceCgi(fullHttpReply); + if( find_option("open-config", 0, 0)!=0 ){ + Th_OpenConfig(1); + } + if( find_option("set-anon-caps", 0, 0)!=0 ){ + const char *zCap = fossil_getenv("TH1_TEST_ANON_CAPS"); + login_set_capabilities(zCap ? zCap : "sx", LOGIN_ANON); + g.useLocalauth = 1; + } + if( find_option("set-user-caps", 0, 0)!=0 ){ + const char *zCap = fossil_getenv("TH1_TEST_USER_CAPS"); + login_set_capabilities(zCap ? zCap : "sx", 0); + g.useLocalauth = 1; + } + db_find_and_open_repository(OPEN_OK_NOT_FOUND|OPEN_SUBSTITUTE,0); + verify_all_options(); + if( g.argc<3 ){ + usage("FILE"); + } + blob_zero(&in); + blob_read_from_file(&in, g.argv[2], ExtFILE); + Th_Render(blob_str(&in)); + Th_PrintTraceLog(); + if( forceCgi ) cgi_reply(); +} + +/* +** COMMAND: test-th-eval +** +** Usage: %fossil test-th-eval SCRIPT +** +** Evaluate SCRIPT as if it were a header or footer or ticket rendering +** script and show the results on standard output. SCRIPT may be either +** a filename or a string of th1 script code. +** +** Options: +** --cgi Include a CGI response header in the output +** --http Include an HTTP response header in the output +** --open-config Open the configuration database +** --set-anon-caps Set anonymous login capabilities +** --set-user-caps Set user login capabilities +** --th-trace Trace TH1 execution (for debugging purposes) +*/ +void test_th_eval(void){ + int rc; + const char *zRc; + const char *zCode = 0; + int forceCgi, fullHttpReply; + Blob code = empty_blob; + Th_InitTraceLog(); + forceCgi = find_option("cgi", 0, 0)!=0; + fullHttpReply = find_option("http", 0, 0)!=0; + if( fullHttpReply ) forceCgi = 1; + if( forceCgi ) Th_ForceCgi(fullHttpReply); + if( find_option("open-config", 0, 0)!=0 ){ + Th_OpenConfig(1); + } + if( find_option("set-anon-caps", 0, 0)!=0 ){ + const char *zCap = fossil_getenv("TH1_TEST_ANON_CAPS"); + login_set_capabilities(zCap ? zCap : "sx", LOGIN_ANON); + g.useLocalauth = 1; + } + if( find_option("set-user-caps", 0, 0)!=0 ){ + const char *zCap = fossil_getenv("TH1_TEST_USER_CAPS"); + login_set_capabilities(zCap ? zCap : "sx", 0); + g.useLocalauth = 1; + } + db_find_and_open_repository(OPEN_OK_NOT_FOUND|OPEN_SUBSTITUTE,0); + verify_all_options(); + if( g.argc!=3 ){ + usage("script"); + } + if(file_isfile(g.argv[2], ExtFILE)){ + blob_read_from_file(&code, g.argv[2], ExtFILE); + zCode = blob_str(&code); + }else{ + zCode = g.argv[2]; + } + Th_FossilInit(TH_INIT_DEFAULT); + rc = Th_Eval(g.interp, 0, zCode, -1); + zRc = Th_ReturnCodeName(rc, 1); + fossil_print("%s%s%s\n", zRc, zRc ? ": " : "", Th_GetResult(g.interp, 0)); + Th_PrintTraceLog(); + blob_reset(&code); + if( forceCgi ) cgi_reply(); +} + +/* +** COMMAND: test-th-source +** +** Usage: %fossil test-th-source FILE +** +** Evaluate the contents of the file named "FILE" as if it were a header +** or footer or ticket rendering script and show the results on standard +** output. +** +** Options: +** --cgi Include a CGI response header in the output +** --http Include an HTTP response header in the output +** --open-config Open the configuration database +** --set-anon-caps Set anonymous login capabilities +** --set-user-caps Set user login capabilities +** --th-trace Trace TH1 execution (for debugging purposes) +** --no-print-result Do not output the final result. Use if it +** interferes with script output. +*/ +void test_th_source(void){ + int rc; + const char *zRc; + int forceCgi, fullHttpReply, fNoPrintRc; + Blob in; + Th_InitTraceLog(); + forceCgi = find_option("cgi", 0, 0)!=0; + fullHttpReply = find_option("http", 0, 0)!=0; + fNoPrintRc = find_option("no-print-result",0,0)!=0; + if( fullHttpReply ) forceCgi = 1; + if( forceCgi ) Th_ForceCgi(fullHttpReply); + if( find_option("open-config", 0, 0)!=0 ){ + Th_OpenConfig(1); + } + if( find_option("set-anon-caps", 0, 0)!=0 ){ + const char *zCap = fossil_getenv("TH1_TEST_ANON_CAPS"); + login_set_capabilities(zCap ? zCap : "sx", LOGIN_ANON); + g.useLocalauth = 1; + } + if( find_option("set-user-caps", 0, 0)!=0 ){ + const char *zCap = fossil_getenv("TH1_TEST_USER_CAPS"); + login_set_capabilities(zCap ? zCap : "sx", 0); + g.useLocalauth = 1; + } + verify_all_options(); + if( g.argc!=3 ){ + usage("file"); + } + blob_zero(&in); + blob_read_from_file(&in, g.argv[2], ExtFILE); + Th_FossilInit(TH_INIT_DEFAULT); + rc = Th_Eval(g.interp, 0, blob_str(&in), -1); + zRc = Th_ReturnCodeName(rc, 1); + if(0==fNoPrintRc){ + fossil_print("%s%s%s\n", zRc, zRc ? ": " : "", + Th_GetResult(g.interp, 0)); + } + Th_PrintTraceLog(); + if( forceCgi ) cgi_reply(); +} + +#ifdef FOSSIL_ENABLE_TH1_HOOKS +/* +** COMMAND: test-th-hook +** +** Usage: %fossil test-th-hook TYPE NAME FLAGS +** +** Evaluates the TH1 script configured for the pre-operation (i.e. a command +** or web page) "hook" or post-operation "notification". The results of the +** script evaluation, if any, will be printed to the standard output channel. +** The NAME argument must be the name of a command or web page; however, it +** does not necessarily have to be a command or web page that is normally +** recognized by Fossil. The FLAGS argument will be used to set the value +** of the "cmd_flags" and/or "web_flags" TH1 variables, if applicable. The +** TYPE argument must be one of the following: +** +** cmdhook Executes the TH1 procedure [command_hook], after +** setting the TH1 variables "cmd_name", "cmd_args", +** and "cmd_flags" to appropriate values. +** +** cmdnotify Executes the TH1 procedure [command_notify], after +** setting the TH1 variables "cmd_name", "cmd_args", +** and "cmd_flags" to appropriate values. +** +** webhook Executes the TH1 procedure [webpage_hook], after +** setting the TH1 variables "web_name", "web_args", +** and "web_flags" to appropriate values. +** +** webnotify Executes the TH1 procedure [webpage_notify], after +** setting the TH1 variables "web_name", "web_args", +** and "web_flags" to appropriate values. +** +** Options: +** --cgi Include a CGI response header in the output +** --http Include an HTTP response header in the output +** --th-trace Trace TH1 execution (for debugging purposes) +*/ +void test_th_hook(void){ + int rc = TH_OK; + int nResult = 0; + char *zResult = 0; + int forceCgi, fullHttpReply; + Th_InitTraceLog(); + forceCgi = find_option("cgi", 0, 0)!=0; + fullHttpReply = find_option("http", 0, 0)!=0; + if( fullHttpReply ) forceCgi = 1; + if( forceCgi ) Th_ForceCgi(fullHttpReply); + verify_all_options(); + if( g.argc<5 ){ + usage("TYPE NAME FLAGS"); + } + if( fossil_stricmp(g.argv[2], "cmdhook")==0 ){ + rc = Th_CommandHook(g.argv[3], (unsigned int)atoi(g.argv[4])); + }else if( fossil_stricmp(g.argv[2], "cmdnotify")==0 ){ + rc = Th_CommandNotify(g.argv[3], (unsigned int)atoi(g.argv[4])); + }else if( fossil_stricmp(g.argv[2], "webhook")==0 ){ + rc = Th_WebpageHook(g.argv[3], (unsigned int)atoi(g.argv[4])); + }else if( fossil_stricmp(g.argv[2], "webnotify")==0 ){ + rc = Th_WebpageNotify(g.argv[3], (unsigned int)atoi(g.argv[4])); + }else{ + fossil_fatal("Unknown TH1 hook %s", g.argv[2]); + } + if( g.interp ){ + zResult = (char*)Th_GetResult(g.interp, &nResult); + } + sendText(0,"RESULT (", -1, 0); + sendText(0,Th_ReturnCodeName(rc, 0), -1, 0); + sendText(0,")", -1, 0); + if( zResult && nResult>0 ){ + sendText(0,": ", -1, 0); + sendText(0,zResult, nResult, 0); + } + sendText(0,"\n", -1, 0); + Th_PrintTraceLog(); + if( forceCgi ) cgi_reply(); +} +#endif |
