diff --git a/TESTING.md b/TESTING.md index c2f13aa..ac4012d 100644 --- a/TESTING.md +++ b/TESTING.md @@ -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/` + +`` 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 `` 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 `` 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 `` in that app's own web.config). `tests/web.config` does not set this itself; on IIS Express it comes from the server-wide `` 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: diff --git a/app/controllers/autoload_controllers.asp b/app/controllers/autoload_controllers.asp index 425e8a9..ee78872 100644 --- a/app/controllers/autoload_controllers.asp +++ b/app/controllers/autoload_controllers.asp @@ -1,2 +1,10 @@ - \ No newline at end of file + +<% +' 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")) +%> diff --git a/app/views/shared/header.asp b/app/views/shared/header.asp index 03fce57..40f627c 100644 --- a/app/views/shared/header.asp +++ b/app/views/shared/header.asp @@ -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 diff --git a/applicationhost.config b/applicationhost.config index 8d6b56d..c2ce470 100644 --- a/applicationhost.config +++ b/applicationhost.config @@ -162,6 +162,14 @@ + + + + + + + + diff --git a/core/autoload_core.asp b/core/autoload_core.asp index 6b406b8..b0d4060 100644 --- a/core/autoload_core.asp +++ b/core/autoload_core.asp @@ -1,7 +1,8 @@ - + + @@ -21,3 +22,27 @@ +<% +' 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 +%> diff --git a/core/helpers.asp b/core/helpers.asp index ca87a1e..cdb18dc 100644 --- a/core/helpers.asp +++ b/core/helpers.asp @@ -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 "
" + + If errInfo("Type") = "Security" Then + Response.Write "Security Error: " & Server.HTMLEncode(errInfo("Message")) + ElseIf isDevelopment Then + Response.Write "Controller Action Error
" + Response.Write "Action: " & Server.HTMLEncode(errInfo("ActionName")) & "
" + Response.Write "Error: " & Server.HTMLEncode(errInfo("Description")) & "
" + Response.Write "Error Number: " & errInfo("Number") + Else + Response.Write "An error occurred
" + Response.Write "Please contact the system administrator if the problem persists." + End If + + Response.Write "
" +End Sub + '----------------------------- ' Utility: Generic Error Reporter '----------------------------- diff --git a/core/lib.CDOEmail.asp b/core/lib.CDOEmail.asp index e55a55f..306e768 100644 --- a/core/lib.CDOEmail.asp +++ b/core/lib.CDOEmail.asp @@ -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 diff --git a/core/lib.Collections.asp b/core/lib.Collections.asp index 60ad607..eff7385 100644 --- a/core/lib.Collections.asp +++ b/core/lib.Collections.asp @@ -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) diff --git a/core/lib.ControllerFactory.asp b/core/lib.ControllerFactory.asp new file mode 100644 index 0000000..cc982e8 --- /dev/null +++ b/core/lib.ControllerFactory.asp @@ -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 +%> diff --git a/core/lib.Data.asp b/core/lib.Data.asp index 4750c85..8d6671f 100644 --- a/core/lib.Data.asp +++ b/core/lib.Data.asp @@ -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 -%> \ No newline at end of file +%> diff --git a/core/lib.HTML.asp b/core/lib.HTML.asp index a993f23..ff27e80 100644 --- a/core/lib.HTML.asp +++ b/core/lib.HTML.asp @@ -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 diff --git a/core/lib.crypto.helper.asp b/core/lib.crypto.helper.asp index e6bdd54..b29ecc5 100644 --- a/core/lib.crypto.helper.asp +++ b/core/lib.crypto.helper.asp @@ -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 -%> \ No newline at end of file +%> diff --git a/core/lib.helpers.asp b/core/lib.helpers.asp index b2ae6d6..fee7a7c 100644 --- a/core/lib.helpers.asp +++ b/core/lib.helpers.asp @@ -1,4 +1,5 @@ <% +Dim protocol 'protocol = IIf(LCase(Request.ServerVariables("HTTPS")) = "1", "https", "http") protocol = "https" Dim timeZones diff --git a/core/lib.json.asp b/core/lib.json.asp index 38bef36..ee17ba3 100644 --- a/core/lib.json.asp +++ b/core/lib.json.asp @@ -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, ".", "")) diff --git a/core/mvc.asp b/core/mvc.asp deleted file mode 100644 index 0bbf373..0000000 --- a/core/mvc.asp +++ /dev/null @@ -1,166 +0,0 @@ - -<% -' 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 "
" - Response.Write "Security Error: Invalid controller name format." - Response.Write "
" - Exit Sub - End If - - If Not ControllerRegistry.IsValidActionFormat(actionName) Then - Response.Write "
" - Response.Write "Security Error: Invalid action name format." - Response.Write "
" - Exit Sub - End If - - ' Security: Check controller whitelist - If Not ControllerRegistry.IsValidController(controllerName) Then - Response.Write "
" - Response.Write "Security Error: Controller '" & Server.HTMLEncode(controllerName) & "' is not registered." - Response.Write "
" - 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 - %> <% - 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 - %> <% - 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 "
" - Response.Write "Controller Action Error
" - Response.Write "Action: " & Server.HTMLEncode(actionName) & "
" - Response.Write "Error: " & Server.HTMLEncode(errorDesc) & "
" - Response.Write "Error Number: " & errorNum - Response.Write "
" - Else - Response.Write "
" - Response.Write "An error occurred
" - Response.Write "Please contact the system administrator if the problem persists." - Response.Write "
" - 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 -%> \ No newline at end of file diff --git a/core/mvc.wsc b/core/mvc.wsc new file mode 100644 index 0000000..a31b13b --- /dev/null +++ b/core/mvc.wsc @@ -0,0 +1,320 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/AGENTS.md b/docs/AGENTS.md deleted file mode 100644 index ae946db..0000000 --- a/docs/AGENTS.md +++ /dev/null @@ -1,1543 +0,0 @@ -# AGENTS.md - -## Purpose - -This file defines the development rules, architecture standards, safety requirements, and agent behavior for this project. - -All AI coding agents working in this repository must read this file before making changes. - -The primary development environment is: - -- ASP Classic -- VBScript -- ADODB -- IIS -- HTML/CSS/JavaScript -- SQL Server, Microsoft Access, SQLite, or another configured database - -VBScript provides limited compile-time validation, weak typing, limited object-oriented features, and minimal runtime safeguards. - -AI agents must compensate for these limitations through: - -- strict conventions -- defensive programming -- architectural boundaries -- validation -- testing -- code inspection -- documentation -- controlled self-improvement - ---- - -# 1. Core Principle - -The AI agent must behave as more than a code generator. - -It must act as: - -- developer -- architect -- static analyzer -- compiler substitute -- security reviewer -- code reviewer -- documentation maintainer -- test author -- project historian - -The agent must actively look for problems that VBScript itself may not catch. - ---- - -# 2. Instruction Priority - -When instructions conflict, follow this priority: - -1. Explicit user instructions -2. Security and data protection requirements -3. This AGENTS.md file -4. Project architecture documentation -5. Existing project conventions -6. Existing implementation patterns -7. Agent preferences - -Never weaken security, validation, or data integrity simply to match poor legacy code. - -If legacy code conflicts with these standards, preserve compatibility where required while gradually improving the implementation. - ---- - -# 3. Mandatory VBScript Rules - -## 3.1 Option Explicit - -Every ASP, VBS, and applicable WSC script must use: - -```vbscript -Option Explicit -``` - -No undeclared variables are permitted. - -Before finishing a task, inspect all changed code for undeclared or misspelled identifiers. - ---- - -## 3.2 Variable Declaration - -All variables must be explicitly declared using an appropriate declaration: - -```vbscript -Dim -Private -Public -Const -``` - -Avoid excessive variable reuse. - -Prefer descriptive names. - -Bad: - -```vbscript -Dim x -Dim y -``` - -Better: - -```vbscript -Dim customerId -Dim orderTotal -``` - ---- - -## 3.3 Variant Safety - -VBScript uses Variants extensively. - -Do not assume that a value has the expected type. - -External values must be normalized near application boundaries. - -Use explicit conversion where appropriate: - -```vbscript -CStr() -CLng() -CInt() -CDbl() -CBool() -CDate() -``` - -Never convert unvalidated input blindly. - -Validate first when conversion can fail. - ---- - -# 4. Null, Empty, and Nothing - -These are different states and must not be treated as equivalent. - -Use: - -```vbscript -IsNull(value) -IsEmpty(value) -object Is Nothing -``` - -Database values may contain `Null`. - -Uninitialized Variants may contain `Empty`. - -Object references may contain `Nothing`. - -Code must explicitly account for the state that is expected. - -Do not rely on accidental VBScript coercion. - ---- - -# 5. Request Data Is Untrusted - -Treat all external input as untrusted, including: - -- Request.Form -- Request.QueryString -- Request.Cookies -- Request.ServerVariables -- HTTP headers -- uploaded files -- JSON payloads -- XML payloads -- API data -- database data originating from users -- Session values derived from user input - -External data must pass through: - -```text -Input - ↓ -Validation - ↓ -Normalization - ↓ -Authorization - ↓ -Business Logic -``` - -Never pass raw Request values directly into database or business operations. - ---- - -# 6. SQL Rules - -## 6.1 Parameterization Is Mandatory - -User-controlled data must never be concatenated into SQL. - -Prohibited: - -```vbscript -sql = "SELECT * FROM Users WHERE Email = '" & email & "'" -``` - -Required: - -```vbscript -Set cmd = Server.CreateObject("ADODB.Command") - -Set cmd.ActiveConnection = conn - -cmd.CommandText = _ - "SELECT UserId, Email " & _ - "FROM Users " & _ - "WHERE Email = ?" - -cmd.Parameters.Append _ - cmd.CreateParameter("@Email", adVarWChar, adParamInput, 255, email) -``` - -Use parameters for: - -- SELECT -- INSERT -- UPDATE -- DELETE -- stored procedure calls - ---- - -## 6.2 Dynamic SQL - -When identifiers such as table names or sort columns must be dynamic, they cannot normally be parameterized. - -In such cases, use a whitelist. - -Example: - -```vbscript -Select Case sortField - Case "Name", "CreatedDate", "Status" - ' allowed - Case Else - sortField = "CreatedDate" -End Select -``` - -Never accept arbitrary table names, column names, or SQL fragments from users. - ---- - -# 7. Output Encoding - -HTML output must be encoded by default. - -Prefer: - -```vbscript -Server.HTMLEncode(value) -``` - -or a centralized helper such as: - -```vbscript -Function H(value) - If IsNull(value) Then - H = "" - Else - H = Server.HTMLEncode(CStr(value)) - End If -End Function -``` - -Raw HTML output should only be used when the value is explicitly trusted and intentionally contains HTML. - -Do not assume database data is safe to output without encoding. - ---- - -# 8. Error Handling - -## 8.1 On Error Resume Next - -Broad use of: - -```vbscript -On Error Resume Next -``` - -is prohibited. - -It may only be used around the smallest practical operation requiring error interception. - -Required pattern: - -```vbscript -On Error Resume Next - -Set rs = cmd.Execute() - -If Err.Number <> 0 Then - errorNumber = Err.Number - errorDescription = Err.Description - - Err.Clear - On Error GoTo 0 - - LogError errorNumber, errorDescription - - Err.Raise _ - vbObjectError + 1000, _ - "CustomerRepository.GetCustomer", _ - "Database operation failed." -End If - -On Error GoTo 0 -``` - -Always restore normal error handling. - ---- - -## 8.2 Never Hide Failures - -Do not silently ignore: - -- failed database operations -- failed file operations -- invalid conversions -- failed COM object creation -- authorization failures -- missing required configuration - -Failures must either: - -- be handled intentionally, or -- be logged and propagated appropriately - ---- - -# 9. Architecture - -The preferred application flow is: - -```text -HTTP Request - ↓ -Router - ↓ -Controller - ↓ -Service - ↓ -Repository - ↓ -Database -``` - -Views receive prepared data from controllers or ViewModels. - ---- - -# 10. Controller Rules - -Controllers may: - -- read Request data -- invoke validation -- invoke services -- prepare ViewModels -- redirect -- select views -- set HTTP status codes -- write responses through response abstractions - -Controllers must not: - -- contain SQL -- implement significant business logic -- directly manage recordsets -- duplicate validation rules -- construct complex HTML -- hide application failures - -Keep controllers thin. - ---- - -# 11. Service Rules - -Services contain application and business logic. - -Services may: - -- coordinate repositories -- enforce business rules -- perform calculations -- enforce workflow rules -- call domain-specific collaborators - -Services must not: - -- access Request directly -- access Response directly -- generate HTML -- contain raw SQL -- depend unnecessarily on ASP global state - -Prefer dependency injection through explicit initialization. - ---- - -# 12. Repository Rules - -Repositories own persistence logic. - -Repositories may: - -- execute ADODB commands -- map records to models -- create parameterized queries -- manage database-specific concerns - -Repositories must not: - -- access Request -- access Response -- generate HTML -- contain UI rules - ---- - -# 13. View Rules - -Views may: - -- render HTML -- display ViewModel data -- use simple formatting helpers - -Views must not: - -- execute SQL -- create database connections -- contain major business logic -- make authorization decisions -- perform complex data transformations - -Encode dynamic output by default. - ---- - -# 14. Models and ViewModels - -Models should represent application or domain data. - -ViewModels should represent data specifically prepared for a view. - -Do not pass ADODB.Recordset objects directly into views unless the existing architecture explicitly requires it and refactoring is not currently practical. - -Prefer converting recordsets into application-friendly structures. - ---- - -# 15. Dependency Management - -VBScript does not provide modern dependency injection. - -Use explicit initialization. - -Example: - -```vbscript -Class CustomerService - - Private m_repository - Private m_logger - - Public Sub Init(repository, logger) - Set m_repository = repository - Set m_logger = logger - End Sub - -End Class -``` - -Dependencies should be visible. - -Avoid hidden global dependencies where practical. - ---- - -# 16. Composition Over Inheritance - -VBScript has limited inheritance capabilities. - -Prefer composition. - -Instead of designing deep pseudo-inheritance systems, compose objects from smaller collaborators. - -Example: - -```text -CustomerService - ├── CustomerRepository - ├── CustomerValidator - └── Logger -``` - ---- - -# 17. Interface Conventions - -VBScript cannot formally declare interfaces. - -When interchangeable implementations are needed, define a documented contract. - -Example: - -```text -IUserRepository - -Required members: - -GetById(userId) -GetByEmail(email) -Create(user) -Update(user) -Delete(userId) -``` - -Agents must verify that implementations satisfy documented contracts. - ---- - -# 18. Object Initialization - -Use `Class_Initialize` only for safe initialization that requires no external dependencies. - -Use an explicit method such as: - -```vbscript -Public Sub Init(...) -``` - -for dependency injection. - -Classes should not secretly fetch dependencies from globals when explicit injection is practical. - ---- - -# 19. Session and Application State - -Avoid storing custom COM objects, recordsets, service instances, or application classes in: - -```vbscript -Session -Application -``` - -Prefer primitive values: - -```vbscript -Session("UserId") = CLng(userId) -Session("Username") = CStr(username) -``` - -Use Application state carefully and protect shared writes appropriately. - ---- - -# 20. ADODB Resource Management - -Explicitly close database resources. - -Example: - -```vbscript -If Not rs Is Nothing Then - If rs.State <> 0 Then - rs.Close - End If - - Set rs = Nothing -End If -``` - -Connections must also be closed when owned by the current operation. - -Do not leave recordsets or connections open unnecessarily. - ---- - -# 21. Connection Ownership - -The project should clearly define who owns a database connection. - -A method that opens a connection is normally responsible for closing it. - -Do not close a connection owned by another component unless the contract explicitly states that responsibility. - ---- - -# 22. Small Functions - -Prefer small cohesive procedures. - -Target guideline: - -```text -Function: -10–40 lines where practical - -Class: -100–300 lines where practical - -Controller: -preferably under 200 lines -``` - -These are guidelines, not absolute limits. - -When code becomes difficult to understand, split it by responsibility. - ---- - -# 23. Naming Conventions - -Use consistent naming because VBScript is case-insensitive and offers limited compiler assistance. - -Preferred examples: - -```text -customerId -customerName -orderRepository -customerService -isValid -hasPermission -``` - -Class names: - -```text -CustomerService -CustomerRepository -CustomerValidator -OrderController -``` - -Private fields may use: - -```text -m_repository -m_logger -m_customerId -``` - -Follow established project conventions if they are consistent and clear. - ---- - -# 24. Boolean Naming - -Prefer names that read naturally: - -```text -isValid -isActive -hasAccess -hasPermission -canDelete -shouldRetry -``` - -Avoid ambiguous names such as: - -```text -flag -value1 -test -status2 -``` - ---- - -# 25. Magic Values - -Avoid unexplained magic values. - -Bad: - -```vbscript -If status = 4 Then -``` - -Better: - -```vbscript -Const STATUS_COMPLETED = 4 -``` - -Use constants or centralized configuration where appropriate. - ---- - -# 26. Configuration - -Environment-specific settings must not be scattered throughout application code. - -Centralize: - -- connection strings -- filesystem paths -- URLs -- email settings -- feature flags -- API keys -- environment names - -Secrets must not be committed to source control. - ---- - -# 27. Logging - -Important errors should be logged with useful context. - -Prefer structured information such as: - -```text -Timestamp -RequestId -UserId if appropriate -Component -Operation -Error number -Error description -Relevant safe context -``` - -Never log: - -- passwords -- authentication tokens -- full credit card data -- secret keys -- highly sensitive personal data unless explicitly necessary and protected - ---- - -# 28. Authentication and Authorization - -Authentication answers: - -```text -Who is this user? -``` - -Authorization answers: - -```text -Is this user allowed to perform this operation? -``` - -They are not interchangeable. - -Authorization must be enforced server-side. - -Never rely solely on hidden buttons, JavaScript, or UI restrictions. - ---- - -# 29. CSRF Protection - -State-changing requests should use CSRF protection where practical. - -This includes: - -- POST -- PUT-like actions -- DELETE-like actions -- administrative operations - -Do not perform destructive actions solely through unprotected GET requests. - ---- - -# 30. HTTP Method Semantics - -Prefer: - -```text -GET → retrieve data -POST → create/change state -``` - -Avoid state-changing GET endpoints unless required by legacy compatibility. - ---- - -# 31. Redirect Safety - -Do not redirect to arbitrary user-provided URLs. - -Validate redirects against: - -- known internal routes -- allowed hosts -- approved paths - ---- - -# 32. File Handling - -Uploaded files must be treated as untrusted. - -Validate: - -- filename -- extension -- actual content where practical -- size -- target path -- permissions - -Prevent path traversal. - -Never construct filesystem paths directly from arbitrary user input. - ---- - -# 33. Include File Discipline - -Includes are dependencies, not architecture. - -Avoid large global include chains. - -Prefer explicit grouped includes such as: - -```text -/config -/framework -/app/controllers -/app/services -/app/repositories -/app/models -/app/views -``` - -Before adding a new include, check whether the functionality belongs in an existing component. - ---- - -# 34. Shared Utilities - -Before creating a utility function, search for an existing implementation. - -Common shared helpers may include: - -```text -HtmlEncode -JsonEncode -JsonEscape -Nz -ToInteger -ToLong -ToBoolean -ToDate -ValidateEmail -ExecuteScalar -ExecuteNonQuery -Logger -UrlEncode -GenerateGuid -``` - -Do not create duplicate helpers with slightly different behavior. - ---- - -# 35. JSON - -Do not build complex JSON through unsafe string concatenation. - -Use the project's JSON serializer or centralized JSON utilities. - -Escape: - -- quotes -- backslashes -- control characters -- line breaks - -Ensure output is valid JSON. - ---- - -# 36. JavaScript Boundaries - -Server-side validation is mandatory even when client-side validation exists. - -JavaScript validation is for user experience. - -VBScript validation is for correctness and security. - -Never trust browser-side validation alone. - ---- - -# 37. Testing Expectations - -Every significant change should be evaluated for testability. - -Where practical, create tests for: - -- validation -- business rules -- conversions -- repositories -- security-sensitive logic -- regressions - -Business logic should be separated from ASP globals so it can be tested independently. - ---- - -# 38. Regression Prevention - -When fixing a bug: - -1. understand the root cause -2. identify the violated invariant -3. fix the underlying problem -4. add or update a test where practical -5. search for similar occurrences elsewhere -6. update documentation if the rule was previously unclear - -Do not patch symptoms repeatedly. - ---- - -# 39. Agent Pre-Change Procedure - -Before modifying code, the agent should: - -1. Read this AGENTS.md. -2. Read CLAUDE.md if present. -3. Inspect the relevant project files. -4. Search for existing implementations. -5. Understand the dependency direction. -6. Identify security implications. -7. Determine whether tests exist. -8. Avoid creating duplicate abstractions. - -For small changes, do this proportionally. - ---- - -# 40. Agent Post-Change Review - -Before declaring work complete, review changed code for: - -- missing Option Explicit -- undeclared variables -- misspelled identifiers -- missing Set statements -- incorrect parameter counts -- Null handling -- Empty handling -- Nothing handling -- unsafe implicit conversion -- SQL injection -- unparameterized SQL -- XSS -- missing HTML encoding -- unsafe redirects -- path traversal -- authorization bypass -- broad On Error Resume Next -- swallowed errors -- open recordsets -- open database connections -- excessive global state -- duplicate utilities -- architectural boundary violations -- unreachable code -- dead code -- missing cleanup -- poor naming - ---- - -# 41. Missing Set Check - -VBScript requires `Set` for object assignment. - -Agents must specifically inspect object assignments. - -Incorrect: - -```vbscript -repo = New CustomerRepository -``` - -Correct: - -```vbscript -Set repo = New CustomerRepository -``` - -This should be part of every static review. - ---- - -# 42. Return Value Check - -VBScript functions return values by assigning to the function name. - -Example: - -```vbscript -Function AddNumbers(a, b) - AddNumbers = a + b -End Function -``` - -Agents must verify all execution paths return the expected value when a return value is required. - ---- - -# 43. ByRef Awareness - -VBScript parameters are `ByRef` by default. - -This can unintentionally mutate caller values. - -Prefer explicitly declaring intent. - -Use: - -```vbscript -Function ValidateCustomer(ByVal customer) -``` - -when modification of the caller's variable is not intended. - -Use `ByRef` intentionally. - ---- - -# 44. Parentheses and Call Syntax - -VBScript procedure-call syntax can be confusing. - -Agents must generate syntactically valid VBScript calls. - -Be especially careful with: - -- `Call` -- parentheses -- functions used for return values -- Subs invoked without `Call` - -Prefer simple consistent calling patterns. - ---- - -# 45. Date Handling - -Do not assume date string formats. - -Prefer true Date values internally. - -Validate and convert input explicitly. - -Be aware of: - -- server locale -- database date formats -- regional settings - -Use parameterized database values rather than embedding formatted date strings in SQL. - ---- - -# 46. Numeric Handling - -Do not assume values from Request are numeric. - -Validate before conversion. - -Example logic: - -```text -Read input -↓ -Trim -↓ -Check required/optional -↓ -Validate numeric format -↓ -Convert -``` - -Handle overflow and invalid values appropriately. - ---- - -# 47. Database Null Mapping - -When mapping database records to models, define expected behavior for Null fields. - -Do not allow arbitrary Null propagation unless the domain explicitly permits it. - -Consider centralized helpers. - ---- - -# 48. Performance - -ASP Classic is synchronous. - -Avoid unnecessary: - -- database round trips -- repeated queries inside loops -- filesystem calls -- external HTTP requests -- COM object creation -- large Session values - -Watch for N+1 query patterns. - -Prefer set-based SQL. - ---- - -# 49. Caching - -Caching may be used where appropriate but must have: - -- clear ownership -- expiration behavior -- invalidation rules -- concurrency awareness - -Never cache sensitive per-user data globally without proper separation. - ---- - -# 50. Application State Concurrency - -ASP `Application` state can be shared across requests. - -When modifying shared Application values, consider: - -```vbscript -Application.Lock -Application.Unlock -``` - -Keep locked regions minimal. - ---- - -# 51. Backward Compatibility - -When modifying legacy code: - -1. identify existing behavior -2. determine whether other code depends on it -3. preserve public contracts where practical -4. avoid unrelated rewrites -5. improve internals incrementally - -Do not modernize code purely for style if it introduces unnecessary risk. - ---- - -# 52. Refactoring Rule - -Refactoring should preserve observable behavior unless changing behavior is an explicit goal. - -Separate: - -```text -behavior change -``` - -from: - -```text -structural cleanup -``` - -where practical. - ---- - -# 53. Documentation Rule - -When introducing: - -- a new architecture convention -- a new shared component -- a new security requirement -- a new dependency -- a new recurring coding pattern - -update relevant documentation. - -Do not leave important architectural knowledge only in source code. - ---- - -# 54. Self-Improvement System - -This file is allowed to evolve. - -Agents may propose or make updates to AGENTS.md when project experience reveals that the instructions are: - -- incomplete -- outdated -- ambiguous -- repeatedly violated -- causing bugs -- missing an important security rule -- missing an architectural convention -- inconsistent with the actual project -- superseded by a better proven pattern - -Self-improvement must be controlled. - ---- - -# 55. When the Agent Should Update AGENTS.md - -Consider updating this file when: - -1. the same mistake occurs more than once -2. a bug reveals a missing development rule -3. a new framework component establishes a reusable pattern -4. a new security requirement becomes necessary -5. project architecture materially changes -6. a repeated manual review step can become an explicit rule -7. a legacy convention is officially replaced -8. new infrastructure becomes standard -9. a user explicitly establishes a permanent project rule - -Do not update this file for trivial one-time implementation details. - ---- - -# 56. Self-Update Safety - -An agent must never silently weaken these core protections: - -- parameterized SQL -- output encoding -- authorization -- input validation -- controlled error handling -- explicit variable declarations -- Option Explicit -- resource cleanup -- security boundaries - -Changes affecting these protections require explicit justification. - ---- - -# 57. Rule Classification - -Rules may be considered: - -```text -CORE -PROJECT -ADVISORY -TEMPORARY -``` - -CORE rules should rarely change. - -Examples: - -```text -CORE: -SQL parameterization -Option Explicit -server-side authorization -output encoding - -PROJECT: -folder structure -repository naming -routing conventions - -ADVISORY: -function length guidelines - -TEMPORARY: -migration-specific compatibility rules -``` - -When adding an important rule, identify its conceptual category if useful. - ---- - -# 58. Learning From Bugs - -After fixing a significant bug, ask: - -```text -Could a rule have prevented this? -``` - -If yes: - -- update AGENTS.md, or -- update a specialized project document, or -- add a test, or -- improve a framework abstraction - -Prefer systemic prevention over repeated manual correction. - ---- - -# 59. Learning From Code Review - -If a code review repeatedly identifies the same problem, convert the feedback into: - -- an explicit agent rule -- a reusable helper -- a test -- an architectural constraint - -The goal is continuous reduction of repeated mistakes. - ---- - -# 60. Update History - -Meaningful changes to AGENTS.md should be recorded below. - -Keep entries concise. - -Format: - -```text -YYYY-MM-DD -- Added rule: -- Reason: -``` - -Example: - -```text -2026-09-02 -- Added explicit ByVal guidance. -- Reason: VBScript defaults to ByRef and accidental mutations were difficult to detect. -``` - ---- - -# 61. Do Not Turn This File Into a Dumping Ground - -AGENTS.md should contain durable development knowledge. - -Do not store: - -- task-specific notes -- temporary debugging output -- one-time TODOs -- credentials -- secrets -- personal information -- giant code examples - -Move specialized guidance into focused files when this file becomes too large. - ---- - -# 62. Specialized Skills - -As the project grows, agents may create focused guidance such as: - -```text -/docs/architecture.md -/docs/security.md -/docs/database.md -/docs/testing.md - -/skills/asp-classic.md -/skills/adodb.md -/skills/security.md -/skills/testing.md -``` - -AGENTS.md remains the central index and policy authority. - ---- - -# 63. Creating New Skills - -Agents may create a new skill or focused guidance document when: - -- a task is repeated frequently -- the knowledge is specialized -- the instructions are too detailed for AGENTS.md -- the process can be reused -- consistent execution would materially improve quality - -A new skill should state: - -```text -Purpose -When to use it -Inputs -Procedure -Rules -Validation -Common failure modes -Output expectations -``` - ---- - -# 64. Updating Existing Skills - -When project behavior changes, update the existing relevant skill instead of creating a duplicate. - -Before creating a new document, search for an existing location for that knowledge. - ---- - -# 65. Agent Decision Journal - -For major architectural changes, record concise rationale in the appropriate documentation. - -Document: - -```text -Decision -Context -Alternatives -Reason selected -Consequences -``` - -Do not record private chain-of-thought. - -Record only useful engineering rationale. - ---- - -# 66. Definition of Done - -A task is not complete merely because the code runs. - -A change is complete when appropriate checks have been performed for: - -```text -Correctness -Security -Architecture -Compatibility -Validation -Error handling -Resource cleanup -Testing -Documentation -Maintainability -``` - ---- - -# 67. Final Agent Checklist - -Before finishing a coding task, answer internally: - -```text -[ ] Did I read the relevant instructions? -[ ] Did I inspect existing code before creating new code? -[ ] Did I preserve architecture boundaries? -[ ] Is all external input validated? -[ ] Is SQL parameterized? -[ ] Is dynamic HTML encoded? -[ ] Is authorization enforced server-side? -[ ] Are errors handled intentionally? -[ ] Are database resources cleaned up? -[ ] Are object assignments using Set? -[ ] Did I account for Null, Empty, and Nothing? -[ ] Did I account for ByRef behavior? -[ ] Did I avoid unnecessary global state? -[ ] Did I avoid duplicating utilities? -[ ] Did I update tests where appropriate? -[ ] Did I update documentation if a durable rule changed? -[ ] Did this task reveal something the agent rules should learn? -``` - ---- - -# 68. Guiding Philosophy - -Do not fight VBScript. - -Compensate for its weaknesses. - -Prefer simple, explicit, understandable code over clever abstractions. - -The desired result is: - -```text -Classic ASP simplicity - + -strict engineering discipline - + -AI-assisted static review - + -modern security practices - = -maintainable ASP Classic applications -``` - ---- - -# Update History - -2026-09-02 -- Initial ASP Classic/VBScript AI development policy created. -- Added controlled self-improvement rules. -- Added architecture, security, validation, ADODB, error handling, testing, and agent review requirements. diff --git a/docs/CLAUDE.md b/docs/CLAUDE.md deleted file mode 100644 index c19f92d..0000000 --- a/docs/CLAUDE.md +++ /dev/null @@ -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. diff --git a/public/Default.asp b/public/Default.asp index 61c5a91..0c9c28a 100644 --- a/public/Default.asp +++ b/public/Default.asp @@ -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 +%> <% @@ -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 +%><% + End If + + MVC.ExecuteAction() + + If Not (MVC.LastError Is Nothing) Then + RenderMvcError MVC.LastError + End If + + If MVC.UseLayout Then +%><% + End If + End If %> diff --git a/run_site.cmd b/run_site.cmd index 2f87205..e564eb5 100644 --- a/run_site.cmd +++ b/run_site.cmd @@ -1,4 +1,16 @@ @echo off setlocal set "ASPC_STARTER_ROOT=%~dp0" -"C:\Program Files\IIS Express\iisexpress.exe" /config:"%~dp0applicationhost.config" \ No newline at end of file +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" diff --git a/tests/component/web.config b/tests/component/web.config index 3bf406c..e67b96c 100644 --- a/tests/component/web.config +++ b/tests/component/web.config @@ -6,7 +6,7 @@ - + diff --git a/tests/integration/TestMvcDispatch.asp b/tests/integration/TestMvcDispatch.asp index 9a8d803..78cafb7 100644 --- a/tests/integration/TestMvcDispatch.asp +++ b/tests/integration/TestMvcDispatch.asp @@ -1,6 +1,7 @@ - + + <% 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 %> diff --git a/tests/integration/web.config b/tests/integration/web.config index 3bf406c..e67b96c 100644 --- a/tests/integration/web.config +++ b/tests/integration/web.config @@ -6,7 +6,7 @@ - + diff --git a/tests/unit/web.config b/tests/unit/web.config index 3bf406c..e67b96c 100644 --- a/tests/unit/web.config +++ b/tests/unit/web.config @@ -6,7 +6,7 @@ - + diff --git a/tests/web.config b/tests/web.config index 3bf406c..e67b96c 100644 --- a/tests/web.config +++ b/tests/web.config @@ -6,7 +6,7 @@ - +