| @@ -43,15 +43,32 @@ The `tests/` IIS application assumes the repository layout keeps `tests/`, `publ | |||
| ## 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/`. | |||
| 2. Create a separate development-only IIS application rooted at `tests/`. | |||
| 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`. | |||
| 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: | |||
| @@ -1,2 +1,10 @@ | |||
| <!--#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 | |||
| Dim pageTitle | |||
| If IsObject(CurrentController) Then | |||
| If IsObject(MVC.CurrentController) Then | |||
| On Error Resume Next | |||
| pageTitle = CurrentController.Title | |||
| pageTitle = MVC.CurrentController.Title | |||
| If Err.Number <> 0 Then | |||
| pageTitle = "RouteKit Classic ASP" | |||
| Err.Clear | |||
| @@ -162,6 +162,14 @@ | |||
| <binding protocol="http" bindingInformation=":8080:localhost" /> | |||
| </bindings> | |||
| </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> | |||
| <!-- To enable logging, please change the below attribute "enabled" to "true" --> | |||
| <logFile logFormat="W3C" directory="%AppData%\Microsoft\IISExpressLogs" enabled="false" /> | |||
| @@ -1,7 +1,8 @@ | |||
| <!--#include file="../Core/helpers.asp"--> | |||
| <!--#include file="../Core/lib.ErrorHandler.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.Data.asp"--> | |||
| <!--#include file="../Core/lib.Migrations.asp"--> | |||
| @@ -21,3 +22,27 @@ | |||
| <!--#include file="../Core/lib.Enumerable.asp"--> | |||
| <!--#include file="../Core/lib.ad.auth.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) | |||
| Dim conn | |||
| if IsWrappedInSingleQuotes(val) then | |||
| QuoteValue = val | |||
| Exit Function | |||
| @@ -92,6 +95,38 @@ Function IIf(condition, trueValue, falseValue) | |||
| On Error GoTo 0 | |||
| 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 | |||
| '----------------------------- | |||
| @@ -113,7 +113,7 @@ Class CDOEmail_Class | |||
| End If | |||
| ' Add attachments if any | |||
| Dim i | |||
| Dim i, errNum, errDesc | |||
| For i = LBound(arrAttachments) To UBound(arrAttachments) | |||
| msg.AddAttachment arrAttachments(i) | |||
| Next | |||
| @@ -374,8 +374,8 @@ Class LinkedList_Class | |||
| End Function | |||
| Public Function TO_Array() | |||
| Dim i, iter | |||
| Dim i, iter, retval | |||
| ReDim retval(Me.Count - 1) | |||
| i = 0 | |||
| Set iter = Me.Iterator | |||
| @@ -491,8 +491,8 @@ Class DynamicArray_Class | |||
| If e < s Then | |||
| Set Slice = DynamicArray() | |||
| Else | |||
| Dim retval, i, j | |||
| ReDim retval(e - s) | |||
| Dim i, j | |||
| j = 0 | |||
| For i = s to e | |||
| Assign retval(j), m_data(i) | |||
| @@ -515,7 +515,7 @@ Class DynamicArray_Class | |||
| End Function | |||
| Public Function TO_Array() | |||
| Dim i | |||
| Dim i, retval | |||
| ReDim retval(m_size - 1) | |||
| For i = 0 to UBOUND(retval) | |||
| 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 | |||
| Private m_connection | |||
| Private m_connection_string | |||
| Private m_shape_connections | |||
| Private m_trace_enabled | |||
| 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 | |||
| Private Sub Class_Initialize | |||
| Set m_shape_connections = Server.CreateObject("Scripting.Dictionary") | |||
| End Sub | |||
| '--------------------------------------------------------------------------------------------------------------------- | |||
| Public Sub Initialize(connection_string) | |||
| m_connection_string = connection_string | |||
| 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 | |||
| set rs = cmd.Execute(, params) | |||
| Set rs = cmd.Execute(, params) | |||
| ElseIf Not IsEmpty(params) then ' one parameter | |||
| set rs = cmd.Execute(, Array(params)) | |||
| Set rs = cmd.Execute(, Array(params)) | |||
| Else | |||
| set rs = cmd.Execute() | |||
| Set rs = cmd.Execute() | |||
| End If | |||
| set ShapeQuery = rs | |||
| Set ShapeQuery = rs | |||
| End Function | |||
| 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 | |||
| dim rs | |||
| If IsArray(params) then | |||
| set rs = cmd.Execute(, params) | |||
| Set rs = cmd.Execute(, params) | |||
| ElseIf Not IsEmpty(params) then ' one parameter | |||
| set rs = cmd.Execute(, Array(params)) | |||
| Set rs = cmd.Execute(, Array(params)) | |||
| Else | |||
| set rs = cmd.Execute() | |||
| Set rs = cmd.Execute() | |||
| End If | |||
| set Query = rs | |||
| Set Query = rs | |||
| 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) | |||
| 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.CommandType = 1 'adCmdText | |||
| cmd.ActiveConnection.CursorLocation = 3 'adUseClient | |||
| dim rs | |||
| If IsArray(params) then | |||
| set rs = cmd.Execute(, params) | |||
| Set rs = cmd.Execute(, params) | |||
| ElseIf Not IsEmpty(params) then ' one parameter | |||
| set rs = cmd.Execute(, Array(params)) | |||
| Set rs = cmd.Execute(, Array(params)) | |||
| Else | |||
| set rs = cmd.Execute() | |||
| Set rs = cmd.Execute() | |||
| End If | |||
| 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 | |||
| set PagedQuery = rs | |||
| Set PagedQuery = rs | |||
| End Function | |||
| '--------------------------------------------------------------------------------------------------------------------- | |||
| Public Sub [Execute](sql, params) | |||
| me.query sql, params | |||
| End Sub | |||
| '--------------------------------------------------------------------------------------------------------------------- | |||
| Public Sub BeginTransaction | |||
| Connection.BeginTrans | |||
| End Sub | |||
| Public Sub RollbackTransaction | |||
| Connection.RollbackTrans | |||
| End Sub | |||
| Public Sub CommitTransaction | |||
| Connection.CommitTrans | |||
| End Sub | |||
| '--------------------------------------------------------------------------------------------------------------------- | |||
| ' Private Methods | |||
| '--------------------------------------------------------------------------------------------------------------------- | |||
| Private Sub Class_terminate | |||
| 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 | |||
| Public Function Connection | |||
| if not isobject(m_connection) then | |||
| if not isobject(m_connection) then | |||
| set m_connection = Server.CreateObject("adodb.connection") | |||
| m_connection.open m_connection_string | |||
| end if | |||
| set Connection = m_connection | |||
| End Function | |||
| end Class | |||
| %> | |||
| %> | |||
| @@ -44,7 +44,7 @@ Class HTML_Helper_Class | |||
| Public Function LinkToUnless(condition, link_text, controller_name, action_name) | |||
| 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 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 | |||
| 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 | |||
| HashPassword = result | |||
| msgLen = UBound(msg) - LBound(msg) + 1 | |||
| 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 | |||
| 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) | |||
| 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 | |||
| %> | |||
| %> | |||
| @@ -1,4 +1,5 @@ | |||
| <% | |||
| Dim protocol | |||
| 'protocol = IIf(LCase(Request.ServerVariables("HTTPS")) = "1", "https", "http") | |||
| protocol = "https" | |||
| Dim timeZones | |||
| @@ -260,6 +260,7 @@ Class aspJSON | |||
| End Function | |||
| Private Function aj_ReadNumericValue(ByVal val) | |||
| Dim numdecimals | |||
| If Instr(val, ".") > 0 Then | |||
| numdecimals = Len(val) - Instr(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" --> | |||
| <% | |||
| @@ -7,7 +16,31 @@ | |||
| router.AddRoute "GET", "", "HomeController", "Index" | |||
| 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 | |||
| setlocal | |||
| 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="EnableCacheBusting" value="false" /> | |||
| <add key="CacheBustParamName" value="v" /> | |||
| <add key="ProductionAppBaseUrl" value="http://localhost:8081/" /> | |||
| <add key="ProductionAppBaseUrl" value="http://localhost:8080/" /> | |||
| </appSettings> | |||
| <system.webServer> | |||
| @@ -1,6 +1,7 @@ | |||
| <!-- #include file="../aspunit/Lib/ASPUnit.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 | |||
| @@ -31,12 +32,70 @@ Function TestDispatchController() | |||
| Set TestDispatchController = TestDispatchController_Class__Singleton | |||
| 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( _ | |||
| ASPUnit.CreateModule( _ | |||
| "MVC Dispatch Smoke Tests", _ | |||
| Array( _ | |||
| ASPUnit.CreateTest("RootRouteResolvesToHomeController"), _ | |||
| ASPUnit.CreateTest("KnownRouteDispatchCompletesWithoutLookupFailure") _ | |||
| ASPUnit.CreateTest("KnownRouteDispatchCompletesWithoutLookupFailure"), _ | |||
| ASPUnit.CreateTest("ControllerNameReturnsDispatchedControllerNameAsString"), _ | |||
| ASPUnit.CreateTest("ControllerNameFeedsUrlGenerationForRedirects") _ | |||
| ), _ | |||
| ASPUnit.CreateLifeCycle("SetupMvcDispatch", "TeardownMvcDispatch") _ | |||
| ) _ | |||
| @@ -47,20 +106,29 @@ Call ASPUnit.Run() | |||
| Sub SetupMvcDispatch() | |||
| Call ResetTestRuntime() | |||
| On Error Resume Next | |||
| MVC_Dispatcher_Class__Singleton = Empty | |||
| TestDispatchController_Class__Singleton = Empty | |||
| TitleTestController_Class__Singleton = Empty | |||
| Set MVC_Instance = Nothing | |||
| On Error GoTo 0 | |||
| Call ExecuteGlobal("Dim dispatchActionRan") | |||
| dispatchActionRan = False | |||
| Call RegisterDefaultRoutes() | |||
| Call EnsureTestMvc() | |||
| Call ControllerRegistry().RegisterController("testdispatchcontroller") | |||
| 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 | |||
| Sub TeardownMvcDispatch() | |||
| On Error Resume Next | |||
| MVC_Dispatcher_Class__Singleton = Empty | |||
| Set MVC_Instance = Nothing | |||
| TestDispatchController_Class__Singleton = Empty | |||
| TitleTestController_Class__Singleton = Empty | |||
| Response.Status = "200 OK" | |||
| On Error GoTo 0 | |||
| Call ResetTestRuntime() | |||
| @@ -77,4 +145,33 @@ Function KnownRouteDispatchCompletesWithoutLookupFailure() | |||
| Call MVC().DispatchRequest("GET", "/dispatch-smoke") | |||
| Call ASPUnit.Ok(dispatchActionRan, "Dispatch should reach a registered controller action without whitelist or lookup failures") | |||
| 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="EnableCacheBusting" value="false" /> | |||
| <add key="CacheBustParamName" value="v" /> | |||
| <add key="ProductionAppBaseUrl" value="http://localhost:8081/" /> | |||
| <add key="ProductionAppBaseUrl" value="http://localhost:8080/" /> | |||
| </appSettings> | |||
| <system.webServer> | |||
| @@ -6,7 +6,7 @@ | |||
| <add key="CacheExpirationYear" value="2030" /> | |||
| <add key="EnableCacheBusting" value="false" /> | |||
| <add key="CacheBustParamName" value="v" /> | |||
| <add key="ProductionAppBaseUrl" value="http://localhost:8081/" /> | |||
| <add key="ProductionAppBaseUrl" value="http://localhost:8080/" /> | |||
| </appSettings> | |||
| <system.webServer> | |||
| @@ -6,7 +6,7 @@ | |||
| <add key="CacheExpirationYear" value="2030" /> | |||
| <add key="EnableCacheBusting" value="false" /> | |||
| <add key="CacheBustParamName" value="v" /> | |||
| <add key="ProductionAppBaseUrl" value="http://localhost:8081/" /> | |||
| <add key="ProductionAppBaseUrl" value="http://localhost:8080/" /> | |||
| </appSettings> | |||
| <system.webServer> | |||
Powered by TurnKey Linux.