/* ** 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" /* ** 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 threadsafe. */ 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 }; /* ** 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); } /* ** True if output is enabled. False if disabled. */ static int enableOutput = 1; /* ** TH command: enable_output BOOLEAN ** ** Enable or disable the puts and hputs commands. */ static int enableOutputCmd( Th_Interp *interp, void *p, int argc, const char **argv, int *argl ){ if( argc!=2 ){ return Th_WrongNumArgs(interp, "enable_output BOOLEAN"); } return Th_ToInt(interp, argv[1], argl[1], &enableOutput); } /* ** Return a name for a TH1 return code. */ const char *Th_ReturnCodeName(int rc){ static char zRc[32]; switch( rc ){ case TH_OK: return "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"; default: { sqlite3_snprintf(sizeof(zRc),zRc,"return code %d",rc); } } return zRc; } /* ** Send text to the appropriate output: Either to the console ** or to the CGI reply buffer. */ static void sendText(const char *z, int n, int encode){ if( enableOutput && n ){ if( n<0 ) n = strlen(z); if( encode ){ z = htmlize(z, n); n = strlen(z); } if( g.cgiOutput ){ cgi_append_content(z, n); }else{ fwrite(z, 1, n, stdout); fflush(stdout); } if( encode ) free((char*)z); } } static void sendError(const char *z, int n, int forceCgi){ if( forceCgi || g.cgiOutput ){ sendText("

", -1, 0); } sendText("ERROR: ", -1, 0); sendText((char*)z, n, 1); sendText(forceCgi || g.cgiOutput ? "

" : "\n", -1, 0); } /* ** TH command: puts STRING ** TH command: html STRING ** ** Output STRING as HTML (html) or unchanged (puts). */ static int putsCmd( Th_Interp *interp, void *pConvert, int argc, const char **argv, int *argl ){ if( argc!=2 ){ return Th_WrongNumArgs(interp, "puts STRING"); } sendText((char*)argv[1], argl[1], pConvert!=0); return TH_OK; } /* ** TH command: wiki STRING ** ** Render the input string as wiki. */ static int wikiCmd( Th_Interp *interp, void *p, int argc, const char **argv, int *argl ){ if( argc!=2 ){ return Th_WrongNumArgs(interp, "wiki STRING"); } if( enableOutput ){ Blob src; blob_init(&src, (char*)argv[1], argl[1]); wiki_convert(&src, 0, WIKI_INLINE|WIKI_NOBADLINKS); blob_reset(&src); } return TH_OK; } /* ** TH 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], argl[1]); Th_SetResult(interp, zOut, -1); free(zOut); return TH_OK; } /* ** TH 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 && argl[1]==6 && memcmp(argv[1],"-local",6)==0 ){ zOut = db_text("??", "SELECT datetime('now','localtime')"); }else{ zOut = db_text("??", "SELECT datetime('now')"); } Th_SetResult(interp, zOut, -1); free(zOut); return TH_OK; } /* ** TH command: hascap STRING... ** ** Return true if the user 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 = 0, i; if( argc<2 ){ return Th_WrongNumArgs(interp, "hascap STRING ..."); } for(i=1; i %d
\n", argl[1], argv[1], rc); } Th_SetResultInt(interp, rc); return TH_OK; } /* ** TH command: hasfeature STRING ** ** Return true if the fossil binary has the given compile-time feature ** enabled. The set of features includes: ** ** "json" = FOSSIL_ENABLE_JSON ** "ssl" = FOSSIL_ENABLE_SSL ** "tcl" = FOSSIL_ENABLE_TCL ** "tclStubs" = FOSSIL_ENABLE_TCL_STUBS ** */ static int hasfeatureCmd( Th_Interp *interp, void *p, int argc, const char **argv, int *argl ){ int rc = 0; char const * zArg; if( argc!=2 ){ return Th_WrongNumArgs(interp, "hasfeature STRING"); } zArg = (char const*)argv[1]; if(NULL==zArg){ /* placeholder for following ifdefs... */ } #if defined(FOSSIL_ENABLE_JSON) else if( 0 == fossil_strnicmp( zArg, "json", 4 ) ){ rc = 1; } #endif #if defined(FOSSIL_ENABLE_SSL) else if( 0 == fossil_strnicmp( zArg, "ssl", 3 ) ){ rc = 1; } #endif #if defined(FOSSIL_ENABLE_TCL) else if( 0 == fossil_strnicmp( zArg, "tcl", 3 ) ){ rc = 1; } #endif #if defined(FOSSIL_ENABLE_TCL_STUBS) else if( 0 == fossil_strnicmp( zArg, "tclStubs", 8 ) ){ rc = 1; } #endif if( g.thTrace ){ Th_Trace("[hasfeature %#h] => %d
\n", argl[1], zArg, rc); } Th_SetResultInt(interp, rc); return TH_OK; } /* ** TH command: anycap STRING ** ** Return true if the 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; if( argc!=2 ){ return Th_WrongNumArgs(interp, "anycap STRING"); } for(i=0; rc==0 && i %d
\n", 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; 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], argl[2], &azElem, &aszElem, &nElem); blob_init(&name, (char*)argv[1], argl[1]); zValue = Th_Fetch(blob_str(&name), &nValue); z = mprintf("", -1, 0); Th_Free(interp, azElem); } 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 = argl[1]; for(n=1, i=0; i=iMax ) break; } } if( niMax ) 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 ){ int openRepository; if( argc!=1 && argc!=2 ){ return Th_WrongNumArgs(interp, "repository ?BOOLEAN?"); } if( argc==2 ){ 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; } #ifdef _WIN32 # include #else # include # include #endif /* ** Get user and kernel times in microseconds. */ static void getCpuTimes(sqlite3_uint64 *piUser, sqlite3_uint64 *piKernel){ #ifdef _WIN32 FILETIME not_used; FILETIME kernel_time; FILETIME user_time; GetProcessTimes(GetCurrentProcess(), ¬_used, ¬_used, &kernel_time, &user_time); if( piUser ){ *piUser = ((((sqlite3_uint64)user_time.dwHighDateTime)<<32) + (sqlite3_uint64)user_time.dwLowDateTime + 5)/10; } if( piKernel ){ *piKernel = ((((sqlite3_uint64)kernel_time.dwHighDateTime)<<32) + (sqlite3_uint64)kernel_time.dwLowDateTime + 5)/10; } #else struct rusage s; getrusage(RUSAGE_SELF, &s); if( piUser ){ *piUser = ((sqlite3_uint64)s.ru_utime.tv_sec)*1000000 + s.ru_utime.tv_usec; } if( piKernel ){ *piKernel = ((sqlite3_uint64)s.ru_stime.tv_sec)*1000000 + s.ru_stime.tv_usec; } #endif } /* ** 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]; getCpuTimes(&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]; getCpuTimes(0, &x); sqlite3_snprintf(sizeof(zUTime), zUTime, "%llu", x); Th_SetResult(interp, zUTime, -1); return TH_OK; } /* ** 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(int needConfig, int forceSetup){ int wasInit = 0; static struct _Command { const char *zName; Th_CommandProc xProc; void *pContext; } aCommand[] = { {"anycap", anycapCmd, 0}, {"combobox", comboboxCmd, 0}, {"enable_output", enableOutputCmd, 0}, {"linecount", linecntCmd, 0}, {"hascap", hascapCmd, 0}, {"hasfeature", hasfeatureCmd, 0}, {"htmlize", htmlizeCmd, 0}, {"date", dateCmd, 0}, {"html", putsCmd, 0}, {"puts", putsCmd, (void*)1}, {"wiki", wikiCmd, 0}, {"repository", repositoryCmd, 0}, {"utime", utimeCmd, 0}, {"stime", stimeCmd, 0}, {0, 0, 0} }; 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. */ db_find_and_open_repository(OPEN_ANY_SCHEMA | OPEN_OK_NOT_FOUND, 0); db_open_config(0); } if( g.interp==0 ){ int i; g.interp = Th_CreateInterp(&vtab); th_register_language(g.interp); /* Basic scripting commands. */ #ifdef FOSSIL_ENABLE_TCL if( 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 %h
\n", g.th1Setup, Th_ReturnCodeName(rc)); } } } /* ** Store a string value in a variable in the interpreter. */ void Th_Store(const char *zName, const char *zValue){ Th_FossilInit(0, 0); if( zValue ){ if( g.thTrace ){ Th_Trace("set %h {%h}
\n", zName, zValue); } Th_SetVar(g.interp, zName, -1, zValue, strlen(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(0, 0); blob_zero(&value); blob_appendf(&value, "%d", iValue); zValue = blob_str(&value); if( g.thTrace ){ Th_Trace("set %h {%h}
\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(0, 0); 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: . */ 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: . */ 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; } /* ** The z[] input contains text mixed with TH1 scripts. ** The TH1 scripts are contained within .... ** TH1 variables are $aaa or $. 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 ** on either stdout or into CGI. */ int Th_Render(const char *z){ int i = 0; int n; int rc = TH_OK; char *zResult; Th_FossilInit(0, 0); while( z[i] ){ if( z[i]=='$' && (n = validVarName(&z[i+1]))>0 ){ const char *zVar; int nVar; int encode = 1; sendText(z, i, 0); if( z[i+1]=='<' ){ /* Variables of the form $ 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); sendText((char*)zResult, n, encode); }else if( z[i]=='<' && isBeginScriptTag(&z[i]) ){ sendText(z, i, 0); z += i+5; for(i=0; z[i] && (z[i]!='<' || !isEndScriptTag(&z[i])); i++){} rc = Th_Eval(g.interp, 0, (const char*)z, i); if( rc!=TH_OK ) break; z += i; if( z[0] ){ z += 6; } i = 0; }else{ i++; } } if( rc==TH_ERROR ){ zResult = (char*)Th_GetResult(g.interp, &n); sendError(zResult, n, 1); }else{ sendText(z, i, 0); } return rc; } /* ** COMMAND: test-th-render */ void test_th_render(void){ Blob in; if( g.argc<3 ){ usage("FILE"); } db_open_config(0); /* Needed for global "tcl" setting. */ blob_zero(&in); blob_read_from_file(&in, g.argv[2]); Th_Render(blob_str(&in)); }