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