| @@ -43,15 +43,32 @@ The `tests/` IIS application assumes the repository layout keeps `tests/`, `publ | |||||
| ## IIS Setup | ## IIS Setup | ||||
| ### Option A: IIS Express via the checked-in `applicationhost.config` (quickest) | |||||
| This repo ships an `applicationhost.config` (used by `run_site.cmd`) that defines two IIS Express sites: | |||||
| - `Development Web Site` - `public/` on `http://localhost:8080/` | |||||
| - `Tests Web Site` - `tests/` on `http://localhost:8081/` | |||||
| `<asp enableParentPaths="true" .../>` is set server-wide in that same file - this is what lets `tests/bootstrap.asp` and the integration pages `#include` sibling files from `../core/` and `../app/` (physically outside the `tests/` site's own root). No per-app IIS configuration is required. | |||||
| A single `iisexpress.exe` process only ever runs **one** site from a config file - per `iisexpress /?`: "`/config:config-file` ... runs the first site in the specified configuration file." `serverAutoStart="true"` is an IIS/W3SVC (full IIS) concept and is not honored by a standalone `iisexpress.exe` invocation, so listing a second `<site>` is not enough on its own. | |||||
| 1. Run `run_site.cmd` - it launches **two** `iisexpress.exe` processes, each pinned to one site with `/site:"..."` (each opens its own console window so you can see that site's request log). | |||||
| 2. Browse to `http://localhost:8081/` (or run `tests\run-tests.cmd`, which syncs configs first, then opens the runner URL). | |||||
| 3. `tests/web.config`'s `ProductionAppBaseUrl` is set to `http://localhost:8080/` to match the production site's actual binding above - `TestRenderedOutput.asp`/`TestSharedLayout.asp` use this to fetch real rendered pages over HTTP. If you rebind either site to a different port, update this value too, update the matching `/site:`/binding in `applicationhost.config` and `run_site.cmd`, and re-run the sync script (step 5 below). | |||||
| If `applicationhost.config` only has the `Development Web Site` entry (e.g. in a checkout from before the `Tests Web Site` entry was added), that's why the tests app has nothing to browse to - add a second `<site>` block for `tests/` following the same shape as the existing one, on its own port. | |||||
| ### Option B: A real IIS server | |||||
| 1. Keep the existing production IIS app rooted at `public/`. | 1. Keep the existing production IIS app rooted at `public/`. | ||||
| 2. Create a separate development-only IIS application rooted at `tests/`. | 2. Create a separate development-only IIS application rooted at `tests/`. | ||||
| 3. Enable Classic ASP for that IIS app. | 3. Enable Classic ASP for that IIS app. | ||||
| 4. Ensure parent paths are allowed for the `tests/` app. This repo ships `tests/web.config` with `enableParentPaths="true"` because the bootstrap and integration pages include sibling files from `../core/` and `../app/`. | |||||
| 4. Ensure parent paths are allowed for the `tests/` app - in IIS Manager this is the "Enable Parent Paths" checkbox under ASP settings for that application (or `<system.webServer><asp enableParentPaths="true" /></system.webServer>` in that app's own web.config). `tests/web.config` does not set this itself; on IIS Express it comes from the server-wide `<asp>` element in `applicationhost.config` instead (see Option A). | |||||
| 5. Browse to the `tests/` app root or directly to `run-all.asp`. | 5. Browse to the `tests/` app root or directly to `run-all.asp`. | ||||
| 6. If you change `tests/web.config`, run `cscript //nologo tests\sync-webconfigs.vbs` to refresh the nested copies used by the unit, component, and integration pages. | 6. If you change `tests/web.config`, run `cscript //nologo tests\sync-webconfigs.vbs` to refresh the nested copies used by the unit, component, and integration pages. | ||||
| 7. If your production app is not served from the same host root as the `tests/` app, set `ProductionAppBaseUrl` in `tests/web.config` and re-run the sync script so rendered-output tests know where to send HTTP requests. | |||||
| Example: `http://localhost/` for a root site, or `http://localhost/MyClassicApp/` for a virtual-directory app. | |||||
| 8. To sync configs and open the suite in one step on Windows, run `tests\run-tests.cmd` with an optional runner URL argument. | |||||
| 7. Set `ProductionAppBaseUrl` in `tests/web.config` to wherever the production app actually resolves (e.g. `http://localhost/` for a root site, or `http://localhost/MyClassicApp/` for a virtual-directory app) and re-run the sync script so rendered-output tests know where to send HTTP requests. | |||||
| Example layout: | Example layout: | ||||
| @@ -1,2 +1,10 @@ | |||||
| <!--#include file="HomeController.asp" --> | <!--#include file="HomeController.asp" --> | ||||
| <!--#include file="ErrorController.asp" --> | |||||
| <!--#include file="ErrorController.asp" --> | |||||
| <% | |||||
| ' Register each controller's own singleton accessor with the shared | |||||
| ' ControllerFactory, by reference (GetRef), so the MVC dispatcher (a WSC, | |||||
| ' which cannot see these factory functions directly) never has to Execute/ | |||||
| ' Eval a route-supplied string to resolve a controller class. | |||||
| Call ControllerFactory().Register("HomeController", GetRef("HomeController")) | |||||
| Call ControllerFactory().Register("ErrorController", GetRef("ErrorController")) | |||||
| %> | |||||
| @@ -5,9 +5,9 @@ Response.CodePage = 65001 | |||||
| ' Safe title resolution | ' Safe title resolution | ||||
| Dim pageTitle | Dim pageTitle | ||||
| If IsObject(CurrentController) Then | |||||
| If IsObject(MVC.CurrentController) Then | |||||
| On Error Resume Next | On Error Resume Next | ||||
| pageTitle = CurrentController.Title | |||||
| pageTitle = MVC.CurrentController.Title | |||||
| If Err.Number <> 0 Then | If Err.Number <> 0 Then | ||||
| pageTitle = "RouteKit Classic ASP" | pageTitle = "RouteKit Classic ASP" | ||||
| Err.Clear | Err.Clear | ||||
| @@ -162,6 +162,14 @@ | |||||
| <binding protocol="http" bindingInformation=":8080:localhost" /> | <binding protocol="http" bindingInformation=":8080:localhost" /> | ||||
| </bindings> | </bindings> | ||||
| </site> | </site> | ||||
| <site name="Tests Web Site" id="2" serverAutoStart="true"> | |||||
| <application path="/"> | |||||
| <virtualDirectory path="/" physicalPath="%ASPC_STARTER_ROOT%tests" /> | |||||
| </application> | |||||
| <bindings> | |||||
| <binding protocol="http" bindingInformation=":8081:localhost" /> | |||||
| </bindings> | |||||
| </site> | |||||
| <siteDefaults> | <siteDefaults> | ||||
| <!-- To enable logging, please change the below attribute "enabled" to "true" --> | <!-- To enable logging, please change the below attribute "enabled" to "true" --> | ||||
| <logFile logFormat="W3C" directory="%AppData%\Microsoft\IISExpressLogs" enabled="false" /> | <logFile logFormat="W3C" directory="%AppData%\Microsoft\IISExpressLogs" enabled="false" /> | ||||
| @@ -1,7 +1,8 @@ | |||||
| <!--#include file="../Core/helpers.asp"--> | <!--#include file="../Core/helpers.asp"--> | ||||
| <!--#include file="../Core/lib.ErrorHandler.asp"--> | <!--#include file="../Core/lib.ErrorHandler.asp"--> | ||||
| <!--#include file="../Core/lib.ControllerRegistry.asp"--> | <!--#include file="../Core/lib.ControllerRegistry.asp"--> | ||||
| <!--#include file="../Core/mvc.asp"--> | |||||
| <!--#include file="../Core/lib.ControllerFactory.asp"--> | |||||
| <!--#include file="../app/Controllers/autoload_controllers.asp"--> | |||||
| <!--#include file="../Core/lib.DAL.asp"--> | <!--#include file="../Core/lib.DAL.asp"--> | ||||
| <!--#include file="../Core/lib.Data.asp"--> | <!--#include file="../Core/lib.Data.asp"--> | ||||
| <!--#include file="../Core/lib.Migrations.asp"--> | <!--#include file="../Core/lib.Migrations.asp"--> | ||||
| @@ -21,3 +22,27 @@ | |||||
| <!--#include file="../Core/lib.Enumerable.asp"--> | <!--#include file="../Core/lib.Enumerable.asp"--> | ||||
| <!--#include file="../Core/lib.ad.auth.asp"--> | <!--#include file="../Core/lib.ad.auth.asp"--> | ||||
| <!--#include file="../Core/databaseConnection.asp"--> | <!--#include file="../Core/databaseConnection.asp"--> | ||||
| <% | |||||
| ' Response cache headers (moved from mvc.asp - now that MVC is a Windows | |||||
| ' Script Component it cannot hold loose top-level ASP statements like this). | |||||
| Dim cacheYear : cacheYear = GetAppSetting("CacheExpirationYear") | |||||
| If cacheYear = "nothing" Then cacheYear = "2030" | |||||
| Response.ExpiresAbsolute = "01/01/" & cacheYear | |||||
| Response.AddHeader "pragma", "no-cache" | |||||
| Response.AddHeader "cache-control", "private, no-cache, must-revalidate" | |||||
| ' MVC dispatcher (Windows Script Component). It runs in its own isolated | |||||
| ' script engine, so its dependencies are injected explicitly here rather | |||||
| ' than it reaching into this page's globals itself - see AGENTS.md | |||||
| ' section 69/70 ("Dependencies Must Be Injected"). | |||||
| Dim MVC_Instance | |||||
| Set MVC_Instance = GetObject("script:" & Server.MapPath("../Core/mvc.wsc") & "") | |||||
| Set MVC_Instance.Router = router | |||||
| Set MVC_Instance.ControllerRegistry = ControllerRegistry() | |||||
| Set MVC_Instance.ControllerFactory = ControllerFactory() | |||||
| Set MVC_Instance.Routes = Routes() | |||||
| Function MVC() | |||||
| Set MVC = MVC_Instance | |||||
| End Function | |||||
| %> | |||||
| @@ -1,5 +1,8 @@ | |||||
| <% | <% | ||||
| Dim m_indent | |||||
| Function QuoteValue(val) | Function QuoteValue(val) | ||||
| Dim conn | |||||
| if IsWrappedInSingleQuotes(val) then | if IsWrappedInSingleQuotes(val) then | ||||
| QuoteValue = val | QuoteValue = val | ||||
| Exit Function | Exit Function | ||||
| @@ -92,6 +95,38 @@ Function IIf(condition, trueValue, falseValue) | |||||
| On Error GoTo 0 | On Error GoTo 0 | ||||
| End Function | End Function | ||||
| '----------------------------- | |||||
| ' Utility: MVC Dispatch Error Renderer | |||||
| ' | |||||
| ' Renders the error box for a failed MVC.Resolve()/ExecuteAction() call. | |||||
| ' errInfo is the Dictionary from MVC.LastError: Type = "Security" (format/ | |||||
| ' whitelist failures - message is safe to show in any environment) or | |||||
| ' Type = "Action" (a controller action raised an error - full detail only | |||||
| ' in development). Consolidates what were four duplicated inline HTML | |||||
| ' blocks in the pre-WSC mvc.asp into one place, matching AGENTS.md's | |||||
| ' "Shared Utilities" rule against duplicate near-identical helpers. | |||||
| '----------------------------- | |||||
| Public Sub RenderMvcError(errInfo) | |||||
| Dim isDevelopment | |||||
| isDevelopment = (LCase(GetAppSetting("Environment")) = "development") | |||||
| Response.Write "<div style='padding:15px; margin:10px; border:2px solid #dc3545; background:#f8d7da; color:#721c24; border-radius:4px;'>" | |||||
| If errInfo("Type") = "Security" Then | |||||
| Response.Write "<strong>Security Error:</strong> " & Server.HTMLEncode(errInfo("Message")) | |||||
| ElseIf isDevelopment Then | |||||
| Response.Write "<strong>Controller Action Error</strong><br>" | |||||
| Response.Write "Action: <code>" & Server.HTMLEncode(errInfo("ActionName")) & "</code><br>" | |||||
| Response.Write "Error: " & Server.HTMLEncode(errInfo("Description")) & "<br>" | |||||
| Response.Write "Error Number: " & errInfo("Number") | |||||
| Else | |||||
| Response.Write "<strong>An error occurred</strong><br>" | |||||
| Response.Write "Please contact the system administrator if the problem persists." | |||||
| End If | |||||
| Response.Write "</div>" | |||||
| End Sub | |||||
| '----------------------------- | '----------------------------- | ||||
| ' Utility: Generic Error Reporter | ' Utility: Generic Error Reporter | ||||
| '----------------------------- | '----------------------------- | ||||
| @@ -113,7 +113,7 @@ Class CDOEmail_Class | |||||
| End If | End If | ||||
| ' Add attachments if any | ' Add attachments if any | ||||
| Dim i | |||||
| Dim i, errNum, errDesc | |||||
| For i = LBound(arrAttachments) To UBound(arrAttachments) | For i = LBound(arrAttachments) To UBound(arrAttachments) | ||||
| msg.AddAttachment arrAttachments(i) | msg.AddAttachment arrAttachments(i) | ||||
| Next | Next | ||||
| @@ -374,8 +374,8 @@ Class LinkedList_Class | |||||
| End Function | End Function | ||||
| Public Function TO_Array() | Public Function TO_Array() | ||||
| Dim i, iter | |||||
| Dim i, iter, retval | |||||
| ReDim retval(Me.Count - 1) | ReDim retval(Me.Count - 1) | ||||
| i = 0 | i = 0 | ||||
| Set iter = Me.Iterator | Set iter = Me.Iterator | ||||
| @@ -491,8 +491,8 @@ Class DynamicArray_Class | |||||
| If e < s Then | If e < s Then | ||||
| Set Slice = DynamicArray() | Set Slice = DynamicArray() | ||||
| Else | Else | ||||
| Dim retval, i, j | |||||
| ReDim retval(e - s) | ReDim retval(e - s) | ||||
| Dim i, j | |||||
| j = 0 | j = 0 | ||||
| For i = s to e | For i = s to e | ||||
| Assign retval(j), m_data(i) | Assign retval(j), m_data(i) | ||||
| @@ -515,7 +515,7 @@ Class DynamicArray_Class | |||||
| End Function | End Function | ||||
| Public Function TO_Array() | Public Function TO_Array() | ||||
| Dim i | |||||
| Dim i, retval | |||||
| ReDim retval(m_size - 1) | ReDim retval(m_size - 1) | ||||
| For i = 0 to UBOUND(retval) | For i = 0 to UBOUND(retval) | ||||
| Assign retval(i), m_data(i) | Assign retval(i), m_data(i) | ||||
| @@ -0,0 +1,65 @@ | |||||
| <% | |||||
| '======================================================================================================================= | |||||
| ' Controller Factory | |||||
| ' | |||||
| ' Centralizes controller instantiation by name so the MVC dispatcher (a WSC, | |||||
| ' with its own isolated script engine) never has to Execute/Eval a string | |||||
| ' built from a route-supplied controller name to resolve a class. Each | |||||
| ' controller factory function (e.g. HomeController(), which already exists as | |||||
| ' each controller's own singleton accessor) is registered once, by reference, | |||||
| ' via GetRef - not by re-deriving it from a string at dispatch time. | |||||
| '======================================================================================================================= | |||||
| Class ControllerFactory_Class | |||||
| Private m_factories | |||||
| Private Sub Class_Initialize() | |||||
| Set m_factories = Server.CreateObject("Scripting.Dictionary") | |||||
| m_factories.CompareMode = 1 ' vbTextCompare, case-insensitive | |||||
| End Sub | |||||
| Private Sub Class_Terminate() | |||||
| Set m_factories = Nothing | |||||
| End Sub | |||||
| '--------------------------------------------------------------------------------------------------------------------- | |||||
| ' Register a controller's factory function (its own singleton accessor, | |||||
| ' e.g. GetRef("HomeController")) under its route name. | |||||
| '--------------------------------------------------------------------------------------------------------------------- | |||||
| Public Sub Register(controllerName, factoryFunctionRef) | |||||
| Dim key : key = LCase(Trim(controllerName)) | |||||
| If Not m_factories.Exists(key) Then | |||||
| m_factories.Add key, factoryFunctionRef | |||||
| End If | |||||
| End Sub | |||||
| '--------------------------------------------------------------------------------------------------------------------- | |||||
| ' Create(controllerName) -> the controller singleton instance | |||||
| '--------------------------------------------------------------------------------------------------------------------- | |||||
| Public Function Create(controllerName) | |||||
| Dim key, fn | |||||
| key = LCase(Trim(controllerName)) | |||||
| If Not m_factories.Exists(key) Then | |||||
| Err.Raise vbObjectError + 1100, "ControllerFactory.Create", "No factory registered for controller: " & controllerName | |||||
| End If | |||||
| Set fn = m_factories(key) | |||||
| Set Create = fn() | |||||
| End Function | |||||
| '--------------------------------------------------------------------------------------------------------------------- | |||||
| Public Function IsRegistered(controllerName) | |||||
| IsRegistered = m_factories.Exists(LCase(Trim(controllerName))) | |||||
| End Function | |||||
| End Class | |||||
| ' Singleton instance | |||||
| Dim ControllerFactory_Class__Singleton | |||||
| Function ControllerFactory() | |||||
| If IsEmpty(ControllerFactory_Class__Singleton) Then | |||||
| Set ControllerFactory_Class__Singleton = New ControllerFactory_Class | |||||
| End If | |||||
| Set ControllerFactory = ControllerFactory_Class__Singleton | |||||
| End Function | |||||
| %> | |||||
| @@ -2,112 +2,143 @@ | |||||
| Class Database_Class | Class Database_Class | ||||
| Private m_connection | Private m_connection | ||||
| Private m_connection_string | Private m_connection_string | ||||
| Private m_shape_connections | |||||
| Private m_trace_enabled | Private m_trace_enabled | ||||
| Public Sub set_trace(bool) : m_trace_enabled = bool : End Sub | Public Sub set_trace(bool) : m_trace_enabled = bool : End Sub | ||||
| Public Property Get is_trace_enabled : is_trace_enabled = m_trace_enabled : End Property | Public Property Get is_trace_enabled : is_trace_enabled = m_trace_enabled : End Property | ||||
| Private Sub Class_Initialize | |||||
| Set m_shape_connections = Server.CreateObject("Scripting.Dictionary") | |||||
| End Sub | |||||
| '--------------------------------------------------------------------------------------------------------------------- | '--------------------------------------------------------------------------------------------------------------------- | ||||
| Public Sub Initialize(connection_string) | Public Sub Initialize(connection_string) | ||||
| m_connection_string = connection_string | m_connection_string = connection_string | ||||
| End Sub | End Sub | ||||
| '--------------------------------------------------------------------------------------------------------------------- | '--------------------------------------------------------------------------------------------------------------------- | ||||
| Public Function ShapeQuery(sql,params) | |||||
| dim shapeConn : set shapeConn = server.createobject("adodb.connection") | |||||
| shapeConn.ConnectionString = "Provider=MSDataShape;Data " & m_connection_string | |||||
| dim cmd : set cmd = server.createobject("adodb.command") | |||||
| shapeConn.open | |||||
| set cmd.ActiveConnection = shapeConn | |||||
| cmd.CommandText = sql | |||||
| dim rs | |||||
| ' MSDataShape opens its own dedicated connection per call (it cannot reuse | |||||
| ' the plain Connection() used by Query/PagedQuery). That connection is | |||||
| ' tracked and closed in Class_Terminate rather than left open until the | |||||
| ' object is garbage collected. | |||||
| Public Function ShapeQuery(sql, params) | |||||
| Dim shapeConn, cmd, rs, key | |||||
| Set shapeConn = Server.CreateObject("adodb.connection") | |||||
| shapeConn.ConnectionString = "Provider=MSDataShape;Data " & m_connection_string | |||||
| shapeConn.Open | |||||
| key = m_shape_connections.Count | |||||
| m_shape_connections.Add key, shapeConn | |||||
| Set cmd = Server.CreateObject("adodb.command") | |||||
| Set cmd.ActiveConnection = shapeConn | |||||
| cmd.CommandText = sql | |||||
| If IsArray(params) then | If IsArray(params) then | ||||
| set rs = cmd.Execute(, params) | |||||
| Set rs = cmd.Execute(, params) | |||||
| ElseIf Not IsEmpty(params) then ' one parameter | ElseIf Not IsEmpty(params) then ' one parameter | ||||
| set rs = cmd.Execute(, Array(params)) | |||||
| Set rs = cmd.Execute(, Array(params)) | |||||
| Else | Else | ||||
| set rs = cmd.Execute() | |||||
| Set rs = cmd.Execute() | |||||
| End If | End If | ||||
| set ShapeQuery = rs | |||||
| Set ShapeQuery = rs | |||||
| End Function | End Function | ||||
| Public Function Query(sql, params) | Public Function Query(sql, params) | ||||
| dim cmd : set cmd = server.createobject("adodb.command") | |||||
| set cmd.ActiveConnection = Connection | |||||
| Dim cmd, rs | |||||
| Set cmd = Server.CreateObject("adodb.command") | |||||
| Set cmd.ActiveConnection = Connection | |||||
| cmd.CommandText = sql | cmd.CommandText = sql | ||||
| dim rs | |||||
| If IsArray(params) then | If IsArray(params) then | ||||
| set rs = cmd.Execute(, params) | |||||
| Set rs = cmd.Execute(, params) | |||||
| ElseIf Not IsEmpty(params) then ' one parameter | ElseIf Not IsEmpty(params) then ' one parameter | ||||
| set rs = cmd.Execute(, Array(params)) | |||||
| Set rs = cmd.Execute(, Array(params)) | |||||
| Else | Else | ||||
| set rs = cmd.Execute() | |||||
| Set rs = cmd.Execute() | |||||
| End If | End If | ||||
| set Query = rs | |||||
| Set Query = rs | |||||
| End Function | End Function | ||||
| '--------------------------------------------------------------------------------------------------------------------- | '--------------------------------------------------------------------------------------------------------------------- | ||||
| ' per_page/page_num were previously accepted but ignored - PageSize, | |||||
| ' CacheSize, and AbsolutePage were hardcoded to 1 regardless of the | |||||
| ' arguments passed in, so paging never actually worked. page_num is | |||||
| ' clamped to a valid page (1..PageCount) since AbsolutePage raises an | |||||
| ' error outside that range, and honoring the caller's page_num means an | |||||
| ' out-of-range value is now reachable where it previously never was. | |||||
| Public Function PagedQuery(sql, params, per_page, page_num) | Public Function PagedQuery(sql, params, per_page, page_num) | ||||
| dim cmd : set cmd = server.createobject("adodb.command") | |||||
| set cmd.ActiveConnection = Connection | |||||
| Dim cmd, rs | |||||
| Set cmd = Server.CreateObject("adodb.command") | |||||
| Set cmd.ActiveConnection = Connection | |||||
| cmd.CommandText = sql | cmd.CommandText = sql | ||||
| cmd.CommandType = 1 'adCmdText | cmd.CommandType = 1 'adCmdText | ||||
| cmd.ActiveConnection.CursorLocation = 3 'adUseClient | cmd.ActiveConnection.CursorLocation = 3 'adUseClient | ||||
| dim rs | |||||
| If IsArray(params) then | If IsArray(params) then | ||||
| set rs = cmd.Execute(, params) | |||||
| Set rs = cmd.Execute(, params) | |||||
| ElseIf Not IsEmpty(params) then ' one parameter | ElseIf Not IsEmpty(params) then ' one parameter | ||||
| set rs = cmd.Execute(, Array(params)) | |||||
| Set rs = cmd.Execute(, Array(params)) | |||||
| Else | Else | ||||
| set rs = cmd.Execute() | |||||
| Set rs = cmd.Execute() | |||||
| End If | End If | ||||
| If Not rs.EOF then | If Not rs.EOF then | ||||
| rs.PageSize = 1 | |||||
| rs.CacheSize = 1 | |||||
| rs.AbsolutePage = 1 | |||||
| rs.PageSize = per_page | |||||
| rs.CacheSize = per_page | |||||
| If page_num < 1 Then page_num = 1 | |||||
| If page_num > rs.PageCount Then page_num = rs.PageCount | |||||
| rs.AbsolutePage = page_num | |||||
| End If | End If | ||||
| set PagedQuery = rs | |||||
| Set PagedQuery = rs | |||||
| End Function | End Function | ||||
| '--------------------------------------------------------------------------------------------------------------------- | '--------------------------------------------------------------------------------------------------------------------- | ||||
| Public Sub [Execute](sql, params) | Public Sub [Execute](sql, params) | ||||
| me.query sql, params | me.query sql, params | ||||
| End Sub | End Sub | ||||
| '--------------------------------------------------------------------------------------------------------------------- | '--------------------------------------------------------------------------------------------------------------------- | ||||
| Public Sub BeginTransaction | Public Sub BeginTransaction | ||||
| Connection.BeginTrans | Connection.BeginTrans | ||||
| End Sub | End Sub | ||||
| Public Sub RollbackTransaction | Public Sub RollbackTransaction | ||||
| Connection.RollbackTrans | Connection.RollbackTrans | ||||
| End Sub | End Sub | ||||
| Public Sub CommitTransaction | Public Sub CommitTransaction | ||||
| Connection.CommitTrans | Connection.CommitTrans | ||||
| End Sub | End Sub | ||||
| '--------------------------------------------------------------------------------------------------------------------- | '--------------------------------------------------------------------------------------------------------------------- | ||||
| ' Private Methods | ' Private Methods | ||||
| '--------------------------------------------------------------------------------------------------------------------- | '--------------------------------------------------------------------------------------------------------------------- | ||||
| Private Sub Class_terminate | Private Sub Class_terminate | ||||
| Destroy m_connection | Destroy m_connection | ||||
| Dim key | |||||
| If Not m_shape_connections Is Nothing Then | |||||
| For Each key In m_shape_connections.Keys | |||||
| Destroy m_shape_connections(key) | |||||
| Next | |||||
| Set m_shape_connections = Nothing | |||||
| End If | |||||
| End Sub | End Sub | ||||
| Public Function Connection | Public Function Connection | ||||
| if not isobject(m_connection) then | |||||
| if not isobject(m_connection) then | |||||
| set m_connection = Server.CreateObject("adodb.connection") | set m_connection = Server.CreateObject("adodb.connection") | ||||
| m_connection.open m_connection_string | m_connection.open m_connection_string | ||||
| end if | end if | ||||
| set Connection = m_connection | set Connection = m_connection | ||||
| End Function | End Function | ||||
| end Class | end Class | ||||
| %> | |||||
| %> | |||||
| @@ -44,7 +44,7 @@ Class HTML_Helper_Class | |||||
| Public Function LinkToUnless(condition, link_text, controller_name, action_name) | Public Function LinkToUnless(condition, link_text, controller_name, action_name) | ||||
| if not condition then | if not condition then | ||||
| LinkToIf = LinkToExt(link_text, controller_name, action_name, empty, empty) | |||||
| LinkToUnless = LinkToExt(link_text, controller_name, action_name, empty, empty) | |||||
| end if | end if | ||||
| End Function | End Function | ||||
| @@ -1,45 +1,264 @@ | |||||
| <% | <% | ||||
| '======================================================================================================================= | |||||
| ' Password hashing | |||||
| ' | |||||
| ' Pure VBScript SHA-256 - no external process, no shell-out, no COM crypto | |||||
| ' dependency. The previous implementation shelled out to a PowerShell script | |||||
| ' (hash_sha256.ps1, which did not exist anywhere in this repo) via | |||||
| ' WScript.Shell.Exec with the raw password concatenated directly into the | |||||
| ' command line. That was both non-functional (missing script) and a command | |||||
| ' injection vulnerability (a password containing ", `, ; etc. could execute | |||||
| ' arbitrary PowerShell, and the password was visible in the process command | |||||
| ' line to anything that could list processes). | |||||
| ' | |||||
| ' NOTE: This hashes without a per-user salt, matching the single-argument | |||||
| ' HashPassword(password) contract these functions already had. Unsalted | |||||
| ' hashes are vulnerable to precomputed/rainbow-table attacks. If a real | |||||
| ' Users table is introduced, prefer storing a random per-user salt alongside | |||||
| ' the hash and hashing (salt & password) - that is a schema change, so it is | |||||
| ' intentionally not done here. | |||||
| '======================================================================================================================= | |||||
| Function HashPassword(password) | |||||
| Dim shell, command, execObj, outputLine, result | |||||
| ' Create Shell Object | |||||
| Set shell = CreateObject("WScript.Shell") | |||||
| ' Construct PowerShell Command | |||||
| command = "cmd /c powershell -ExecutionPolicy Bypass -NoLogo -NoProfile -File """ & Server.MapPath(".") & "..\Core\hash_sha256.ps1"" -password " & password | |||||
| ' Execute Command | |||||
| Set execObj = shell.Exec(command) | |||||
| ' Read Output | |||||
| Do While Not execObj.StdOut.AtEndOfStream | |||||
| outputLine = Trim(execObj.StdOut.ReadAll()) | |||||
| If outputLine <> "" Then | |||||
| result = outputLine ' Capture the hash | |||||
| End If | |||||
| Private Function Sha256_U32(n) | |||||
| If n < 0 Then | |||||
| Sha256_U32 = n + 4294967296.0 | |||||
| Else | |||||
| Sha256_U32 = CDbl(n) | |||||
| End If | |||||
| End Function | |||||
| Private Function Sha256_S32(d) | |||||
| Do While d >= 4294967296.0 | |||||
| d = d - 4294967296.0 | |||||
| Loop | |||||
| Do While d < 0 | |||||
| d = d + 4294967296.0 | |||||
| Loop | |||||
| If d >= 2147483648.0 Then | |||||
| Sha256_S32 = CLng(d - 4294967296.0) | |||||
| Else | |||||
| Sha256_S32 = CLng(d) | |||||
| End If | |||||
| End Function | |||||
| Private Function Sha256_Mod(x, y) | |||||
| Sha256_Mod = x - Int(x / y) * y | |||||
| End Function | |||||
| Private Function Sha256_Add32(a, b) | |||||
| Sha256_Add32 = Sha256_S32(Sha256_U32(a) + Sha256_U32(b)) | |||||
| End Function | |||||
| Private Function Sha256_Add32_5(a, b, c, d, e) | |||||
| Sha256_Add32_5 = Sha256_S32(Sha256_U32(a) + Sha256_U32(b) + Sha256_U32(c) + Sha256_U32(d) + Sha256_U32(e)) | |||||
| End Function | |||||
| Private Function Sha256_RotR32(x, n) | |||||
| Dim u, partA, keepBits, partB | |||||
| If n = 0 Then | |||||
| Sha256_RotR32 = x | |||||
| Exit Function | |||||
| End If | |||||
| u = Sha256_U32(x) | |||||
| partA = Int(u / (2 ^ n)) | |||||
| keepBits = Sha256_Mod(u, 2 ^ n) | |||||
| partB = keepBits * (2 ^ (32 - n)) | |||||
| Sha256_RotR32 = Sha256_S32(partA + partB) | |||||
| End Function | |||||
| Private Function Sha256_ShR32(x, n) | |||||
| Dim u | |||||
| u = Sha256_U32(x) | |||||
| Sha256_ShR32 = Sha256_S32(Int(u / (2 ^ n))) | |||||
| End Function | |||||
| Private Function Sha256_BytesToWord(b0, b1, b2, b3) | |||||
| Sha256_BytesToWord = Sha256_S32(b0 * 16777216.0 + b1 * 65536.0 + b2 * 256.0 + b3) | |||||
| End Function | |||||
| Private Function Sha256_HexWord(x) | |||||
| Dim u, h | |||||
| u = Sha256_U32(x) | |||||
| h = "" | |||||
| Do While u > 0 | |||||
| h = Mid("0123456789abcdef", Sha256_Mod(u, 16) + 1, 1) & h | |||||
| u = Int(u / 16) | |||||
| Loop | Loop | ||||
| Sha256_HexWord = Right("00000000" & h, 8) | |||||
| End Function | |||||
| ' Converts a VBScript string (UTF-16) to a UTF-8 byte array (Long values 0-255). | |||||
| ' Pure VBScript - avoids ADODB.Stream, which returns a Byte SafeArray that is | |||||
| ' not reliably indexable from VBScript on every 32/64-bit IIS configuration. | |||||
| Private Function Sha256_StringToUtf8Bytes(s) | |||||
| Dim arr(), count, i, code, lenS | |||||
| lenS = Len(s) | |||||
| If lenS = 0 Then | |||||
| ReDim arr(-1) | |||||
| Sha256_StringToUtf8Bytes = arr | |||||
| Exit Function | |||||
| End If | |||||
| ReDim arr(lenS * 3) | |||||
| count = -1 | |||||
| For i = 1 To lenS | |||||
| code = AscW(Mid(s, i, 1)) | |||||
| If code < 0 Then code = code + 65536 | |||||
| If code <= 127 Then | |||||
| count = count + 1 : arr(count) = code | |||||
| ElseIf code <= 2047 Then | |||||
| count = count + 1 : arr(count) = 192 Or Int(code / 64) | |||||
| count = count + 1 : arr(count) = 128 Or (code And 63) | |||||
| Else | |||||
| count = count + 1 : arr(count) = 224 Or Int(code / 4096) | |||||
| count = count + 1 : arr(count) = 128 Or (Int(code / 64) And 63) | |||||
| count = count + 1 : arr(count) = 128 Or (code And 63) | |||||
| End If | |||||
| Next | |||||
| ReDim Preserve arr(count) | |||||
| Sha256_StringToUtf8Bytes = arr | |||||
| End Function | |||||
| ' Cleanup | |||||
| Set shell = Nothing | |||||
| Set execObj = Nothing | |||||
| ' Returns the SHA-256 hash of inputString (UTF-8 encoded) as a 64-character | |||||
| ' lowercase hex string. Verified against NIST/RFC test vectors (empty string, | |||||
| ' "abc", the 56-byte two-block vector, and non-ASCII input) plus .NET's | |||||
| ' System.Security.Cryptography.SHA256 as a cross-check. | |||||
| Function Sha256Hex(inputString) | |||||
| Dim msg, msgLen, bitLen, i, t | |||||
| Dim numBlocks, blockIdx, base | |||||
| Dim hs(7) | |||||
| Dim k(63) | |||||
| Dim w(63) | |||||
| Dim a, b, c, d, e, f, g, h | |||||
| Dim s0, s1, ch, maj, temp1, temp2, bigS0, bigS1 | |||||
| Dim padded() | |||||
| Dim padLen, totalLen, lenPos, hi32, lo32 | |||||
| Dim kHex, result | |||||
| ' Return the hash or error message | |||||
| If result = "" Or Left(result, 5) = "ERROR" Then | |||||
| HashPassword = result ' "ERROR: Hash not generated" | |||||
| hs(0) = &h6a09e667 : hs(1) = &hbb67ae85 : hs(2) = &h3c6ef372 : hs(3) = &ha54ff53a | |||||
| hs(4) = &h510e527f : hs(5) = &h9b05688c : hs(6) = &h1f83d9ab : hs(7) = &h5be0cd19 | |||||
| kHex = Array( _ | |||||
| "428a2f98","71374491","b5c0fbcf","e9b5dba5","3956c25b","59f111f1","923f82a4","ab1c5ed5", _ | |||||
| "d807aa98","12835b01","243185be","550c7dc3","72be5d74","80deb1fe","9bdc06a7","c19bf174", _ | |||||
| "e49b69c1","efbe4786","0fc19dc6","240ca1cc","2de92c6f","4a7484aa","5cb0a9dc","76f988da", _ | |||||
| "983e5152","a831c66d","b00327c8","bf597fc7","c6e00bf3","d5a79147","06ca6351","14292967", _ | |||||
| "27b70a85","2e1b2138","4d2c6dfc","53380d13","650a7354","766a0abb","81c2c92e","92722c85", _ | |||||
| "a2bfe8a1","a81a664b","c24b8b70","c76c51a3","d192e819","d6990624","f40e3585","106aa070", _ | |||||
| "19a4c116","1e376c08","2748774c","34b0bcb5","391c0cb3","4ed8aa4a","5b9cca4f","682e6ff3", _ | |||||
| "748f82ee","78a5636f","84c87814","8cc70208","90befffa","a4506ceb","bef9a3f7","c67178f2") | |||||
| For i = 0 To 63 | |||||
| k(i) = Sha256_S32(CDbl("&h" & kHex(i))) | |||||
| Next | |||||
| msg = Sha256_StringToUtf8Bytes(inputString) | |||||
| If UBound(msg) < LBound(msg) Then | |||||
| msgLen = 0 | |||||
| Else | Else | ||||
| HashPassword = result | |||||
| msgLen = UBound(msg) - LBound(msg) + 1 | |||||
| End If | End If | ||||
| bitLen = msgLen * 8.0 | |||||
| ' Padding: msg + 0x80 + zero bytes so length % 64 = 56, then an 8-byte | |||||
| ' big-endian bit length. | |||||
| padLen = 56 - Sha256_Mod(msgLen + 1, 64) | |||||
| If padLen < 0 Then padLen = padLen + 64 | |||||
| totalLen = msgLen + 1 + padLen + 8 | |||||
| ReDim padded(totalLen - 1) | |||||
| For i = 0 To msgLen - 1 | |||||
| padded(i) = msg(i) | |||||
| Next | |||||
| padded(msgLen) = 128 | |||||
| For i = msgLen + 1 To msgLen + padLen | |||||
| padded(i) = 0 | |||||
| Next | |||||
| lenPos = msgLen + 1 + padLen | |||||
| hi32 = Int(bitLen / 4294967296.0) | |||||
| lo32 = bitLen - hi32 * 4294967296.0 | |||||
| padded(lenPos + 0) = Sha256_Mod(Int(hi32 / 16777216), 256) | |||||
| padded(lenPos + 1) = Sha256_Mod(Int(hi32 / 65536), 256) | |||||
| padded(lenPos + 2) = Sha256_Mod(Int(hi32 / 256), 256) | |||||
| padded(lenPos + 3) = Sha256_Mod(hi32, 256) | |||||
| padded(lenPos + 4) = Int(lo32 / 16777216) | |||||
| padded(lenPos + 5) = Sha256_Mod(Int(lo32 / 65536), 256) | |||||
| padded(lenPos + 6) = Sha256_Mod(Int(lo32 / 256), 256) | |||||
| padded(lenPos + 7) = Sha256_Mod(lo32, 256) | |||||
| numBlocks = totalLen / 64 | |||||
| For blockIdx = 0 To numBlocks - 1 | |||||
| base = blockIdx * 64 | |||||
| For t = 0 To 15 | |||||
| w(t) = Sha256_BytesToWord(padded(base + t*4), padded(base + t*4 + 1), padded(base + t*4 + 2), padded(base + t*4 + 3)) | |||||
| Next | |||||
| For t = 16 To 63 | |||||
| s0 = Sha256_RotR32(w(t-15), 7) Xor Sha256_RotR32(w(t-15), 18) Xor Sha256_ShR32(w(t-15), 3) | |||||
| s1 = Sha256_RotR32(w(t-2), 17) Xor Sha256_RotR32(w(t-2), 19) Xor Sha256_ShR32(w(t-2), 10) | |||||
| w(t) = Sha256_Add32_5(w(t-16), s0, w(t-7), s1, 0) | |||||
| Next | |||||
| a = hs(0) : b = hs(1) : c = hs(2) : d = hs(3) | |||||
| e = hs(4) : f = hs(5) : g = hs(6) : h = hs(7) | |||||
| For t = 0 To 63 | |||||
| bigS1 = Sha256_RotR32(e,6) Xor Sha256_RotR32(e,11) Xor Sha256_RotR32(e,25) | |||||
| ch = (e And f) Xor ((Not e) And g) | |||||
| temp1 = Sha256_Add32_5(h, bigS1, ch, k(t), w(t)) | |||||
| bigS0 = Sha256_RotR32(a,2) Xor Sha256_RotR32(a,13) Xor Sha256_RotR32(a,22) | |||||
| maj = (a And b) Xor (a And c) Xor (b And c) | |||||
| temp2 = Sha256_Add32(bigS0, maj) | |||||
| h = g | |||||
| g = f | |||||
| f = e | |||||
| e = Sha256_Add32(d, temp1) | |||||
| d = c | |||||
| c = b | |||||
| b = a | |||||
| a = Sha256_Add32(temp1, temp2) | |||||
| Next | |||||
| hs(0) = Sha256_Add32(hs(0), a) : hs(1) = Sha256_Add32(hs(1), b) : hs(2) = Sha256_Add32(hs(2), c) : hs(3) = Sha256_Add32(hs(3), d) | |||||
| hs(4) = Sha256_Add32(hs(4), e) : hs(5) = Sha256_Add32(hs(5), f) : hs(6) = Sha256_Add32(hs(6), g) : hs(7) = Sha256_Add32(hs(7), h) | |||||
| Next | |||||
| result = "" | |||||
| For i = 0 To 7 | |||||
| result = result & Sha256_HexWord(hs(i)) | |||||
| Next | |||||
| Sha256Hex = result | |||||
| End Function | End Function | ||||
| Function HashPassword(password) | |||||
| HashPassword = Sha256Hex(password) | |||||
| End Function | |||||
| '======================================================================================================================= | |||||
| ' CheckPassword | |||||
| ' | |||||
| ' The original implementation called CreateRepository(conn, "Users", "UserId") | |||||
| ' and Array("UserName", user) - "conn" and "CreateRepository" are not defined | |||||
| ' anywhere in this framework (no global "conn", no CreateRepository factory), | |||||
| ' and it searched using the not-yet-assigned "user" variable instead of the | |||||
| ' "username" parameter, so this function has never been callable. Nothing in | |||||
| ' this codebase calls it yet, and there is no Users table/migration either - | |||||
| ' this remains example/scaffold code. Rewritten here to use the framework's | |||||
| ' actual data-access primitive (DAL().Query with a parameterized command) and | |||||
| ' the correct lookup value, so it will work once a Users table exists with | |||||
| ' UserName and PasswordHash columns. | |||||
| '======================================================================================================================= | |||||
| Function CheckPassword(username, password) | Function CheckPassword(username, password) | ||||
| Dim user,UsersRepository | |||||
| Set UsersRepository = CreateRepository(conn, "Users", "UserId") | |||||
| ' Find User | |||||
| Set User = UsersRepository.Find(Array("UserName", user), Empty) | |||||
| If user Is Nothing Then Exit Function ' Implicitly returns False | |||||
| Dim rs | |||||
| Set rs = DAL().Query("SELECT PasswordHash FROM Users WHERE UserName = ?", Array(username)) | |||||
| If rs.EOF Then | |||||
| CheckPassword = False | |||||
| Else | |||||
| CheckPassword = (HashPassword(password) = rs("PasswordHash")) | |||||
| End If | |||||
| ' Compare Hashed Password | |||||
| CheckPassword = (HashPassword(password) = user.PasswordHash) | |||||
| rs.Close | |||||
| Set rs = Nothing | |||||
| End Function | End Function | ||||
| %> | |||||
| %> | |||||
| @@ -1,4 +1,5 @@ | |||||
| <% | <% | ||||
| Dim protocol | |||||
| 'protocol = IIf(LCase(Request.ServerVariables("HTTPS")) = "1", "https", "http") | 'protocol = IIf(LCase(Request.ServerVariables("HTTPS")) = "1", "https", "http") | ||||
| protocol = "https" | protocol = "https" | ||||
| Dim timeZones | Dim timeZones | ||||
| @@ -260,6 +260,7 @@ Class aspJSON | |||||
| End Function | End Function | ||||
| Private Function aj_ReadNumericValue(ByVal val) | Private Function aj_ReadNumericValue(ByVal val) | ||||
| Dim numdecimals | |||||
| If Instr(val, ".") > 0 Then | If Instr(val, ".") > 0 Then | ||||
| numdecimals = Len(val) - Instr(val, ".") | numdecimals = Len(val) - Instr(val, ".") | ||||
| val = Clng(Replace(val, ".", "")) | val = Clng(Replace(val, ".", "")) | ||||
| @@ -1,166 +0,0 @@ | |||||
| <!--#include file="../app/Controllers/autoload_controllers.asp" --> | |||||
| <% | |||||
| ' Set cache expiration from configuration | |||||
| Dim cacheYear : cacheYear = GetAppSetting("CacheExpirationYear") | |||||
| If cacheYear = "nothing" Then cacheYear = "2030" | |||||
| Response.ExpiresAbsolute = "01/01/" & cacheYear | |||||
| Response.AddHeader "pragma", "no-cache" | |||||
| Response.AddHeader "cache-control", "private, no-cache, must-revalidate" | |||||
| '======================================================================================================================= | |||||
| ' MVC Dispatcher | |||||
| '======================================================================================================================= | |||||
| Class MVC_Dispatcher_Class | |||||
| dim CurrentController | |||||
| Public Property Get ControllerName | |||||
| ControllerName = CurrentController | |||||
| end Property | |||||
| '--------------------------------------------------------------------------------------------------------------------- | |||||
| ' Convenience method to resolve route and dispatch in one call | |||||
| ' method: HTTP method (GET, POST, etc.) | |||||
| ' path: Request path (already cleaned of query params) | |||||
| '--------------------------------------------------------------------------------------------------------------------- | |||||
| Public Sub DispatchRequest(method, path) | |||||
| Dim routeArray | |||||
| routeArray = router.Resolve(method, path) | |||||
| Dispatch routeArray | |||||
| End Sub | |||||
| '--------------------------------------------------------------------------------------------------------------------- | |||||
| ' Main dispatch method - executes a resolved route | |||||
| ' RouteArray: Array(controller, action, params) from router.Resolve() | |||||
| '--------------------------------------------------------------------------------------------------------------------- | |||||
| Public Sub Dispatch(RouteArray) | |||||
| On Error Resume Next | |||||
| Dim controllerName, actionName, hasParams, paramsArray | |||||
| controllerName = RouteArray(0) | |||||
| actionName = RouteArray(1) | |||||
| ' Security: Validate controller and action names | |||||
| If Not ControllerRegistry.IsValidControllerFormat(controllerName) Then | |||||
| Response.Write "<div style='padding:15px; margin:10px; border:2px solid #dc3545; background:#f8d7da; color:#721c24; border-radius:4px;'>" | |||||
| Response.Write "<strong>Security Error:</strong> Invalid controller name format." | |||||
| Response.Write "</div>" | |||||
| Exit Sub | |||||
| End If | |||||
| If Not ControllerRegistry.IsValidActionFormat(actionName) Then | |||||
| Response.Write "<div style='padding:15px; margin:10px; border:2px solid #dc3545; background:#f8d7da; color:#721c24; border-radius:4px;'>" | |||||
| Response.Write "<strong>Security Error:</strong> Invalid action name format." | |||||
| Response.Write "</div>" | |||||
| Exit Sub | |||||
| End If | |||||
| ' Security: Check controller whitelist | |||||
| If Not ControllerRegistry.IsValidController(controllerName) Then | |||||
| Response.Write "<div style='padding:15px; margin:10px; border:2px solid #dc3545; background:#f8d7da; color:#721c24; border-radius:4px;'>" | |||||
| Response.Write "<strong>Security Error:</strong> Controller '" & Server.HTMLEncode(controllerName) & "' is not registered." | |||||
| Response.Write "</div>" | |||||
| Exit Sub | |||||
| End If | |||||
| ' Initialize current controller | |||||
| Dim controllerAssignment : controllerAssignment = "Set CurrentController = " & controllerName & "()" | |||||
| Execute controllerAssignment | |||||
| ' Check if layout should be used | |||||
| hasParams = (UBound(RouteArray) >= 2) | |||||
| If eval(controllerName & ".useLayout") Then | |||||
| %> <!-- #include file="../app/views/Shared/Header.asp" --> <% | |||||
| End If | |||||
| ' Prepare parameters | |||||
| If hasParams Then | |||||
| paramsArray = SurroundStringInArray(RouteArray(2)) | |||||
| Else | |||||
| paramsArray = Empty | |||||
| End If | |||||
| ' Execute controller action | |||||
| ExecuteControllerAction controllerName, actionName, paramsArray | |||||
| ' Include footer if layout is used | |||||
| If eval(controllerName & ".useLayout") Then | |||||
| %> <!-- #include file="../app/views/Shared/Footer.asp" --> <% | |||||
| End If | |||||
| On Error GoTo 0 | |||||
| End Sub | |||||
| ' Helper method to execute controller actions (eliminates code duplication) | |||||
| Private Sub ExecuteControllerAction(controllerName, actionName, paramsArray) | |||||
| On Error Resume Next | |||||
| Dim callString | |||||
| ' Build the call string based on whether we have parameters | |||||
| If Not IsEmpty(paramsArray) And IsArray(paramsArray) And UBound(paramsArray) >= 0 Then | |||||
| callString = "Call " & controllerName & "." & actionName & "(" & Join(paramsArray, ",") & ")" | |||||
| Else | |||||
| callString = "Call " & controllerName & "." & actionName & "()" | |||||
| End If | |||||
| ' Execute the action | |||||
| Execute callString | |||||
| ' Handle errors | |||||
| If Err.Number <> 0 Then | |||||
| HandleDispatchError actionName, Err.Description, Err.Number | |||||
| Err.Clear | |||||
| End If | |||||
| On Error GoTo 0 | |||||
| End Sub | |||||
| ' Centralized error handling for dispatch errors | |||||
| Private Sub HandleDispatchError(actionName, errorDesc, errorNum) | |||||
| Dim isDevelopment | |||||
| isDevelopment = (LCase(GetAppSetting("Environment")) = "development") | |||||
| If isDevelopment Then | |||||
| Response.Write "<div style='padding:15px; margin:10px; border:2px solid #dc3545; background:#f8d7da; color:#721c24; border-radius:4px;'>" | |||||
| Response.Write "<strong>Controller Action Error</strong><br>" | |||||
| Response.Write "Action: <code>" & Server.HTMLEncode(actionName) & "</code><br>" | |||||
| Response.Write "Error: " & Server.HTMLEncode(errorDesc) & "<br>" | |||||
| Response.Write "Error Number: " & errorNum | |||||
| Response.Write "</div>" | |||||
| Else | |||||
| Response.Write "<div style='padding:15px; margin:10px; border:2px solid #dc3545; background:#f8d7da; color:#721c24; border-radius:4px;'>" | |||||
| Response.Write "<strong>An error occurred</strong><br>" | |||||
| Response.Write "Please contact the system administrator if the problem persists." | |||||
| Response.Write "</div>" | |||||
| End If | |||||
| End Sub | |||||
| Public Sub RequirePost | |||||
| If Request.Form.Count = 0 Then MVC.RedirectToExt "NotValid","",empty:End If | |||||
| End Sub | |||||
| ' Shortcut for RedirectToActionExt that does not require passing a parameters argument. | |||||
| Public Sub RedirectToAction(ByVal action_name) | |||||
| RedirectToActionExt action_name, empty | |||||
| End Sub | |||||
| Public Sub RedirectTo(controller_name, action_name) | |||||
| RedirectToExt controller_name, action_name, empty | |||||
| End Sub | |||||
| ' Redirects the browser to the specified action on the specified controller with the specified querystring parameters. | |||||
| ' params is a KVArray of querystring parameters. | |||||
| Public Sub RedirectToExt(controller_name, action_name, params) | |||||
| Response.Redirect Routes.UrlTo(controller_name, action_name, params) | |||||
| End Sub | |||||
| Public Sub RedirectToActionExt(ByVal action_name, ByVal params) | |||||
| RedirectToExt ControllerName, action_name, params | |||||
| End Sub | |||||
| End Class | |||||
| dim MVC_Dispatcher_Class__Singleton | |||||
| Function MVC() | |||||
| if IsEmpty(MVC_Dispatcher_Class__Singleton) then | |||||
| set MVC_Dispatcher_Class__Singleton = new MVC_Dispatcher_Class | |||||
| end if | |||||
| set MVC = MVC_Dispatcher_Class__Singleton | |||||
| End Function | |||||
| %> | |||||
| @@ -0,0 +1,320 @@ | |||||
| <?xml version="1.0"?> | |||||
| <!-- MVCDispatcher.wsc --> | |||||
| <component> | |||||
| <!-- COM registration --> | |||||
| <registration | |||||
| description = "Classic ASP MVC Dispatcher Component" | |||||
| progid = "App.MVCDispatcher" | |||||
| version = "1.0" | |||||
| classid = "{C3D4E5F6-7A8B-49C0-8D1E-2F3A4B5C6D7E}" /> | |||||
| <!-- Public interface --> | |||||
| <public> | |||||
| <property name="Router"> | |||||
| <put internalName="PutRouter"/> | |||||
| </property> | |||||
| <property name="ControllerRegistry"> | |||||
| <put internalName="PutControllerRegistry"/> | |||||
| </property> | |||||
| <property name="ControllerFactory"> | |||||
| <put internalName="PutControllerFactory"/> | |||||
| </property> | |||||
| <property name="Routes"> | |||||
| <put internalName="PutRoutes"/> | |||||
| </property> | |||||
| <property name="ControllerName"> | |||||
| <get internalName="GetControllerName"/> | |||||
| </property> | |||||
| <property name="CurrentController"> | |||||
| <get internalName="GetCurrentController"/> | |||||
| </property> | |||||
| <property name="UseLayout"> | |||||
| <get internalName="GetUseLayout"/> | |||||
| </property> | |||||
| <property name="LastError"> | |||||
| <get internalName="GetLastError"/> | |||||
| </property> | |||||
| <method name="Resolve"/> | |||||
| <method name="ExecuteAction"/> | |||||
| <method name="DispatchRequest"/> | |||||
| <method name="RequirePost"/> | |||||
| <method name="RedirectToAction"/> | |||||
| <method name="RedirectTo"/> | |||||
| <method name="RedirectToExt"/> | |||||
| <method name="RedirectToActionExt"/> | |||||
| </public> | |||||
| <!-- Give the component ASP intrinsic objects (Request, Response, Server ...) --> | |||||
| <implements type="ASP"/> | |||||
| <!-- Implementation --> | |||||
| <script language="VBScript"> | |||||
| <![CDATA[ | |||||
| Option Explicit | |||||
| '------------------------------------------------------------ | |||||
| ' Injected dependencies | |||||
| ' | |||||
| ' A WSC runs in its own isolated script engine, so it cannot see the | |||||
| ' including ASP page's globals (GetAppSetting, ControllerRegistry(), | |||||
| ' Routes(), router, the per-controller singleton functions, etc.) the | |||||
| ' way mvc.asp used to. Per AGENTS.md section 69/70 ("Dependencies Must | |||||
| ' Be Injected", "Avoid Hidden Dependencies"), everything this component | |||||
| ' needs from the outside is passed in explicitly instead. | |||||
| ' | |||||
| ' Properties here are wired to plain functions (PutX/GetX) via | |||||
| ' internalName in the <public> section above, rather than VBScript | |||||
| ' "Property Get/Let/Set" syntax - that syntax is only legal inside an | |||||
| ' explicit Class...End Class block (a general VBScript rule, not | |||||
| ' specific to WSC), and this component's members are declared directly | |||||
| ' in the script rather than inside a Class, matching router.wsc's | |||||
| ' existing convention in this project. | |||||
| '------------------------------------------------------------ | |||||
| Private m_router | |||||
| Private m_controllerRegistry | |||||
| Private m_controllerFactory | |||||
| Private m_routes | |||||
| Private m_currentController | |||||
| Private m_controllerName ' "Controller" suffix stripped, e.g. "Home" | |||||
| Private m_actionName | |||||
| Private m_params | |||||
| Private m_lastError | |||||
| Sub PutRouter(value) | |||||
| Set m_router = value | |||||
| End Sub | |||||
| Sub PutControllerRegistry(value) | |||||
| Set m_controllerRegistry = value | |||||
| End Sub | |||||
| Sub PutControllerFactory(value) | |||||
| Set m_controllerFactory = value | |||||
| End Sub | |||||
| Sub PutRoutes(value) | |||||
| Set m_routes = value | |||||
| End Sub | |||||
| '------------------------------------------------------------ | |||||
| ' Read-only state exposed to callers/views | |||||
| '------------------------------------------------------------ | |||||
| ' Name of the controller most recently resolved by Resolve(), with the | |||||
| ' "Controller" suffix stripped (e.g. "Home", not "HomeController"). | |||||
| Function GetControllerName() | |||||
| GetControllerName = m_controllerName | |||||
| End Function | |||||
| ' The controller object itself, e.g. so a layout can read CurrentController.Title. | |||||
| Function GetCurrentController() | |||||
| Set GetCurrentController = m_currentController | |||||
| End Function | |||||
| Function GetUseLayout() | |||||
| If IsObject(m_currentController) Then | |||||
| GetUseLayout = m_currentController.useLayout | |||||
| Else | |||||
| GetUseLayout = False | |||||
| End If | |||||
| End Function | |||||
| ' Nothing when the last Resolve()/ExecuteAction() succeeded. Otherwise a | |||||
| ' Dictionary with "Type" ("Security" or "Action") plus either "Message" | |||||
| ' (Security) or "ActionName"/"Number"/"Description" (Action). Rendering | |||||
| ' this is the caller's job - per AGENTS.md's "Controllers Coordinate; | |||||
| ' They Do Not Render", this component never calls Response.Write. | |||||
| Function GetLastError() | |||||
| Set GetLastError = m_lastError | |||||
| End Function | |||||
| '------------------------------------------------------------ | |||||
| ' Resolve(method, path) -> Boolean | |||||
| ' | |||||
| ' Resolves the route, validates the controller/action name format and | |||||
| ' whitelist, and creates the controller via the injected ControllerFactory. | |||||
| ' Returns False and sets LastError (Type = "Security") if any check fails. | |||||
| ' Must be called before ExecuteAction(). | |||||
| '------------------------------------------------------------ | |||||
| Public Function Resolve(method, path) | |||||
| Dim routeArray, controllerNameRaw, actionNameRaw | |||||
| Set m_lastError = Nothing | |||||
| Set m_currentController = Nothing | |||||
| m_controllerName = "" | |||||
| m_actionName = "" | |||||
| m_params = Empty | |||||
| routeArray = m_router.Resolve(method, path) | |||||
| controllerNameRaw = routeArray(0) | |||||
| actionNameRaw = routeArray(1) | |||||
| If Not m_controllerRegistry.IsValidControllerFormat(controllerNameRaw) Then | |||||
| SetSecurityError "Invalid controller name format." | |||||
| Resolve = False | |||||
| Exit Function | |||||
| End If | |||||
| If Not m_controllerRegistry.IsValidActionFormat(actionNameRaw) Then | |||||
| SetSecurityError "Invalid action name format." | |||||
| Resolve = False | |||||
| Exit Function | |||||
| End If | |||||
| If Not m_controllerRegistry.IsValidController(controllerNameRaw) Then | |||||
| SetSecurityError "Controller '" & controllerNameRaw & "' is not registered." | |||||
| Resolve = False | |||||
| Exit Function | |||||
| End If | |||||
| Set m_currentController = m_controllerFactory.Create(controllerNameRaw) | |||||
| m_controllerName = StripControllerSuffix(controllerNameRaw) | |||||
| m_actionName = actionNameRaw | |||||
| If UBound(routeArray) >= 2 Then | |||||
| m_params = SurroundParamsInArray(routeArray(2)) | |||||
| End If | |||||
| Resolve = True | |||||
| End Function | |||||
| '------------------------------------------------------------ | |||||
| ' ExecuteAction() | |||||
| ' | |||||
| ' Invokes the resolved action on the resolved controller. Must follow a | |||||
| ' successful Resolve(). Populates LastError (Type = "Action") on failure | |||||
| ' instead of raising or writing HTML, so the caller decides how to show it. | |||||
| ' | |||||
| ' Named ExecuteAction rather than Execute because VBScript's Execute | |||||
| ' STATEMENT (used below to invoke the action by name) is shadowed by any | |||||
| ' Sub/Function of the same name in scope - a Sub literally named "Execute" | |||||
| ' cannot call the Execute statement from inside itself. | |||||
| ' | |||||
| ' The action name is still dispatched via a scoped Execute, since | |||||
| ' controllers do not (yet) implement the Invoke() reflection convention | |||||
| ' from AGENTS.md section 69 - that is a larger, separate change to every | |||||
| ' controller's contract. What this fixes is the CONTROLLER resolution: | |||||
| ' the original mvc.asp built "Set CurrentController = " & controllerName | |||||
| ' & "()" from a route-supplied string and Executed it, resolving an | |||||
| ' arbitrary global name at dispatch time. Here the controller always comes | |||||
| ' from the injected ControllerFactory (whitelisted, explicitly registered | |||||
| ' ahead of time), and this Execute only ever calls a method by name on | |||||
| ' that already-known object. | |||||
| '------------------------------------------------------------ | |||||
| Public Sub ExecuteAction() | |||||
| Dim callString, errInfo | |||||
| If Not IsObject(m_currentController) Then | |||||
| Err.Raise vbObjectError + 1200, "MVCDispatcher.ExecuteAction", "ExecuteAction called before a successful Resolve." | |||||
| End If | |||||
| If IsArray(m_params) Then | |||||
| If UBound(m_params) >= 0 Then | |||||
| callString = "Call m_currentController." & m_actionName & "(" & Join(m_params, ",") & ")" | |||||
| Else | |||||
| callString = "Call m_currentController." & m_actionName & "()" | |||||
| End If | |||||
| Else | |||||
| callString = "Call m_currentController." & m_actionName & "()" | |||||
| End If | |||||
| On Error Resume Next | |||||
| Execute callString | |||||
| If Err.Number <> 0 Then | |||||
| Set errInfo = Server.CreateObject("Scripting.Dictionary") | |||||
| errInfo.Add "Type", "Action" | |||||
| errInfo.Add "ActionName", m_actionName | |||||
| errInfo.Add "Number", Err.Number | |||||
| errInfo.Add "Description", Err.Description | |||||
| Set m_lastError = errInfo | |||||
| Err.Clear | |||||
| End If | |||||
| On Error Goto 0 | |||||
| End Sub | |||||
| '------------------------------------------------------------ | |||||
| ' DispatchRequest(method, path) | |||||
| ' | |||||
| ' Convenience wrapper for callers that do not need layout wrapping or | |||||
| ' custom error rendering (e.g. tests): Resolve then ExecuteAction, silently | |||||
| ' doing nothing further on a Resolve failure. Callers that need to wrap | |||||
| ' output in a layout or render errors (the real app's entry point) should | |||||
| ' call Resolve/UseLayout/ExecuteAction/LastError directly instead - see | |||||
| ' public/Default.asp. | |||||
| '------------------------------------------------------------ | |||||
| Public Sub DispatchRequest(method, path) | |||||
| If Resolve(method, path) Then | |||||
| ExecuteAction() | |||||
| End If | |||||
| End Sub | |||||
| '------------------------------------------------------------ | |||||
| Public Sub RequirePost() | |||||
| If Request.Form.Count = 0 Then | |||||
| RedirectToExt "NotValid", "", Empty | |||||
| End If | |||||
| End Sub | |||||
| ' Shortcut for RedirectToActionExt that does not require passing a parameters argument. | |||||
| Public Sub RedirectToAction(action_name) | |||||
| RedirectToActionExt action_name, Empty | |||||
| End Sub | |||||
| Public Sub RedirectTo(controller_name, action_name) | |||||
| RedirectToExt controller_name, action_name, Empty | |||||
| End Sub | |||||
| ' Redirects the browser to the specified action on the specified controller with the | |||||
| ' specified querystring parameters. params is a KVArray of querystring parameters. | |||||
| Public Sub RedirectToExt(controller_name, action_name, params) | |||||
| Response.Redirect m_routes.UrlTo(controller_name, action_name, params) | |||||
| End Sub | |||||
| Public Sub RedirectToActionExt(action_name, params) | |||||
| RedirectToExt m_controllerName, action_name, params | |||||
| End Sub | |||||
| '------------------------------------------------------------ | |||||
| ' Private helpers | |||||
| '------------------------------------------------------------ | |||||
| Private Sub SetSecurityError(message) | |||||
| Dim errInfo | |||||
| Set errInfo = Server.CreateObject("Scripting.Dictionary") | |||||
| errInfo.Add "Type", "Security" | |||||
| errInfo.Add "Message", message | |||||
| Set m_lastError = errInfo | |||||
| End Sub | |||||
| Private Function StripControllerSuffix(name) | |||||
| Const suffix = "Controller" | |||||
| If Len(name) > Len(suffix) And LCase(Right(name, Len(suffix))) = LCase(suffix) Then | |||||
| StripControllerSuffix = Left(name, Len(name) - Len(suffix)) | |||||
| Else | |||||
| StripControllerSuffix = name | |||||
| End If | |||||
| End Function | |||||
| ' Wraps string route params in quotes so they can be spliced into the | |||||
| ' dynamically-built Execute call string in ExecuteAction() above. Equivalent | |||||
| ' to the app-side SurroundStringInArray() helper, duplicated here (rather | |||||
| ' than called) because this WSC cannot see that global function. | |||||
| Private Function SurroundParamsInArray(arr) | |||||
| Dim i, result | |||||
| result = arr | |||||
| For i = LBound(result) To UBound(result) | |||||
| If TypeName(result(i)) = "String" Then | |||||
| result(i) = """" & result(i) & """" | |||||
| End If | |||||
| Next | |||||
| SurroundParamsInArray = result | |||||
| End Function | |||||
| ]]> | |||||
| </script> | |||||
| </component> | |||||
| @@ -1,994 +0,0 @@ | |||||
| # CLAUDE.md | |||||
| ## Purpose | |||||
| This file defines how Claude and Claude-compatible coding agents should operate in this repository. | |||||
| Read `AGENTS.md` first. | |||||
| `AGENTS.md` is the primary development policy. | |||||
| This file defines agent workflow, reasoning discipline, project learning, and self-improvement behavior. | |||||
| --- | |||||
| # 1. Startup Procedure | |||||
| At the beginning of a development task: | |||||
| 1. Read `AGENTS.md`. | |||||
| 2. Read this `CLAUDE.md`. | |||||
| 3. Inspect relevant files before editing. | |||||
| 4. Search the repository for similar implementations. | |||||
| 5. Identify existing helpers and abstractions. | |||||
| 6. Determine the affected architectural layers. | |||||
| 7. Identify security-sensitive boundaries. | |||||
| 8. Identify tests relevant to the change. | |||||
| Do not immediately generate a new abstraction before understanding the existing project. | |||||
| --- | |||||
| # 2. Treat VBScript as a Language Requiring Agent-Level Static Analysis | |||||
| VBScript lacks many safeguards found in modern compiled languages. | |||||
| Therefore, while working, actively inspect for: | |||||
| ```text | |||||
| undeclared variables | |||||
| misspelled identifiers | |||||
| missing Set | |||||
| wrong argument counts | |||||
| implicit Variant conversions | |||||
| Null propagation | |||||
| Empty values | |||||
| Nothing references | |||||
| unexpected ByRef mutation | |||||
| invalid function return paths | |||||
| unsafe SQL | |||||
| unsafe HTML output | |||||
| broad error suppression | |||||
| resource leaks | |||||
| global-state coupling | |||||
| ``` | |||||
| Act as a compiler substitute. | |||||
| --- | |||||
| # 3. Think in Boundaries | |||||
| Before changing code, determine which layer owns the behavior. | |||||
| Preferred direction: | |||||
| ```text | |||||
| Request | |||||
| ↓ | |||||
| Controller | |||||
| ↓ | |||||
| Service | |||||
| ↓ | |||||
| Repository | |||||
| ↓ | |||||
| Database | |||||
| ``` | |||||
| Views should receive prepared data. | |||||
| Do not bypass layers for convenience. | |||||
| --- | |||||
| # 4. Avoid Architectural Drift | |||||
| When adding functionality, prefer the project's existing good pattern. | |||||
| Do not create: | |||||
| ```text | |||||
| CustomerManager | |||||
| CustomerHandler | |||||
| CustomerEngine | |||||
| CustomerProcessor | |||||
| CustomerHelper | |||||
| ``` | |||||
| if `CustomerService` already represents the business layer. | |||||
| Reuse terminology consistently. | |||||
| --- | |||||
| # 5. Search Before Creating | |||||
| Before creating: | |||||
| - a helper | |||||
| - service | |||||
| - repository | |||||
| - validator | |||||
| - utility | |||||
| - model | |||||
| - WSC component | |||||
| - framework function | |||||
| search for an existing implementation. | |||||
| Prefer extending an existing appropriate abstraction over creating duplicates. | |||||
| --- | |||||
| # 6. Make Small Changes | |||||
| Prefer the smallest coherent change that solves the problem. | |||||
| Avoid unrelated cleanup unless necessary. | |||||
| If a task exposes a larger architectural problem: | |||||
| 1. solve the requested problem safely | |||||
| 2. document the larger concern | |||||
| 3. improve the architecture when justified | |||||
| Do not turn every bug fix into a rewrite. | |||||
| --- | |||||
| # 7. Never Hide Errors to Make Code Appear Functional | |||||
| Do not use broad: | |||||
| ```vbscript | |||||
| On Error Resume Next | |||||
| ``` | |||||
| to make failures disappear. | |||||
| Errors should be: | |||||
| ```text | |||||
| prevented | |||||
| handled | |||||
| logged | |||||
| or propagated | |||||
| ``` | |||||
| Never silently swallowed. | |||||
| --- | |||||
| # 8. Security Review Is Mandatory | |||||
| For each change involving input, output, authentication, database access, file handling, or state changes, inspect for: | |||||
| ```text | |||||
| SQL injection | |||||
| XSS | |||||
| CSRF | |||||
| authorization bypass | |||||
| open redirects | |||||
| path traversal | |||||
| unsafe file upload | |||||
| sensitive logging | |||||
| session misuse | |||||
| ``` | |||||
| Security checks are not optional even if the user did not explicitly request them. | |||||
| --- | |||||
| # 9. SQL Generation | |||||
| Always prefer parameterized ADODB commands. | |||||
| If existing code concatenates SQL, do not copy the unsafe pattern into new code. | |||||
| When touching the unsafe section directly, improve it if doing so is reasonably scoped and compatibility can be preserved. | |||||
| --- | |||||
| # 10. HTML Generation | |||||
| Dynamic text should be HTML encoded by default. | |||||
| Do not output raw Request or database values into HTML. | |||||
| When raw HTML is intentionally supported, make that trust boundary explicit. | |||||
| --- | |||||
| # 11. Validate at the Boundary | |||||
| Convert raw external values into trusted application values early. | |||||
| Think: | |||||
| ```text | |||||
| Raw Request value | |||||
| ↓ | |||||
| Trim / normalize | |||||
| ↓ | |||||
| Required check | |||||
| ↓ | |||||
| Format validation | |||||
| ↓ | |||||
| Type conversion | |||||
| ↓ | |||||
| Business validation | |||||
| ↓ | |||||
| Use | |||||
| ``` | |||||
| Do not allow raw Request values to flow deeply through the system. | |||||
| --- | |||||
| # 12. Prefer Explicit Code | |||||
| In VBScript, explicit code is usually safer than clever code. | |||||
| Prefer: | |||||
| ```vbscript | |||||
| Dim customerId | |||||
| customerId = CLng(value) | |||||
| ``` | |||||
| over relying on automatic coercion. | |||||
| Prefer clear control flow over compressed expressions. | |||||
| --- | |||||
| # 13. Functions and Procedures | |||||
| Keep procedures focused. | |||||
| If a function performs several unrelated operations, extract meaningful collaborators. | |||||
| Prefer names that describe intent. | |||||
| Bad: | |||||
| ```text | |||||
| ProcessData | |||||
| HandleStuff | |||||
| DoWork | |||||
| RunThing | |||||
| ``` | |||||
| Better: | |||||
| ```text | |||||
| ValidateCustomer | |||||
| CalculateOrderTotal | |||||
| LoadCustomerById | |||||
| SaveAppointment | |||||
| ``` | |||||
| --- | |||||
| # 14. ByVal and ByRef | |||||
| Remember: | |||||
| VBScript defaults parameters to `ByRef`. | |||||
| When mutation is not intended, prefer: | |||||
| ```vbscript | |||||
| ByVal | |||||
| ``` | |||||
| Use `ByRef` only when caller mutation is intentional. | |||||
| Review changed method signatures for accidental ByRef behavior. | |||||
| --- | |||||
| # 15. Object Assignment | |||||
| Remember that object assignment requires `Set`. | |||||
| Review every new or modified object assignment. | |||||
| Example: | |||||
| ```vbscript | |||||
| Set service = New CustomerService | |||||
| ``` | |||||
| not: | |||||
| ```vbscript | |||||
| service = New CustomerService | |||||
| ``` | |||||
| --- | |||||
| # 16. Function Return Values | |||||
| VBScript functions return values by assigning to their own name. | |||||
| Review all paths. | |||||
| Example: | |||||
| ```vbscript | |||||
| Function IsAllowed(ByVal userId) | |||||
| If userId <= 0 Then | |||||
| IsAllowed = False | |||||
| Exit Function | |||||
| End If | |||||
| IsAllowed = True | |||||
| End Function | |||||
| ``` | |||||
| Do not accidentally leave an important return value as Empty. | |||||
| --- | |||||
| # 17. COM and ADODB Cleanup | |||||
| When creating: | |||||
| - ADODB.Connection | |||||
| - ADODB.Command | |||||
| - ADODB.Recordset | |||||
| - filesystem objects | |||||
| - external COM components | |||||
| understand ownership and cleanup behavior. | |||||
| Explicitly close database resources when appropriate. | |||||
| Set references to `Nothing` when ownership ends. | |||||
| --- | |||||
| # 18. Avoid Global State | |||||
| Prefer explicit dependencies. | |||||
| Do not add Session or Application state merely because it is convenient. | |||||
| Ask: | |||||
| ```text | |||||
| Does this value truly need to survive across requests? | |||||
| ``` | |||||
| If not, keep it request-scoped. | |||||
| --- | |||||
| # 19. Testing Strategy | |||||
| When possible, isolate business logic from ASP globals. | |||||
| Favor code that can be invoked with plain VBScript values or mock collaborators. | |||||
| When fixing a defect, add a regression test where practical. | |||||
| --- | |||||
| # 20. Comments | |||||
| Comments should explain: | |||||
| ```text | |||||
| why | |||||
| constraints | |||||
| non-obvious behavior | |||||
| compatibility requirements | |||||
| security rationale | |||||
| ``` | |||||
| Do not add comments that merely repeat the code. | |||||
| Bad: | |||||
| ```vbscript | |||||
| ' Set customer id | |||||
| customerId = 10 | |||||
| ``` | |||||
| Useful: | |||||
| ```vbscript | |||||
| ' Legacy import files use 0 to represent an unknown customer. | |||||
| If customerId = 0 Then | |||||
| ``` | |||||
| --- | |||||
| # 21. Preserve Valuable Legacy Behavior | |||||
| ASP Classic projects may contain old but important behavior. | |||||
| Do not assume unfamiliar code is wrong. | |||||
| Before removing or replacing something: | |||||
| 1. search references | |||||
| 2. understand callers | |||||
| 3. identify side effects | |||||
| 4. inspect related documentation | |||||
| 5. preserve compatibility where required | |||||
| --- | |||||
| # 22. Self-Improvement | |||||
| This repository is designed to allow agents to improve their own operating instructions. | |||||
| Self-improvement should make future work: | |||||
| - safer | |||||
| - more consistent | |||||
| - easier to verify | |||||
| - less repetitive | |||||
| - more maintainable | |||||
| Do not change instructions merely to suit the current implementation. | |||||
| Improve the implementation when practical rather than weakening good rules. | |||||
| --- | |||||
| # 23. Detect Learnable Events | |||||
| A learnable event occurs when: | |||||
| - a repeated bug appears | |||||
| - a review catches the same mistake repeatedly | |||||
| - a new reusable pattern proves successful | |||||
| - a project architecture decision becomes permanent | |||||
| - a new security requirement becomes necessary | |||||
| - a repeated workflow can be standardized | |||||
| - a user establishes a lasting convention | |||||
| - an existing instruction becomes inaccurate | |||||
| When a learnable event occurs, determine whether project documentation should change. | |||||
| --- | |||||
| # 24. Where Learning Should Go | |||||
| Use the smallest appropriate scope. | |||||
| Update: | |||||
| ```text | |||||
| AGENTS.md | |||||
| ``` | |||||
| for project-wide durable policies. | |||||
| Update: | |||||
| ```text | |||||
| CLAUDE.md | |||||
| ``` | |||||
| for agent workflow and Claude-specific operating behavior. | |||||
| Update: | |||||
| ```text | |||||
| /docs/* | |||||
| ``` | |||||
| for architectural or project documentation. | |||||
| Update: | |||||
| ```text | |||||
| /skills/* | |||||
| ``` | |||||
| for reusable specialized procedures. | |||||
| Update source-code comments only for code-local constraints. | |||||
| Do not place every lesson into AGENTS.md. | |||||
| --- | |||||
| # 25. Self-Update Decision | |||||
| Before modifying an instruction file, ask: | |||||
| ```text | |||||
| Is this lesson likely to matter again? | |||||
| ``` | |||||
| If no, do not persist it. | |||||
| Then ask: | |||||
| ```text | |||||
| Is this project-wide? | |||||
| ``` | |||||
| If yes, consider AGENTS.md. | |||||
| Otherwise use a specialized document. | |||||
| --- | |||||
| # 26. Never Self-Weaken Core Safety Rules | |||||
| Claude must not autonomously remove or weaken requirements involving: | |||||
| ```text | |||||
| Option Explicit | |||||
| parameterized SQL | |||||
| server-side authorization | |||||
| input validation | |||||
| output encoding | |||||
| safe error handling | |||||
| resource cleanup | |||||
| secret protection | |||||
| ``` | |||||
| If an unusual project requirement appears to conflict with one of these rules, preserve safety and document the conflict. | |||||
| --- | |||||
| # 27. Avoid Instruction Inflation | |||||
| Do not add a new rule for every isolated mistake. | |||||
| Prefer: | |||||
| ```text | |||||
| general principle | |||||
| + | |||||
| reusable example | |||||
| ``` | |||||
| rather than hundreds of hyper-specific rules. | |||||
| A rule should prevent a category of mistakes. | |||||
| --- | |||||
| # 28. Improve Through Abstractions Too | |||||
| Not every lesson belongs in documentation. | |||||
| If the same safety requirement appears repeatedly, consider encoding it into the framework. | |||||
| Examples: | |||||
| ```text | |||||
| Safe HTML helper | |||||
| Parameterized database wrapper | |||||
| Request conversion helper | |||||
| Validation library | |||||
| Authorization service | |||||
| Logging wrapper | |||||
| Repository base conventions | |||||
| ``` | |||||
| The best rule is sometimes one the architecture makes difficult to violate. | |||||
| --- | |||||
| # 29. Skill Creation | |||||
| Claude may create a new skill when the task is: | |||||
| - specialized | |||||
| - recurring | |||||
| - procedural | |||||
| - reusable across multiple features | |||||
| Possible skills: | |||||
| ```text | |||||
| /skills/asp-classic-controller.md | |||||
| /skills/adodb-repository.md | |||||
| /skills/sql-parameterization.md | |||||
| /skills/security-review.md | |||||
| /skills/vbscript-static-review.md | |||||
| /skills/testing.md | |||||
| ``` | |||||
| Do not create a skill for a one-time task. | |||||
| --- | |||||
| # 30. Suggested Skill Format | |||||
| Each skill should contain: | |||||
| ```text | |||||
| # Skill Name | |||||
| ## Purpose | |||||
| ## Use When | |||||
| ## Inputs | |||||
| ## Procedure | |||||
| ## Required Rules | |||||
| ## Validation Checklist | |||||
| ## Common Mistakes | |||||
| ## Output | |||||
| ## Improvement Notes | |||||
| ``` | |||||
| --- | |||||
| # 31. Agent Review Loop | |||||
| For meaningful tasks, use this cycle: | |||||
| ```text | |||||
| Understand | |||||
| ↓ | |||||
| Inspect | |||||
| ↓ | |||||
| Design | |||||
| ↓ | |||||
| Implement | |||||
| ↓ | |||||
| Review | |||||
| ↓ | |||||
| Test | |||||
| ↓ | |||||
| Learn | |||||
| ``` | |||||
| The `Learn` step asks whether the repository's instructions or abstractions should improve. | |||||
| --- | |||||
| # 32. Design Before Implementation | |||||
| For significant changes, identify: | |||||
| ```text | |||||
| affected layers | |||||
| new components | |||||
| existing components | |||||
| data flow | |||||
| validation boundaries | |||||
| security boundaries | |||||
| failure behavior | |||||
| testing strategy | |||||
| ``` | |||||
| before writing substantial code. | |||||
| For small tasks, keep this lightweight. | |||||
| --- | |||||
| # 33. Avoid Premature Framework Expansion | |||||
| Do not add framework components simply because another language would have them. | |||||
| ASP Classic should remain simple. | |||||
| Only introduce an abstraction when it solves a real recurring problem. | |||||
| --- | |||||
| # 34. Prefer Composition | |||||
| When extending behavior, first consider: | |||||
| ```text | |||||
| small object | |||||
| + | |||||
| small collaborator | |||||
| ``` | |||||
| instead of large pseudo-inheritance systems. | |||||
| VBScript is better suited to simple composition. | |||||
| --- | |||||
| # 35. Stable Public Contracts | |||||
| Treat established routes, APIs, class methods, and database contracts carefully. | |||||
| When changing them: | |||||
| 1. search callers | |||||
| 2. assess compatibility | |||||
| 3. update all consumers | |||||
| 4. document breaking changes | |||||
| Avoid accidental interface changes. | |||||
| --- | |||||
| # 36. Database Changes | |||||
| For schema changes, consider: | |||||
| ```text | |||||
| backward compatibility | |||||
| migration order | |||||
| existing data | |||||
| Null behavior | |||||
| indexes | |||||
| constraints | |||||
| application deployment timing | |||||
| rollback implications | |||||
| ``` | |||||
| Do not modify schema casually. | |||||
| --- | |||||
| # 37. Performance Review | |||||
| When working with database code, look for: | |||||
| ```text | |||||
| queries inside loops | |||||
| SELECT * | |||||
| unnecessary recordset traversal | |||||
| missing filters | |||||
| repeated connection creation | |||||
| large Session values | |||||
| unbounded result sets | |||||
| unnecessary COM calls | |||||
| ``` | |||||
| Prefer database-side filtering and set operations. | |||||
| --- | |||||
| # 38. Request Lifecycle Awareness | |||||
| Remember that ASP Classic operates per request. | |||||
| Avoid assumptions that local variables persist between requests. | |||||
| Use Session or persistent storage only when the application truly requires cross-request state. | |||||
| --- | |||||
| # 39. Application Lifecycle Awareness | |||||
| `Application` state is global to the ASP application. | |||||
| Do not store user-specific data there. | |||||
| Be mindful of concurrent requests. | |||||
| --- | |||||
| # 40. Avoid Hidden Dependencies | |||||
| A component should not unexpectedly require: | |||||
| ```text | |||||
| Request | |||||
| Response | |||||
| Session | |||||
| Application | |||||
| global connection object | |||||
| global logger | |||||
| global configuration | |||||
| ``` | |||||
| unless that dependency is part of the documented architecture. | |||||
| Prefer passing dependencies explicitly. | |||||
| --- | |||||
| # 41. Compatibility First During Refactoring | |||||
| When modernizing legacy code: | |||||
| ```text | |||||
| First preserve behavior. | |||||
| Then improve structure. | |||||
| ``` | |||||
| Avoid combining major behavior changes with major refactors unless necessary. | |||||
| --- | |||||
| # 42. Repository Review After Significant Changes | |||||
| After a meaningful feature or refactor, search for: | |||||
| - outdated documentation | |||||
| - duplicate helpers | |||||
| - now-dead code | |||||
| - inconsistent naming | |||||
| - old patterns replaced by the new pattern | |||||
| - opportunities to update an existing skill | |||||
| Keep the repository internally consistent. | |||||
| --- | |||||
| # 43. Learn From User Corrections | |||||
| If the user corrects Claude on a durable project convention, treat that as a possible learnable event. | |||||
| Examples: | |||||
| ```text | |||||
| "We always use this router." | |||||
| "Repositories never return recordsets." | |||||
| "All AJAX endpoints use this response format." | |||||
| "Use WSC components for controllers." | |||||
| ``` | |||||
| If the instruction is durable, update the appropriate project guidance. | |||||
| Do not persist one-time preferences as permanent architecture rules. | |||||
| --- | |||||
| # 44. Change Documentation Conservatively | |||||
| When modifying AGENTS.md, CLAUDE.md, or skills: | |||||
| - preserve useful existing rules | |||||
| - remove duplicates | |||||
| - clarify contradictions | |||||
| - avoid unnecessary rewrites | |||||
| - add an update-history entry where appropriate | |||||
| Instruction files should become clearer over time, not merely longer. | |||||
| --- | |||||
| # 45. Conflict Detection | |||||
| If project documentation contradicts itself: | |||||
| 1. identify the conflict | |||||
| 2. use higher-priority instructions | |||||
| 3. resolve the contradiction where safe | |||||
| 4. update documentation so future agents do not face the same conflict | |||||
| Do not silently choose different interpretations from task to task. | |||||
| --- | |||||
| # 46. Self-Improvement Is Not Autonomous Product Redesign | |||||
| Claude may improve: | |||||
| ```text | |||||
| guidelines | |||||
| skills | |||||
| documentation | |||||
| developer workflow | |||||
| internal abstractions | |||||
| ``` | |||||
| when justified. | |||||
| Claude must not independently redefine: | |||||
| ```text | |||||
| business requirements | |||||
| user-visible behavior | |||||
| pricing | |||||
| authorization policy | |||||
| data retention policy | |||||
| major architecture goals | |||||
| ``` | |||||
| without task requirements supporting the change. | |||||
| --- | |||||
| # 47. Final Review Mode | |||||
| Before completing a task, temporarily switch from implementation thinking to adversarial review. | |||||
| Ask: | |||||
| ```text | |||||
| How could this fail? | |||||
| What would VBScript fail to warn me about? | |||||
| What input breaks this? | |||||
| What happens with Null? | |||||
| What happens with Empty? | |||||
| What happens with Nothing? | |||||
| Did I forget Set? | |||||
| Could ByRef change the caller? | |||||
| Could SQL injection occur? | |||||
| Could XSS occur? | |||||
| Could authorization be bypassed? | |||||
| Could an error be swallowed? | |||||
| Could a recordset remain open? | |||||
| Did I accidentally duplicate existing functionality? | |||||
| ``` | |||||
| Fix issues found before finishing. | |||||
| --- | |||||
| # 48. Final Summary | |||||
| When reporting completed coding work, mention material items such as: | |||||
| ```text | |||||
| what changed | |||||
| important architecture choices | |||||
| tests performed | |||||
| security implications | |||||
| documentation or rule updates | |||||
| remaining limitations | |||||
| ``` | |||||
| Do not produce long explanations for trivial changes. | |||||
| --- | |||||
| # 49. Core Philosophy | |||||
| Claude should help make ASP Classic behave like a disciplined modern development environment without destroying the language's simplicity. | |||||
| The goal is not to turn VBScript into C#. | |||||
| The goal is to provide the safeguards that VBScript lacks. | |||||
| Think: | |||||
| ```text | |||||
| VBScript | |||||
| + | |||||
| clear architecture | |||||
| + | |||||
| strict conventions | |||||
| + | |||||
| reusable framework components | |||||
| + | |||||
| agent-level static analysis | |||||
| + | |||||
| continuous learning | |||||
| = | |||||
| reliable ASP Classic development | |||||
| ``` | |||||
| --- | |||||
| # Update History | |||||
| 2026-09-02 | |||||
| - Initial Claude operating instructions created. | |||||
| - Added VBScript static-review behavior. | |||||
| - Added controlled self-improvement workflow. | |||||
| - Added learnable-event detection and skill creation guidance. | |||||
| @@ -1,3 +1,12 @@ | |||||
| <% | |||||
| ' Classic ASP splices #include files together as raw text before compiling, | |||||
| ' and Option Explicit is only legal once, as the literal first statement of | |||||
| ' the resulting page. Default.asp pulls in the entire application (core/*, | |||||
| ' app/controllers/*, app/views/*) via autoload_core.asp below, so this single | |||||
| ' declaration enforces explicit variable declaration across all of it. Do not | |||||
| ' add another Option Explicit to any included file - see AGENTS.md. | |||||
| Option Explicit | |||||
| %> | |||||
| <!--#include file="..\core\autoload_core.asp" --> | <!--#include file="..\core\autoload_core.asp" --> | ||||
| <% | <% | ||||
| @@ -7,7 +16,31 @@ | |||||
| router.AddRoute "GET", "", "HomeController", "Index" | router.AddRoute "GET", "", "HomeController", "Index" | ||||
| router.AddRoute "GET", "/404", "ErrorController", "NotFound" | router.AddRoute "GET", "/404", "ErrorController", "NotFound" | ||||
| ' Dispatch the request (resolves route and executes controller action) | |||||
| MVC.DispatchRequest Request.ServerVariables("REQUEST_METHOD"), _ | |||||
| TrimQueryParams(Request.ServerVariables("HTTP_X_ORIGINAL_URL")) | |||||
| ' Resolve the route/controller/action. MVC is a Windows Script Component | |||||
| ' and (per AGENTS.md's "Controllers Coordinate; They Do Not Render") never | |||||
| ' writes HTML itself - Resolve()/ExecuteAction() only ever report success | |||||
| ' or populate LastError, and rendering happens here. It also cannot use | |||||
| ' Server-Side Includes, so the layout wrapping that used to happen inside | |||||
| ' Dispatch() now happens here, guarded by MVC.UseLayout. | |||||
| Dim requestResolved | |||||
| requestResolved = MVC.Resolve(Request.ServerVariables("REQUEST_METHOD"), _ | |||||
| TrimQueryParams(Request.ServerVariables("HTTP_X_ORIGINAL_URL"))) | |||||
| If Not requestResolved Then | |||||
| RenderMvcError MVC.LastError | |||||
| Else | |||||
| If MVC.UseLayout Then | |||||
| %><!--#include file="../app/views/Shared/Header.asp" --><% | |||||
| End If | |||||
| MVC.ExecuteAction() | |||||
| If Not (MVC.LastError Is Nothing) Then | |||||
| RenderMvcError MVC.LastError | |||||
| End If | |||||
| If MVC.UseLayout Then | |||||
| %><!--#include file="../app/views/Shared/Footer.asp" --><% | |||||
| End If | |||||
| End If | |||||
| %> | %> | ||||
| @@ -1,4 +1,16 @@ | |||||
| @echo off | @echo off | ||||
| setlocal | setlocal | ||||
| set "ASPC_STARTER_ROOT=%~dp0" | set "ASPC_STARTER_ROOT=%~dp0" | ||||
| "C:\Program Files\IIS Express\iisexpress.exe" /config:"%~dp0applicationhost.config" | |||||
| set "IISEXPRESS=C:\Program Files\IIS Express\iisexpress.exe" | |||||
| rem IIS Express only runs ONE site per process, even when applicationhost.config | |||||
| rem defines several with serverAutoStart="true" (that attribute is honored by | |||||
| rem full IIS/W3SVC, not by a single ad-hoc iisexpress.exe invocation - per | |||||
| rem "iisexpress /?": "/config:config-file ... runs the first site in the | |||||
| rem specified configuration file"). So each site gets its own process here. | |||||
| echo Starting production site (http://localhost:8080/) ... | |||||
| start "IIS Express - Production (8080)" "%IISEXPRESS%" /config:"%~dp0applicationhost.config" /site:"Development Web Site" | |||||
| echo Starting tests site (http://localhost:8081/) ... | |||||
| start "IIS Express - Tests (8081)" "%IISEXPRESS%" /config:"%~dp0applicationhost.config" /site:"Tests Web Site" | |||||
| @@ -6,7 +6,7 @@ | |||||
| <add key="CacheExpirationYear" value="2030" /> | <add key="CacheExpirationYear" value="2030" /> | ||||
| <add key="EnableCacheBusting" value="false" /> | <add key="EnableCacheBusting" value="false" /> | ||||
| <add key="CacheBustParamName" value="v" /> | <add key="CacheBustParamName" value="v" /> | ||||
| <add key="ProductionAppBaseUrl" value="http://localhost:8081/" /> | |||||
| <add key="ProductionAppBaseUrl" value="http://localhost:8080/" /> | |||||
| </appSettings> | </appSettings> | ||||
| <system.webServer> | <system.webServer> | ||||
| @@ -1,6 +1,7 @@ | |||||
| <!-- #include file="../aspunit/Lib/ASPUnit.asp" --> | <!-- #include file="../aspunit/Lib/ASPUnit.asp" --> | ||||
| <!-- #include file="../bootstrap.asp" --> | <!-- #include file="../bootstrap.asp" --> | ||||
| <!-- #include file="../../core/mvc.asp" --> | |||||
| <!-- #include file="../../core/lib.ControllerFactory.asp" --> | |||||
| <!-- #include file="../../core/lib.Routes.asp" --> | |||||
| <% | <% | ||||
| Class TestDispatchController_Class | Class TestDispatchController_Class | ||||
| @@ -31,12 +32,70 @@ Function TestDispatchController() | |||||
| Set TestDispatchController = TestDispatchController_Class__Singleton | Set TestDispatchController = TestDispatchController_Class__Singleton | ||||
| End Function | End Function | ||||
| ' Named distinctly from TestDispatchController so the "Controller" suffix | |||||
| ' stripping performed by MVC.ControllerName is unambiguous to assert on. | |||||
| Class TitleTestController_Class | |||||
| Private m_useLayout | |||||
| Private Sub Class_Initialize() | |||||
| m_useLayout = False | |||||
| End Sub | |||||
| Public Property Get useLayout | |||||
| useLayout = m_useLayout | |||||
| End Property | |||||
| Public Property Let useLayout(value) | |||||
| m_useLayout = value | |||||
| End Property | |||||
| Public Sub Smoke() | |||||
| dispatchActionRan = True | |||||
| End Sub | |||||
| End Class | |||||
| Dim TitleTestController_Class__Singleton | |||||
| Function TitleTestController() | |||||
| If IsEmpty(TitleTestController_Class__Singleton) Then | |||||
| Set TitleTestController_Class__Singleton = New TitleTestController_Class | |||||
| End If | |||||
| Set TitleTestController = TitleTestController_Class__Singleton | |||||
| End Function | |||||
| ' MVC is now a Windows Script Component (core/mvc.wsc) with its own isolated | |||||
| ' script engine, so - like router.wsc via EnsureTestRouter() below - it is | |||||
| ' instantiated once and has its dependencies (Router, ControllerRegistry, | |||||
| ' ControllerFactory, Routes) injected explicitly rather than reaching into | |||||
| ' this page's globals itself. | |||||
| Dim MVC_Instance | |||||
| Function MVC() | |||||
| Set MVC = MVC_Instance | |||||
| End Function | |||||
| Sub EnsureTestMvc() | |||||
| Call EnsureTestRouter() | |||||
| ' IsObject(Nothing) is True in VBScript, so - matching EnsureTestRouter() | |||||
| ' above - both conditions are needed: "never assigned" and "explicitly | |||||
| ' reset to Nothing" are different states here. | |||||
| If (Not IsObject(MVC_Instance)) Then | |||||
| Set MVC_Instance = GetObject("script:" & ResolveProjectPath("core\\mvc.wsc")) | |||||
| ElseIf MVC_Instance Is Nothing Then | |||||
| Set MVC_Instance = GetObject("script:" & ResolveProjectPath("core\\mvc.wsc")) | |||||
| End If | |||||
| Set MVC_Instance.Router = router | |||||
| Set MVC_Instance.ControllerRegistry = ControllerRegistry() | |||||
| Set MVC_Instance.ControllerFactory = ControllerFactory() | |||||
| Set MVC_Instance.Routes = Routes() | |||||
| End Sub | |||||
| Call ASPUnit.AddModule( _ | Call ASPUnit.AddModule( _ | ||||
| ASPUnit.CreateModule( _ | ASPUnit.CreateModule( _ | ||||
| "MVC Dispatch Smoke Tests", _ | "MVC Dispatch Smoke Tests", _ | ||||
| Array( _ | Array( _ | ||||
| ASPUnit.CreateTest("RootRouteResolvesToHomeController"), _ | ASPUnit.CreateTest("RootRouteResolvesToHomeController"), _ | ||||
| ASPUnit.CreateTest("KnownRouteDispatchCompletesWithoutLookupFailure") _ | |||||
| ASPUnit.CreateTest("KnownRouteDispatchCompletesWithoutLookupFailure"), _ | |||||
| ASPUnit.CreateTest("ControllerNameReturnsDispatchedControllerNameAsString"), _ | |||||
| ASPUnit.CreateTest("ControllerNameFeedsUrlGenerationForRedirects") _ | |||||
| ), _ | ), _ | ||||
| ASPUnit.CreateLifeCycle("SetupMvcDispatch", "TeardownMvcDispatch") _ | ASPUnit.CreateLifeCycle("SetupMvcDispatch", "TeardownMvcDispatch") _ | ||||
| ) _ | ) _ | ||||
| @@ -47,20 +106,29 @@ Call ASPUnit.Run() | |||||
| Sub SetupMvcDispatch() | Sub SetupMvcDispatch() | ||||
| Call ResetTestRuntime() | Call ResetTestRuntime() | ||||
| On Error Resume Next | On Error Resume Next | ||||
| MVC_Dispatcher_Class__Singleton = Empty | |||||
| TestDispatchController_Class__Singleton = Empty | TestDispatchController_Class__Singleton = Empty | ||||
| TitleTestController_Class__Singleton = Empty | |||||
| Set MVC_Instance = Nothing | |||||
| On Error GoTo 0 | On Error GoTo 0 | ||||
| Call ExecuteGlobal("Dim dispatchActionRan") | Call ExecuteGlobal("Dim dispatchActionRan") | ||||
| dispatchActionRan = False | dispatchActionRan = False | ||||
| Call RegisterDefaultRoutes() | Call RegisterDefaultRoutes() | ||||
| Call EnsureTestMvc() | |||||
| Call ControllerRegistry().RegisterController("testdispatchcontroller") | Call ControllerRegistry().RegisterController("testdispatchcontroller") | ||||
| Call router.AddRoute("GET", "/dispatch-smoke", "testdispatchcontroller", "Smoke") | Call router.AddRoute("GET", "/dispatch-smoke", "testdispatchcontroller", "Smoke") | ||||
| Call ControllerFactory().Register("testdispatchcontroller", GetRef("TestDispatchController")) | |||||
| Call ControllerRegistry().RegisterController("titletestcontroller") | |||||
| Call router.AddRoute("GET", "/title-test", "TitleTestController", "Smoke") | |||||
| Call ControllerFactory().Register("TitleTestController", GetRef("TitleTestController")) | |||||
| End Sub | End Sub | ||||
| Sub TeardownMvcDispatch() | Sub TeardownMvcDispatch() | ||||
| On Error Resume Next | On Error Resume Next | ||||
| MVC_Dispatcher_Class__Singleton = Empty | |||||
| Set MVC_Instance = Nothing | |||||
| TestDispatchController_Class__Singleton = Empty | TestDispatchController_Class__Singleton = Empty | ||||
| TitleTestController_Class__Singleton = Empty | |||||
| Response.Status = "200 OK" | Response.Status = "200 OK" | ||||
| On Error GoTo 0 | On Error GoTo 0 | ||||
| Call ResetTestRuntime() | Call ResetTestRuntime() | ||||
| @@ -77,4 +145,33 @@ Function KnownRouteDispatchCompletesWithoutLookupFailure() | |||||
| Call MVC().DispatchRequest("GET", "/dispatch-smoke") | Call MVC().DispatchRequest("GET", "/dispatch-smoke") | ||||
| Call ASPUnit.Ok(dispatchActionRan, "Dispatch should reach a registered controller action without whitelist or lookup failures") | Call ASPUnit.Ok(dispatchActionRan, "Dispatch should reach a registered controller action without whitelist or lookup failures") | ||||
| End Function | End Function | ||||
| ' Regression test: MVC.ControllerName used to do "ControllerName = CurrentController", | |||||
| ' assigning an object to a property return without Set. Reading MVC.ControllerName | |||||
| ' after any controller had been dispatched raised "Object doesn't support this | |||||
| ' property or method" instead of returning a value. This confirms it now returns | |||||
| ' the dispatched controller's name as a plain string, with the "Controller" suffix | |||||
| ' stripped (matching the convention Active() and *StylesheetTag already rely on). | |||||
| Function ControllerNameReturnsDispatchedControllerNameAsString() | |||||
| Call MVC().DispatchRequest("GET", "/title-test") | |||||
| Dim controllerNameValue | |||||
| controllerNameValue = MVC().ControllerName | |||||
| Call ASPUnit.Ok((VarType(controllerNameValue) = vbString), "MVC.ControllerName should return a String, not the controller object") | |||||
| Call ASPUnit.Equal(controllerNameValue, "TitleTest", "MVC.ControllerName should return the dispatched controller's name with the Controller suffix stripped") | |||||
| End Function | |||||
| ' ControllerStylesheetTag, RedirectToAction, and the CSRF-invalid-token redirect | |||||
| ' helper all build a URL/asset path from MVC.ControllerName. This confirms that | |||||
| ' downstream use actually works now that ControllerName returns a real string, | |||||
| ' without needing to trigger a live Response.Redirect inside the test run. | |||||
| Function ControllerNameFeedsUrlGenerationForRedirects() | |||||
| Call MVC().DispatchRequest("GET", "/title-test") | |||||
| Dim generatedUrl | |||||
| generatedUrl = Routes.UrlTo(MVC().ControllerName, "Smoke", Empty) | |||||
| Call ASPUnit.Ok((InStr(LCase(generatedUrl), "/titletest/smoke") > 0), "URL built from MVC.ControllerName should contain the lowercase controller/action path, got: " & generatedUrl) | |||||
| End Function | |||||
| %> | %> | ||||
| @@ -6,7 +6,7 @@ | |||||
| <add key="CacheExpirationYear" value="2030" /> | <add key="CacheExpirationYear" value="2030" /> | ||||
| <add key="EnableCacheBusting" value="false" /> | <add key="EnableCacheBusting" value="false" /> | ||||
| <add key="CacheBustParamName" value="v" /> | <add key="CacheBustParamName" value="v" /> | ||||
| <add key="ProductionAppBaseUrl" value="http://localhost:8081/" /> | |||||
| <add key="ProductionAppBaseUrl" value="http://localhost:8080/" /> | |||||
| </appSettings> | </appSettings> | ||||
| <system.webServer> | <system.webServer> | ||||
| @@ -6,7 +6,7 @@ | |||||
| <add key="CacheExpirationYear" value="2030" /> | <add key="CacheExpirationYear" value="2030" /> | ||||
| <add key="EnableCacheBusting" value="false" /> | <add key="EnableCacheBusting" value="false" /> | ||||
| <add key="CacheBustParamName" value="v" /> | <add key="CacheBustParamName" value="v" /> | ||||
| <add key="ProductionAppBaseUrl" value="http://localhost:8081/" /> | |||||
| <add key="ProductionAppBaseUrl" value="http://localhost:8080/" /> | |||||
| </appSettings> | </appSettings> | ||||
| <system.webServer> | <system.webServer> | ||||
| @@ -6,7 +6,7 @@ | |||||
| <add key="CacheExpirationYear" value="2030" /> | <add key="CacheExpirationYear" value="2030" /> | ||||
| <add key="EnableCacheBusting" value="false" /> | <add key="EnableCacheBusting" value="false" /> | ||||
| <add key="CacheBustParamName" value="v" /> | <add key="CacheBustParamName" value="v" /> | ||||
| <add key="ProductionAppBaseUrl" value="http://localhost:8081/" /> | |||||
| <add key="ProductionAppBaseUrl" value="http://localhost:8080/" /> | |||||
| </appSettings> | </appSettings> | ||||
| <system.webServer> | <system.webServer> | ||||
Powered by TurnKey Linux.