Index: auto.def
==================================================================
--- auto.def
+++ auto.def
@@ -4,10 +4,11 @@
options {
with-openssl:path|auto|none
=> {Look for openssl in the given path, or auto or none}
with-zlib:path => {Look for zlib in the given path}
+ with-th1-hooks=0 => {Enable TH1 hooks for commands and web pages}
with-tcl:path => {Enable Tcl integration, with Tcl in the specified path}
with-tcl-stubs=0 => {Enable Tcl integration via stubs library mechanism}
with-tcl-private-stubs=0
=> {Enable Tcl integration via private stubs mechanism}
internal-sqlite=1 => {Don't use the internal SQLite, use the system one}
@@ -79,10 +80,15 @@
# have #ifdef guards around the whole file without
# reading config.h first.
define-append EXTRA_CFLAGS -DFOSSIL_ENABLE_JSON
define FOSSIL_ENABLE_JSON
}
+
+if {[opt-bool with-th1-hooks]} {
+ define-append EXTRA_CFLAGS -DFOSSIL_ENABLE_TH1_HOOKS
+ define FOSSIL_ENABLE_TH1_HOOKS
+}
#if {[opt-bool markdown]} {
# # no-op. Markdown is now enabled by default.
#}
Index: src/configure.c
==================================================================
--- src/configure.c
+++ src/configure.c
@@ -97,10 +97,11 @@
{ "timeline-max-comment", CONFIGSET_SKIN },
{ "timeline-plaintext", CONFIGSET_SKIN },
{ "adunit", CONFIGSET_SKIN },
{ "adunit-omit-if-admin", CONFIGSET_SKIN },
{ "adunit-omit-if-user", CONFIGSET_SKIN },
+ { "th1-hooks", CONFIGSET_TH1 },
{ "th1-setup", CONFIGSET_TH1 },
{ "th1-uri-regexp", CONFIGSET_TH1 },
#ifdef FOSSIL_ENABLE_TCL
{ "tcl", CONFIGSET_TH1 },
Index: src/db.c
==================================================================
--- src/db.c
+++ src/db.c
@@ -785,10 +785,31 @@
assert( g.zMainDbType!=0 );
db_attach(zDbName, zLabel);
if( pWasAttached ) *pWasAttached = 1;
}
}
+
+/*
+** Close the user database.
+*/
+void db_close_config(){
+ if( g.useAttach ){
+ db_detach("configdb");
+ g.useAttach = 0;
+ g.zConfigDbName = 0;
+ }else if( g.dbConfig ){
+ sqlite3_close(g.dbConfig);
+ g.dbConfig = 0;
+ g.zConfigDbType = 0;
+ g.zConfigDbName = 0;
+ }else if( g.db && fossil_strcmp(g.zMainDbType, "configdb")==0 ){
+ sqlite3_close(g.db);
+ g.db = 0;
+ g.zMainDbType = 0;
+ g.zConfigDbName = 0;
+ }
+}
/*
** Open the user database in "~/.fossil". Create the database anew if
** it does not already exist.
**
@@ -801,11 +822,14 @@
** case, invoke this routine with useAttach as 1.
*/
void db_open_config(int useAttach){
char *zDbName;
char *zHome;
- if( g.zConfigDbName ) return;
+ if( g.zConfigDbName ){
+ if( useAttach==g.useAttach ) return;
+ db_close_config();
+ }
#if defined(_WIN32) || defined(__CYGWIN__)
zHome = fossil_getenv("LOCALAPPDATA");
if( zHome==0 ){
zHome = fossil_getenv("APPDATA");
if( zHome==0 ){
@@ -2168,14 +2192,15 @@
{ "ssh-command", 0, 40, 0, 0, "" },
{ "ssl-ca-location", 0, 40, 0, 0, "" },
{ "ssl-identity", 0, 40, 0, 0, "" },
#ifdef FOSSIL_ENABLE_TCL
{ "tcl", 0, 0, 0, 0, "off" },
- { "tcl-setup", 0, 40, 0, 1, "" },
+ { "tcl-setup", 0, 40, 1, 1, "" },
#endif
- { "th1-setup", 0, 40, 0, 1, "" },
- { "th1-uri-regexp", 0, 40, 0, 0, "" },
+ { "th1-hooks", 0, 0, 0, 0, "off" },
+ { "th1-setup", 0, 40, 1, 1, "" },
+ { "th1-uri-regexp", 0, 40, 1, 0, "" },
{ "web-browser", 0, 32, 0, 0, "" },
{ "white-foreground", 0, 0, 0, 0, "off" },
{ 0,0,0,0,0,0 }
};
@@ -2369,19 +2394,23 @@
** scripts to be evaluated from TH1. Additionally, the Tcl
** interpreter will be able to evaluate arbitrary TH1
** expressions and scripts. Default: off.
**
** tcl-setup This is the setup script to be evaluated after creating
-** and initializing the Tcl interpreter. By default, this
+** (versionable) and initializing the Tcl interpreter. By default, this
** is empty and no extra setup is performed.
+**
+** th1-hooks If enabled (and Fossil was compiled with support for TH1
+** hooks), special TH1 commands will be called before and
+** after any Fossil command or web page. Default: off.
**
** th1-setup This is the setup script to be evaluated after creating
-** and initializing the TH1 interpreter. By default, this
+** (versionable) and initializing the TH1 interpreter. By default, this
** is empty and no extra setup is performed.
**
** th1-uri-regexp Specify which URI's are allowed in HTTP requests from
-** TH1 scripts. If empty, no HTTP requests are allowed
+** (versionable) TH1 scripts. If empty, no HTTP requests are allowed
** whatsoever. The default is an empty string.
**
** web-browser A shell command used to launch your preferred
** web browser when given a URL as an argument.
** Defaults to "start" on windows, "open" on Mac,
Index: src/main.c
==================================================================
--- src/main.c
+++ src/main.c
@@ -194,10 +194,13 @@
char zCsrfToken[12]; /* Value of the anti-CSRF token */
int okCsrf; /* Anti-CSRF token is present and valid */
int parseCnt[10]; /* Counts of artifacts parsed */
FILE *fDebug; /* Write debug information here, if the file exists */
+#ifdef FOSSIL_ENABLE_TH1_HOOKS
+ int fNoThHook; /* Disable all TH1 command/webpage hooks */
+#endif
int thTrace; /* True to enable TH1 debugging output */
Blob thLog; /* Text of the TH1 debugging output */
int isHome; /* True if rendering the "home" page */
@@ -620,10 +623,13 @@
g.fSshClient = 0;
g.zSshCmd = 0;
if( g.fSqlTrace ) g.fSqlStats = 1;
g.fSqlPrint = find_option("sqlprint", 0, 0)!=0;
g.fHttpTrace = find_option("httptrace", 0, 0)!=0;
+#ifdef FOSSIL_ENABLE_TH1_HOOKS
+ g.fNoThHook = find_option("no-th-hook", 0, 0)!=0;
+#endif
g.zHttpAuth = 0;
g.zLogin = find_option("user", "U", 1);
g.zSSLIdentity = find_option("ssl-identity", 0, 1);
g.zErrlog = find_option("errorlog", 0, 1);
if( find_option("utc",0,0) ) g.fTimeFormat = 1;
@@ -649,13 +655,30 @@
if( !is_valid_fd(2) ) fossil_panic("file descriptor 2 not open");
/* if( is_valid_fd(3) ) fossil_warning("file descriptor 3 is open"); */
#endif
rc = name_search(zCmdName, aCommand, count(aCommand), &idx);
if( rc==1 ){
- fossil_fatal("%s: unknown command: %s\n"
- "%s: use \"help\" for more information\n",
- g.argv[0], zCmdName, g.argv[0]);
+#ifdef FOSSIL_ENABLE_TH1_HOOKS
+ if( !g.isHTTP && !g.fNoThHook ){
+ rc = Th_CommandHook(zCmdName, 0);
+ }else{
+ rc = TH_OK;
+ }
+ if( rc==TH_OK || rc==TH_RETURN || rc==TH_CONTINUE ){
+ if( rc==TH_OK || rc==TH_RETURN ){
+#endif
+ fossil_fatal("%s: unknown command: %s\n"
+ "%s: use \"help\" for more information\n",
+ g.argv[0], zCmdName, g.argv[0]);
+#ifdef FOSSIL_ENABLE_TH1_HOOKS
+ }
+ if( !g.isHTTP && !g.fNoThHook && (rc==TH_OK || rc==TH_CONTINUE) ){
+ Th_CommandNotify(zCmdName, 0);
+ }
+ }
+ fossil_exit(0);
+#endif
}else if( rc==2 ){
int i, n;
Blob couldbe;
blob_zero(&couldbe);
n = strlen(zCmdName);
@@ -669,11 +692,44 @@
"%s: use \"help\" for more information\n",
g.argv[0], zCmdName, g.argv[0], blob_str(&couldbe), g.argv[0]);
fossil_exit(1);
}
atexit( fossil_atexit );
- aCommand[idx].xFunc();
+#ifdef FOSSIL_ENABLE_TH1_HOOKS
+ /*
+ ** The TH1 return codes from the hook will be handled as follows:
+ **
+ ** TH_OK: The xFunc() and the TH1 notification will both be executed.
+ **
+ ** TH_ERROR: The xFunc() will be executed, the TH1 notification will be
+ ** skipped. If the xFunc() is being hooked, the error message
+ ** will be emitted.
+ **
+ ** TH_BREAK: The xFunc() and the TH1 notification will both be skipped.
+ **
+ ** TH_RETURN: The xFunc() will be executed, the TH1 notification will be
+ ** skipped.
+ **
+ ** TH_CONTINUE: The xFunc() will be skipped, the TH1 notification will be
+ ** executed.
+ */
+ if( !g.isHTTP && !g.fNoThHook ){
+ rc = Th_CommandHook(aCommand[idx].zName, aCommand[idx].cmdFlags);
+ }else{
+ rc = TH_OK;
+ }
+ if( rc==TH_OK || rc==TH_RETURN || rc==TH_CONTINUE ){
+ if( rc==TH_OK || rc==TH_RETURN ){
+#endif
+ aCommand[idx].xFunc();
+#ifdef FOSSIL_ENABLE_TH1_HOOKS
+ }
+ if( !g.isHTTP && !g.fNoThHook && (rc==TH_OK || rc==TH_CONTINUE) ){
+ Th_CommandNotify(aCommand[idx].zName, aCommand[idx].cmdFlags);
+ }
+ }
+#endif
fossil_exit(0);
/*NOT_REACHED*/
return 0;
}
@@ -861,10 +917,13 @@
fossil_print("SQLite %s %.30s\n", sqlite3_libversion(), sqlite3_sourceid());
fossil_print("Schema version %s\n", AUX_SCHEMA);
fossil_print("zlib %s, loaded %s\n", ZLIB_VERSION, zlibVersion());
#if defined(FOSSIL_ENABLE_SSL)
fossil_print("SSL (%s)\n", SSLeay_version(SSLEAY_VERSION));
+#endif
+#if defined(FOSSIL_ENABLE_TH1_HOOKS)
+ fossil_print("TH1_HOOKS\n");
#endif
#if defined(FOSSIL_ENABLE_TCL)
Th_FossilInit(TH_INIT_DEFAULT | TH_INIT_FORCE_TCL);
rc = Th_Eval(g.interp, 0, "tclInvoke info patchlevel", -1);
zRc = Th_ReturnCodeName(rc, 0);
@@ -1486,21 +1545,37 @@
}
/* Locate the method specified by the path and execute the function
** that implements that method.
*/
- if( name_search(g.zPath, aWebpage, count(aWebpage), &idx) &&
- name_search("not_found", aWebpage, count(aWebpage), &idx) ){
+ if( name_search(g.zPath, aWebpage, count(aWebpage), &idx) ){
#ifdef FOSSIL_ENABLE_JSON
if(g.json.isJsonMode){
json_err(FSL_JSON_E_RESOURCE_NOT_FOUND,NULL,0);
}else
#endif
{
- cgi_set_status(404,"Not Found");
- @
Not Found
- @ Page not found: %h(g.zPath)
+#ifdef FOSSIL_ENABLE_TH1_HOOKS
+ int rc;
+ if( !g.fNoThHook ){
+ rc = Th_WebpageHook(g.zPath, 0);
+ }else{
+ rc = TH_OK;
+ }
+ if( rc==TH_OK || rc==TH_RETURN || rc==TH_CONTINUE ){
+ if( rc==TH_OK || rc==TH_RETURN ){
+#endif
+ cgi_set_status(404,"Not Found");
+ @ Not Found
+ @ Page not found: %h(g.zPath)
+#ifdef FOSSIL_ENABLE_TH1_HOOKS
+ }
+ if( !g.fNoThHook && (rc==TH_OK || rc==TH_CONTINUE) ){
+ Th_WebpageNotify(g.zPath, 0);
+ }
+ }
+#endif
}
}else if( aWebpage[idx].xFunc!=page_xfer && db_schema_is_outofdate() ){
#ifdef FOSSIL_ENABLE_JSON
if(g.json.isJsonMode){
json_err(FSL_JSON_E_DB_NEEDS_REBUILD,NULL,0);
@@ -1510,11 +1585,45 @@
@ Server Configuration Error
@ The database schema on the server is out-of-date. Please ask
@ the administrator to run fossil rebuild.
}
}else{
- aWebpage[idx].xFunc();
+#ifdef FOSSIL_ENABLE_TH1_HOOKS
+ /*
+ ** The TH1 return codes from the hook will be handled as follows:
+ **
+ ** TH_OK: The xFunc() and the TH1 notification will both be executed.
+ **
+ ** TH_ERROR: The xFunc() will be executed, the TH1 notification will be
+ ** skipped. If the xFunc() is being hooked, the error message
+ ** will be emitted.
+ **
+ ** TH_BREAK: The xFunc() and the TH1 notification will both be skipped.
+ **
+ ** TH_RETURN: The xFunc() will be executed, the TH1 notification will be
+ ** skipped.
+ **
+ ** TH_CONTINUE: The xFunc() will be skipped, the TH1 notification will be
+ ** executed.
+ */
+ int rc;
+ if( !g.fNoThHook ){
+ rc = Th_WebpageHook(aWebpage[idx].zName, aWebpage[idx].cmdFlags);
+ }else{
+ rc = TH_OK;
+ }
+ if( rc==TH_OK || rc==TH_RETURN || rc==TH_CONTINUE ){
+ if( rc==TH_OK || rc==TH_RETURN ){
+#endif
+ aWebpage[idx].xFunc();
+#ifdef FOSSIL_ENABLE_TH1_HOOKS
+ }
+ if( !g.fNoThHook && (rc==TH_OK || rc==TH_CONTINUE) ){
+ Th_WebpageNotify(aWebpage[idx].zName, aWebpage[idx].cmdFlags);
+ }
+ }
+#endif
}
/* Return the result.
*/
cgi_reply();
Index: src/makemake.tcl
==================================================================
--- src/makemake.tcl
+++ src/makemake.tcl
@@ -408,10 +408,14 @@
# FOSSIL_ENABLE_JSON = 1
#### Enable HTTPS support via OpenSSL (links to libssl and libcrypto)
#
# FOSSIL_ENABLE_SSL = 1
+
+#### Enable hooks for commands and web pages via TH1
+#
+# FOSSIL_ENABLE_TH1_HOOKS = 1
#### Enable scripting support via Tcl/Tk
#
# FOSSIL_ENABLE_TCL = 1
@@ -537,10 +541,16 @@
# With HTTPS support
ifdef FOSSIL_ENABLE_SSL
TCC += -DFOSSIL_ENABLE_SSL=1
RCC += -DFOSSIL_ENABLE_SSL=1
endif
+
+# With TH1 hook support
+ifdef FOSSIL_ENABLE_TH1_HOOKS
+TCC += -DFOSSIL_ENABLE_TH1_HOOKS=1
+RCC += -DFOSSIL_ENABLE_TH1_HOOKS=1
+endif
# With Tcl support
ifdef FOSSIL_ENABLE_TCL
TCC += -DFOSSIL_ENABLE_TCL=1
RCC += -DFOSSIL_ENABLE_TCL=1
@@ -1024,10 +1034,13 @@
# Uncomment to enable JSON API
# FOSSIL_ENABLE_JSON = 1
# Uncomment to enable SSL support
# FOSSIL_ENABLE_SSL = 1
+
+# Uncomment to enable TH1 hooks
+# FOSSIL_ENABLE_TH1_HOOKS = 1
# Uncomment to enable Tcl support
# FOSSIL_ENABLE_TCL = 1
!ifdef FOSSIL_ENABLE_SSL
@@ -1082,10 +1095,15 @@
TCC = $(TCC) /DFOSSIL_ENABLE_SSL=1
RCC = $(RCC) /DFOSSIL_ENABLE_SSL=1
LIBS = $(LIBS) $(SSLLIB)
LIBDIR = $(LIBDIR) /LIBPATH:$(SSLLIBDIR)
!endif
+
+!ifdef FOSSIL_ENABLE_TH1_HOOKS
+TCC = $(TCC) /DFOSSIL_ENABLE_TH1_HOOKS=1
+RCC = $(RCC) /DFOSSIL_ENABLE_TH1_HOOKS=1
+!endif
!ifdef FOSSIL_ENABLE_TCL
TCC = $(TCC) /DFOSSIL_ENABLE_TCL=1
RCC = $(RCC) /DFOSSIL_ENABLE_TCL=1
TCC = $(TCC) /DFOSSIL_ENABLE_TCL_STUBS=1
Index: src/th_main.c
==================================================================
--- src/th_main.c
+++ src/th_main.c
@@ -31,10 +31,22 @@
#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_DEFAULT (TH_INIT_NONE) /* Default flags. */
+#define TH_INIT_HOOK (TH_INIT_NEED_CONFIG | TH_INIT_FORCE_SETUP)
+#endif
+
+#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
/*
** Global variable counting the number of outstanding calls to malloc()
** made by the th1 implementation. This is used to catch memory leaks
@@ -327,10 +339,11 @@
**
** Return true if the fossil binary has the given compile-time feature
** enabled. The set of features includes:
**
** "ssl" = FOSSIL_ENABLE_SSL
+** "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
@@ -355,10 +368,15 @@
}
#if defined(FOSSIL_ENABLE_SSL)
else if( 0 == fossil_strnicmp( zArg, "ssl\0", 4 ) ){
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;
}
@@ -984,10 +1002,23 @@
if( openRepository ){
db_find_and_open_repository(OPEN_ANY_SCHEMA | OPEN_OK_NOT_FOUND, 0);
}
db_open_config(0);
}
+
+/*
+** Attempts to close the configuration ("user") database. Optionally, also
+** attempts to close the repository.
+*/
+void Th_CloseConfig(
+ int closeRepository
+){
+ db_close_config();
+ if( closeRepository ){
+ db_close(1);
+ }
+}
/*
** Make sure the interpreter has been initialized. Initialize it if
** it has not been already.
**
@@ -1095,10 +1126,34 @@
Th_Trace("set %h {%h}
\n", zName, zValue);
}
Th_SetVar(g.interp, zName, -1, zValue, strlen(zValue));
}
}
+
+/*
+** Store a list value in a variable in the interpreter.
+*/
+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\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){
@@ -1192,10 +1247,162 @@
if( z[0]!='>' ) return 0;
i += 2;
}
return i;
}
+
+#ifdef FOSSIL_ENABLE_TH1_HOOKS
+/*
+** 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,
+ char cmdFlags
+){
+ int rc = TH_OK;
+ Th_OpenConfig(1);
+ if( fossil_getenv("TH1_ENABLE_HOOKS")==0 && !db_get_boolean("th1-hooks", 0) ){
+ return rc;
+ }
+ Th_CloseConfig(1);
+ 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.
+ */
+ if( memcmp(zResult, NO_COMMAND_HOOK_ERROR, nResult)!=0 ){
+ sendError(zResult, nResult, 0);
+ }
+ }
+ /*
+ ** 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
\n", zName,
+ Th_ReturnCodeName(rc, 0));
+ }
+ return (rc != TH_ERROR) ? rc : TH_OK;
+}
+
+/*
+** 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,
+ char cmdFlags
+){
+ int rc = TH_OK;
+ Th_OpenConfig(1);
+ if( fossil_getenv("TH1_ENABLE_HOOKS")==0 && !db_get_boolean("th1-hooks", 0) ){
+ return rc;
+ }
+ Th_CloseConfig(1);
+ 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
\n", zName,
+ Th_ReturnCodeName(rc, 0));
+ }
+ 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,
+ char cmdFlags
+){
+ int rc = TH_OK;
+ Th_OpenConfig(1);
+ if( fossil_getenv("TH1_ENABLE_HOOKS")==0 && !db_get_boolean("th1-hooks", 0) ){
+ return rc;
+ }
+ Th_CloseConfig(1);
+ 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.
+ */
+ if( memcmp(zResult, NO_WEBPAGE_HOOK_ERROR, nResult)!=0 ){
+ sendError(zResult, nResult, 1);
+ }
+ }
+ /*
+ ** 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
\n", zName,
+ Th_ReturnCodeName(rc, 0));
+ }
+ return (rc != TH_ERROR) ? rc : TH_OK;
+}
+
+/*
+** 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,
+ char cmdFlags
+){
+ int rc = TH_OK;
+ Th_OpenConfig(1);
+ if( fossil_getenv("TH1_ENABLE_HOOKS")==0 && !db_get_boolean("th1-hooks", 0) ){
+ return rc;
+ }
+ Th_CloseConfig(1);
+ 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
\n", zName,
+ Th_ReturnCodeName(rc, 0));
+ }
+ return rc;
+}
+#endif
/*
** The z[] input contains text mixed with TH1 scripts.
** The TH1 scripts are contained within ....
** TH1 variables are $aaa or $. The first form of
@@ -1292,5 +1499,36 @@
rc = Th_Eval(g.interp, 0, g.argv[2], -1);
zRc = Th_ReturnCodeName(rc, 1);
fossil_print("%s%s%s\n", zRc, zRc ? ": " : "", Th_GetResult(g.interp, 0));
Th_PrintTraceLog();
}
+
+#ifdef FOSSIL_ENABLE_TH1_HOOKS
+/*
+** COMMAND: test-th-hook
+*/
+void test_th_hook(void){
+ int rc = TH_OK;
+ int nResult = 0;
+ char *zResult;
+ if( g.argc<5 ){
+ usage("TYPE NAME FLAGS");
+ }
+ if( fossil_stricmp(g.argv[2], "cmdhook")==0 ){
+ rc = Th_CommandHook(g.argv[3], (char)atoi(g.argv[4]));
+ }else if( fossil_stricmp(g.argv[2], "cmdnotify")==0 ){
+ rc = Th_CommandNotify(g.argv[3], (char)atoi(g.argv[4]));
+ }else if( fossil_stricmp(g.argv[2], "webhook")==0 ){
+ rc = Th_WebpageHook(g.argv[3], (char)atoi(g.argv[4]));
+ }else if( fossil_stricmp(g.argv[2], "webnotify")==0 ){
+ rc = Th_WebpageNotify(g.argv[3], (char)atoi(g.argv[4]));
+ }else{
+ fossil_fatal("Unknown TH1 hook %s\n", g.argv[2]);
+ }
+ zResult = (char*)Th_GetResult(g.interp, &nResult);
+ sendText("RESULT (", -1, 0);
+ sendText(Th_ReturnCodeName(rc, 0), -1, 0);
+ sendText("): ", -1, 0);
+ sendText(zResult, nResult, 0);
+ sendText("\n", -1, 0);
+}
+#endif
Index: test/tester.tcl
==================================================================
--- test/tester.tcl
+++ test/tester.tcl
@@ -181,10 +181,66 @@
# Append all arguments into a single value and then returns it.
#
proc appendArgs {args} {
eval append result $args
}
+
+# Return the name of the versioned settings file containing the TH1
+# setup script.
+#
+proc getTh1SetupFileName {} {
+ #
+ # NOTE: This uses the "testdir" global variable provided by the
+ # test suite; alternatively, the root of the source tree
+ # could be obtained directly from Fossil.
+ #
+ return [file normalize [file join [file dirname $::testdir] \
+ .fossil-settings th1-setup]]
+}
+
+# Return the saved name of the versioned settings file containing
+# the TH1 setup script.
+#
+proc getSavedTh1SetupFileName {} {
+ return [appendArgs [getTh1SetupFileName] . [pid]]
+}
+
+# Sets the TH1 setup script to the one provided. Prior to calling
+# this, the [saveTh1SetupFile] procedure should be called in order to
+# preserve the existing TH1 setup script. Prior to completing the test,
+# the [restoreTh1SetupFile] procedure should be called to restore the
+# original TH1 setup script.
+#
+proc writeTh1SetupFile { data } {
+ return [write_file [getTh1SetupFileName] $data]
+}
+
+# Saves the TH1 setup script file by renaming it, based on the current
+# process ID.
+#
+proc saveTh1SetupFile {} {
+ set oldFileName [getTh1SetupFileName]
+ if {[file exists $oldFileName]} then {
+ set newFileName [getSavedTh1SetupFileName]
+ catch {file delete $newFileName}
+ file rename $oldFileName $newFileName
+ file delete $oldFileName
+ }
+}
+
+# Restores the original TH1 setup script file by renaming it back, based
+# on the current process ID.
+#
+proc restoreTh1SetupFile {} {
+ set oldFileName [getSavedTh1SetupFileName]
+ if {[file exists $oldFileName]} then {
+ set newFileName [getTh1SetupFileName]
+ catch {file delete $newFileName}
+ file rename $oldFileName $newFileName
+ file delete $oldFileName
+ }
+}
# Perform a test
#
set test_count 0
proc test {name expr} {
ADDED test/th1-hooks-input.txt
Index: test/th1-hooks-input.txt
==================================================================
--- /dev/null
+++ test/th1-hooks-input.txt
@@ -0,0 +1,4 @@
+GET ${url} HTTP/1.1
+Host: localhost
+User-Agent: Fossil
+
ADDED test/th1-hooks.test
Index: test/th1-hooks.test
==================================================================
--- /dev/null
+++ test/th1-hooks.test
@@ -0,0 +1,201 @@
+#
+# Copyright (c) 2011 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/
+#
+############################################################################
+#
+# TH1 Hooks
+#
+
+fossil test-th-eval "hasfeature th1Hooks"
+
+if {$::RESULT ne "1"} then {
+ puts "Fossil was not compiled with TH1 hooks support."; return
+}
+
+###############################################################################
+
+set env(TH1_ENABLE_HOOKS) 1; # TH1 hooks must be enabled for this test.
+
+###############################################################################
+
+proc fossil_th1_hook_http { repository url } {
+ set suffix [appendArgs [pid] - [clock seconds] .txt]
+ set inFileName [file join $::tempPath [appendArgs test-http-in- $suffix]]
+ set outFileName [file join $::tempPath [appendArgs test-http-out- $suffix]]
+ set data [subst [read_file [file join $::testdir th1-hooks-input.txt]]]
+
+ write_file $inFileName $data
+ fossil http $repository $inFileName $outFileName 127.0.0.1
+ set result [expr {[file exists $outFileName] ? [read_file $outFileName] : ""}]
+
+ if {1} then {
+ catch {file delete $inFileName}
+ catch {file delete $outFileName}
+ }
+
+ return $result
+}
+
+proc first_data_line {} {
+ return [lindex [split [string trim $::RESULT] \r\n] 0]
+}
+
+proc second_data_line {} {
+ return [lindex [split [string trim $::RESULT] \r\n] 1]
+}
+
+proc third_data_line {} {
+ return [lindex [split [string trim $::RESULT] \r\n] 2]
+}
+
+proc last_data_line {} {
+ return [lindex [split [string trim $::RESULT] \r\n] end]
+}
+
+proc next_to_last_data_line {} {
+ return [lindex [split [string trim $::RESULT] \r\n] end-1]
+}
+
+###############################################################################
+
+set testTh1Setup {
+ proc initialize_hook_log {} {
+ if {![info exists ::hook_log]} {
+ set ::hook_log ""
+ }
+ }
+
+ proc append_hook_log { args } {
+ initialize_hook_log
+ if {[string length $::hook_log] > 0} {
+ set ::hook_log "$::hook_log "
+ }
+ for {set i 0} {$i < [llength $args]} {set i [expr {$i + 1}]} {
+ set ::hook_log $::hook_log[lindex $args $i]
+ }
+ }
+
+ proc emit_hook_log {} {
+ initialize_hook_log
+ html "\n$::hook_log
\n"
+ }
+
+ proc command_hook {} {
+ append_hook_log command_hook " " $::cmd_name
+ if {$::cmd_name eq "test1"} {
+ puts [repository]; continue
+ } elseif {$::cmd_name eq "test2"} {
+ error "unsupported command"
+ } elseif {$::cmd_name eq "test3"} {
+ emit_hook_log
+ break "TH_BREAK return code"
+ } elseif {$::cmd_name eq "test4"} {
+ emit_hook_log
+ return -code 2 "TH_RETURN return code"
+ } elseif {$::cmd_name eq "timeline"} {
+ if {$::cmd_args eq "custom"} {
+ emit_hook_log
+ return "custom timeline"
+ } else {
+ emit_hook_log
+ error "unsupported timeline"
+ }
+ }
+ }
+
+ proc command_notify {} {
+ append_hook_log command_notify " " $::cmd_name
+ emit_hook_log
+ }
+
+ proc webpage_hook {} {
+ append_hook_log webpage_hook " " $::web_name
+ if {$::web_name eq "test1"} {
+ puts [repository]; continue
+ }
+ }
+
+ proc webpage_notify {} {
+ append_hook_log webpage_notify " " $::web_name
+ emit_hook_log
+ }
+}
+
+###############################################################################
+
+set data [fossil info]
+regexp -line -- {^repository: (.*)$} $data dummy repository
+
+if {[string length $repository] == 0 || ![file exists $repository]} then {
+ error "unable to locate repository"
+}
+
+###############################################################################
+
+saveTh1SetupFile; writeTh1SetupFile $testTh1Setup
+
+###############################################################################
+
+fossil timeline custom; # NOTE: Bad "WHEN" argument.
+test th1-cmd-hooks-1a {[string map [list \r\n \n] [string trim $RESULT]] eq {command_hook timeline
+ERROR: unsupported timeline
++++ no more data (0) +++
+
+command_hook timeline command_notify timeline
}}
+
+###############################################################################
+
+fossil timeline
+test th1-cmd-hooks-2a {[first_data_line] eq {command_hook timeline
}}
+test th1-cmd-hooks-2b {[second_data_line] eq {ERROR: unsupported timeline}}
+test th1-cmd-hooks-2c {[regexp -- {=== \d{4}-\d{2}-\d{2} ===} [third_data_line]]}
+test th1-cmd-hooks-2d {[last_data_line] eq {command_hook timeline command_notify timeline
}}
+
+###############################################################################
+
+fossil test1
+test th1-custom-cmd-1a {[next_to_last_data_line] eq $repository}
+test th1-custom-cmd-1b {[last_data_line] eq {command_hook test1 command_notify test1
}}
+
+###############################################################################
+
+fossil test2
+test th1-custom-cmd-2a {[first_data_line] eq {ERROR: unsupported command}}
+
+###############################################################################
+
+fossil test3
+test th1-custom-cmd-3a {[string trim $RESULT] eq {command_hook test3
}}
+
+###############################################################################
+
+fossil test4
+test th1-custom-cmd-4a {[string trim $RESULT] eq {command_hook test4
}}
+
+###############################################################################
+
+set RESULT [fossil_th1_hook_http $repository /timeline]
+test th1-web-hooks-1a {[regexp {Fossil: Timeline} $RESULT]}
+test th1-web-hooks-1b {[regexp {command_hook http webpage_hook timeline webpage_notify timeline
} $RESULT]}
+
+###############################################################################
+
+set RESULT [fossil_th1_hook_http $repository /test1]
+test th1-custom-web-1a {[next_to_last_data_line] eq $repository}
+test th1-custom-web-1b {[last_data_line] eq {command_hook http webpage_hook test1 webpage_notify test1
}}
+
+###############################################################################
+
+restoreTh1SetupFile
Index: win/Makefile.mingw
==================================================================
--- win/Makefile.mingw
+++ win/Makefile.mingw
@@ -47,10 +47,14 @@
# FOSSIL_ENABLE_JSON = 1
#### Enable HTTPS support via OpenSSL (links to libssl and libcrypto)
#
# FOSSIL_ENABLE_SSL = 1
+
+#### Enable hooks for commands and web pages via TH1
+#
+# FOSSIL_ENABLE_TH1_HOOKS = 1
#### Enable scripting support via Tcl/Tk
#
# FOSSIL_ENABLE_TCL = 1
@@ -176,10 +180,16 @@
# With HTTPS support
ifdef FOSSIL_ENABLE_SSL
TCC += -DFOSSIL_ENABLE_SSL=1
RCC += -DFOSSIL_ENABLE_SSL=1
endif
+
+# With TH1 hook support
+ifdef FOSSIL_ENABLE_TH1_HOOKS
+TCC += -DFOSSIL_ENABLE_TH1_HOOKS=1
+RCC += -DFOSSIL_ENABLE_TH1_HOOKS=1
+endif
# With Tcl support
ifdef FOSSIL_ENABLE_TCL
TCC += -DFOSSIL_ENABLE_TCL=1
RCC += -DFOSSIL_ENABLE_TCL=1
Index: win/Makefile.mingw.mistachkin
==================================================================
--- win/Makefile.mingw.mistachkin
+++ win/Makefile.mingw.mistachkin
@@ -47,10 +47,14 @@
FOSSIL_ENABLE_JSON = 1
#### Enable HTTPS support via OpenSSL (links to libssl and libcrypto)
#
FOSSIL_ENABLE_SSL = 1
+
+#### Enable hooks for commands and web pages via TH1
+#
+FOSSIL_ENABLE_TH1_HOOKS = 1
#### Enable scripting support via Tcl/Tk
#
FOSSIL_ENABLE_TCL = 1
@@ -176,10 +180,16 @@
# With HTTPS support
ifdef FOSSIL_ENABLE_SSL
TCC += -DFOSSIL_ENABLE_SSL=1
RCC += -DFOSSIL_ENABLE_SSL=1
endif
+
+# With TH1 hook support
+ifdef FOSSIL_ENABLE_TH1_HOOKS
+TCC += -DFOSSIL_ENABLE_TH1_HOOKS=1
+RCC += -DFOSSIL_ENABLE_TH1_HOOKS=1
+endif
# With Tcl support
ifdef FOSSIL_ENABLE_TCL
TCC += -DFOSSIL_ENABLE_TCL=1
RCC += -DFOSSIL_ENABLE_TCL=1
Index: win/Makefile.msc
==================================================================
--- win/Makefile.msc
+++ win/Makefile.msc
@@ -21,10 +21,13 @@
# Uncomment to enable JSON API
# FOSSIL_ENABLE_JSON = 1
# Uncomment to enable SSL support
# FOSSIL_ENABLE_SSL = 1
+
+# Uncomment to enable TH1 hooks
+# FOSSIL_ENABLE_TH1_HOOKS = 1
# Uncomment to enable Tcl support
# FOSSIL_ENABLE_TCL = 1
!ifdef FOSSIL_ENABLE_SSL
@@ -79,10 +82,15 @@
TCC = $(TCC) /DFOSSIL_ENABLE_SSL=1
RCC = $(RCC) /DFOSSIL_ENABLE_SSL=1
LIBS = $(LIBS) $(SSLLIB)
LIBDIR = $(LIBDIR) /LIBPATH:$(SSLLIBDIR)
!endif
+
+!ifdef FOSSIL_ENABLE_TH1_HOOKS
+TCC = $(TCC) /DFOSSIL_ENABLE_TH1_HOOKS=1
+RCC = $(RCC) /DFOSSIL_ENABLE_TH1_HOOKS=1
+!endif
!ifdef FOSSIL_ENABLE_TCL
TCC = $(TCC) /DFOSSIL_ENABLE_TCL=1
RCC = $(RCC) /DFOSSIL_ENABLE_TCL=1
TCC = $(TCC) /DFOSSIL_ENABLE_TCL_STUBS=1
Index: win/fossil.rc
==================================================================
--- win/fossil.rc
+++ win/fossil.rc
@@ -101,10 +101,15 @@
VALUE "CommandLineIsUnicode", "Yes\0"
#endif /* defined(BROKEN_MINGW_CMDLINE) */
#if defined(FOSSIL_ENABLE_SSL)
VALUE "SslEnabled", "Yes, " OPENSSL_VERSION_TEXT "\0"
#endif /* defined(FOSSIL_ENABLE_SSL) */
+#if defined(FOSSIL_ENABLE_TH1_HOOKS)
+ VALUE "Th1Hooks", "Yes\0"
+#else
+ VALUE "Th1Hooks", "No\0"
+#endif
#if defined(FOSSIL_ENABLE_TCL)
VALUE "TclEnabled", "Yes, Tcl " TCL_PATCH_LEVEL "\0"
#if defined(USE_TCL_STUBS)
VALUE "UseTclStubsEnabled", "Yes\0"
#else