/*
** 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_NO_ENCODE ((u32)0x00000020) /* Do not html-encode sendText()*/
/* output. */
#define TH_INIT_MASK ((u32)0x0000003F) /* 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 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 };
/*
** 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");
}
}
/*
** - adopted from ls_cmd_rev in checkin.c
** - adopted commands/error handling for usage within th1
** - interface adopted 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], 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 \n", argl[1],argv[1],enableOutput);
}
return rc;
}
/*
** TH1 command: enable_htmlify ?BOOLEAN?
**
** Enable or disable the HTML escaping done by all output which
** originates from TH1 (via sendText()).
**
** If passed no arguments it instead returns 0 or 1 to indicate the
** current state.
*/
static int enableHtmlifyCmd(
Th_Interp *interp,
void *p,
int argc,
const char **argv,
int *argl
){
int rc = 0, buul;
if( argc>3 ){
return Th_WrongNumArgs(interp,
"enable_htmlify [TRACE_LABEL] ?BOOLEAN?");
}
buul = (TH_INIT_NO_ENCODE & g.th1Flags) ? 0 : 1;
Th_SetResultInt(g.interp, buul);
if(argc>1){
if( g.thTrace ){
Th_Trace("enable_htmlify {%.*s} -> %d \n",
argl[1],argv[1],buul);
}
rc = Th_ToInt(interp, argv[argc-1], argl[argc-1], &buul);
if(!rc){
if(buul){
g.th1Flags &= ~TH_INIT_NO_ENCODE;
}else{
g.th1Flags |= TH_INIT_NO_ENCODE;
}
}
}
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, with the exception that that flag is ignored if
** g.th1Flags has the TH_INIT_NO_ENCODE flag.
**
** 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(TH_INIT_NO_ENCODE & g.th1Flags){
encode = 0;
}
if( enableOutput && n ){
if( n<0 ) n = strlen(z);
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, "
} \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} \n",
Th_ReturnCodeName(rc, 0), nTrRes, zTrRes);
}
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(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 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 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, so that, e.g., TH_INIT_NO_ENCODE does not get
** inadvertently toggled off by a recursive call.
*/;
}
/*
** 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;
}
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;
}
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