Kaynağa Gözat

Implement M4: Views (ViewRenderer + Home.html)

Adds Framework/ViewRenderer.wsc (WscMvc.ViewRenderer), loading separate
.html templates and substituting {{Key}} placeholders with HTML-encoded
values from a Scripting.Dictionary. Wires /hello through the new pipeline
(HomeController -> Application -> ViewRenderer -> Views/Home.html) while
preserving the exact pre-M4 response body, so the M1 acceptance test
doubles as a live regression check for the render pipeline.

Views/ is unreachable over HTTP by the same physical-separation guarantee
already used for Framework/ and Controllers/ (sibling of public/, never
inside it) - verified directly, not assumed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
master
Daniel Covington 2 hafta önce
ebeveyn
işleme
d6288e3d62
14 değiştirilmiş dosya ile 412 ekleme ve 27 silme
  1. +9
    -3
      Controllers/HomeController.wsc
  2. +29
    -8
      Framework/Application.wsc
  3. +126
    -0
      Framework/ViewRenderer.wsc
  4. +3
    -3
      IMPLEMENTATION_PLAN.md
  5. +1
    -0
      Views/Home.html
  6. +15
    -2
      docs/ARCHITECTURE.md
  7. +10
    -0
      docs/DECISIONS.md
  8. +65
    -0
      docs/TEST-RESULTS.md
  9. +5
    -4
      public/Default.asp
  10. +1
    -1
      test-app/Controllers/SelfTestController.wsc
  11. +5
    -4
      test-app/public/Default.asp
  12. +141
    -2
      tests/Test-Components.vbs
  13. +1
    -0
      tools/Register-Components.ps1
  14. +1
    -0
      tools/Unregister-Components.ps1

+ 9
- 3
Controllers/HomeController.wsc Dosyayı Görüntüle

@@ -10,7 +10,8 @@

<public>
<method name="Hello">
<parameter name="body"/>
<parameter name="viewName"/>
<parameter name="message"/>
</method>
</public>

@@ -18,8 +19,13 @@
<![CDATA[
Option Explicit

Sub Hello(body)
body = "Hello from WSC-MVC!"
' Returns scalar view-data only (viewName + message), never rendered HTML -
' Application.wsc owns building the Dictionary and calling ViewRenderer, so
' this controller stays primitive-only like the rest of the request-data
' boundary (see docs/ARCHITECTURE.md).
Sub Hello(viewName, message)
viewName = "Home"
message = "Hello from WSC-MVC!"
End Sub
]]>
</script>


+ 29
- 8
Framework/Application.wsc Dosyayı Görüntüle

@@ -12,6 +12,7 @@
<method name="Run">
<parameter name="ctx"/>
<parameter name="applicationName"/>
<parameter name="viewsDir"/>
<parameter name="statusLine"/>
<parameter name="contentType"/>
<parameter name="body"/>
@@ -27,7 +28,7 @@ Option Explicit
' (COM/method failure -> 500) outcomes, and one place logs them. ctx is our
' own WscMvc.RequestContext object (not an ASP intrinsic), carrying only
' primitive request data. No ASP intrinsics are referenced here.
Sub Run(ctx, applicationName, statusLine, contentType, body, allowHeader)
Sub Run(ctx, applicationName, viewsDir, statusLine, contentType, body, allowHeader)
Dim router, handlerKey

statusLine = ""
@@ -69,7 +70,7 @@ Sub Run(ctx, applicationName, statusLine, contentType, body, allowHeader)

Select Case handlerKey
Case "Home.Hello"
RunHomeHello ctx, statusLine, contentType, body, allowHeader
RunHomeHello ctx, viewsDir, statusLine, contentType, body, allowHeader
Case "SelfTest.RunSelfTest"
RunSelfTest ctx, statusLine, contentType, body, allowHeader
Case Else
@@ -79,8 +80,8 @@ Sub Run(ctx, applicationName, statusLine, contentType, body, allowHeader)
LogOutcome ctx, statusLine
End Sub

Sub RunHomeHello(ctx, statusLine, contentType, body, allowHeader)
Dim ctrl, helloBody
Sub RunHomeHello(ctx, viewsDir, statusLine, contentType, body, allowHeader)
Dim ctrl, viewName, message, data, renderer, renderedBody

Set ctrl = Nothing
On Error Resume Next
@@ -93,9 +94,9 @@ Sub RunHomeHello(ctx, statusLine, contentType, body, allowHeader)
End If
On Error Goto 0

helloBody = ""
viewName = "" : message = ""
On Error Resume Next
ctrl.Hello helloBody
ctrl.Hello viewName, message
If Err.Number <> 0 Then
Err.Clear
On Error Goto 0
@@ -104,12 +105,32 @@ Sub RunHomeHello(ctx, statusLine, contentType, body, allowHeader)
Exit Sub
End If
On Error Goto 0
Set ctrl = Nothing

Set data = Nothing
Set renderer = Nothing
On Error Resume Next
Set data = CreateObject("Scripting.Dictionary")
data.Add "Message", message
Set renderer = CreateObject("WscMvc.ViewRenderer")
renderedBody = ""
renderer.Render viewsDir, viewName, data, renderedBody
If Err.Number <> 0 Then
Err.Clear
On Error Goto 0
Set data = Nothing
Set renderer = Nothing
InternalServerError statusLine, contentType, body, allowHeader
Exit Sub
End If
On Error Goto 0
Set data = Nothing
Set renderer = Nothing

statusLine = "200 OK"
contentType = "text/html; charset=utf-8"
body = helloBody
body = renderedBody
allowHeader = ""
Set ctrl = Nothing
End Sub

Sub RunSelfTest(ctx, statusLine, contentType, body, allowHeader)


+ 126
- 0
Framework/ViewRenderer.wsc Dosyayı Görüntüle

@@ -0,0 +1,126 @@
<?xml version="1.0"?>
<?component error="true" debug="false"?>
<component>
<registration
description="WscMvc ViewRenderer"
progid="WscMvc.ViewRenderer"
version="1.00"
classid="{4948DF84-5DC6-448A-9F1B-EB596C28842B}">
</registration>

<public>
<method name="Render">
<parameter name="viewsDir"/>
<parameter name="viewName"/>
<parameter name="data"/>
<parameter name="output"/>
</method>
</public>

<script language="VBScript">
<![CDATA[
Option Explicit

' Loads a separate .html template and substitutes {{Key}} placeholders with
' HTML-encoded values from a Scripting.Dictionary. Never references ASP
' intrinsics - viewsDir is a plain path string resolved by the caller (same
' pattern as RequestContext's logDir). Only text-context HTML encoding is
' implemented (SPEC SS8); attribute/URL/JS-context encoding is out of scope
' until a view actually needs it. Template content is never executed as
' VBScript or evaluated as an expression - substitution is a single literal
' scan-and-replace pass.
Sub Render(viewsDir, viewName, data, output)
Dim fso, templatePath, template, stream

output = ""

If Not IsValidViewName(viewName) Then
Err.Raise vbObjectError + 1, "WscMvc.ViewRenderer", "Invalid view name"
End If

Set fso = CreateObject("Scripting.FileSystemObject")
templatePath = viewsDir & "\" & viewName & ".html"

If Not fso.FileExists(templatePath) Then
Set fso = Nothing
Err.Raise vbObjectError + 2, "WscMvc.ViewRenderer", "View template not found"
End If

Set stream = fso.OpenTextFile(templatePath, 1) ' 1 = ForReading
template = stream.ReadAll
stream.Close
Set stream = Nothing
Set fso = Nothing

output = ExpandTemplate(template, data)
End Sub

Function IsValidViewName(viewName)
Dim i, c
IsValidViewName = False
If IsNull(viewName) Or IsEmpty(viewName) Then Exit Function
If Len(viewName) = 0 Then Exit Function
For i = 1 To Len(viewName)
c = Mid(viewName, i, 1)
If Not ((c >= "A" And c <= "Z") Or (c >= "a" And c <= "z") Or _
(c >= "0" And c <= "9") Or c = "_" Or c = "-") Then
Exit Function
End If
Next
IsValidViewName = True
End Function

' Single left-to-right scan for {{Key}} tokens. Unterminated "{{" and a key
' with no matching dictionary entry are both treated as template-authoring
' errors (Err.Raise) rather than emitted as literal text, so rendered output
' is always fully resolved - never partially-substituted markup.
Function ExpandTemplate(template, data)
Dim result, pos, openPos, closePos, key, hasKey

result = ""
pos = 1

Do
openPos = InStr(pos, template, "{{")
If openPos = 0 Then
result = result & Mid(template, pos)
Exit Do
End If

result = result & Mid(template, pos, openPos - pos)

closePos = InStr(openPos + 2, template, "}}")
If closePos = 0 Then
Err.Raise vbObjectError + 3, "WscMvc.ViewRenderer", "Unterminated view placeholder"
End If

key = Trim(Mid(template, openPos + 2, closePos - openPos - 2))

hasKey = False
If Not data Is Nothing Then
If data.Exists(key) Then hasKey = True
End If
If Not hasKey Then
Err.Raise vbObjectError + 4, "WscMvc.ViewRenderer", "Unresolved view placeholder"
End If

result = result & HtmlEncode(CStr(data(key)))
pos = closePos + 2
Loop

ExpandTemplate = result
End Function

Function HtmlEncode(s)
Dim r
r = s
r = Replace(r, "&", "&amp;")
r = Replace(r, "<", "&lt;")
r = Replace(r, ">", "&gt;")
r = Replace(r, """", "&quot;")
r = Replace(r, "'", "&#39;")
HtmlEncode = r
End Function
]]>
</script>
</component>

+ 3
- 3
IMPLEMENTATION_PLAN.md Dosyayı Görüntüle

@@ -32,9 +32,9 @@ Gate: repeated/concurrent requests and failure cases behave predictably. **MET**
Gate: route matrix and malformed URL tests pass. **MET** — see `docs/TEST-RESULTS.md`. WSH covers full Router/Application routing contract; IIS HTTP covers GET/POST-routed behavior and malformed route handling. PUT/DELETE can be intercepted by IIS before Classic ASP on the tested host, so those are not treated as framework HTTP-route assertions.

## M4 — Views
- [ ] Safe separate template loading, text-context encoding, default-deny for private templates.
- [ ] Template missing/escaping tests; add layout after renderer works.
Gate: output is deterministic and untrusted text does not become HTML.
- [x] Safe separate template loading, text-context encoding, default-deny for private templates.
- [x] Template missing/escaping tests; add layout after renderer works.
Gate: output is deterministic and untrusted text does not become HTML. **MET** for the vertical slice shipped — see `docs/TEST-RESULTS.md`. `Views/Layout.html` (a layout convention) is not yet added since there is still only one view; add it when a second view needs shared chrome, not speculatively.

## M5 — Data
- [ ] ADODB contract and provider-specific integration test database.


+ 1
- 0
Views/Home.html Dosyayı Görüntüle

@@ -0,0 +1 @@
{{Message}}

+ 15
- 2
docs/ARCHITECTURE.md Dosyayı Görüntüle

@@ -12,7 +12,9 @@ GET / or GET /hello
-> CreateObject("WscMvc.Router")
-> Router.Match(ctx, "production", ..., handlerKey) [Framework/Router.wsc]
-> CreateObject("WscMvc.HomeController")
-> HomeController.Hello(body) [Controllers/HomeController.wsc]
-> HomeController.Hello(viewName, message) [Controllers/HomeController.wsc]
-> CreateObject("WscMvc.ViewRenderer")
-> ViewRenderer.Render(viewsDir, viewName, data, body) [Framework/ViewRenderer.wsc]
-> LogOutcome ctx, statusLine (best-effort append to logs/app.log)
-> Default.asp sets Response.Status/ContentType, optional Allow header, writes body
```
@@ -60,6 +62,7 @@ curl http://100.127.62.31:8091/self-test
| `Framework/RequestContext.wsc` | `WscMvc.RequestContext` | `{1C36FA55-34DF-4974-94B9-D657389362B2}` |
| `Framework/Router.wsc` | `WscMvc.Router` | `{C92F9338-B478-4EAD-B865-892FFB1E1C51}` |
| `Framework/Application.wsc` | `WscMvc.Application` | `{851C7763-1638-42FE-A166-BF3DD3A96A88}` |
| `Framework/ViewRenderer.wsc` | `WscMvc.ViewRenderer` | `{4948DF84-5DC6-448A-9F1B-EB596C28842B}` |
| `Controllers/HomeController.wsc` | `WscMvc.HomeController` | `{87488446-60BE-4068-8368-0B709BB68F3F}` |
| `test-app/Controllers/SelfTestController.wsc` | `WscMvc.SelfTestController` | `{D2634944-4646-4C55-956E-4C05E7E10904}` |

@@ -85,6 +88,16 @@ Both are 64-bit, no managed code, anonymous auth identity `IUSR`. Each site's `p
- Each site's `logs/` has an explicit, scoped `icacls ... /grant "IIS_IUSRS:(OI)(CI)M"` (now automated by `tools/Setup-Site.ps1`, previously a manual one-off command) so classic ASP (impersonating `IUSR` for anonymous requests) can write `app.log`. No other project directory grants `IUSR`/`IIS_IUSRS` write access — verified with a recursive `icacls /T` scan, see `docs/DECISIONS.md`.
- Default document is `Default.asp` on both sites; each site's one route is served via a rewrite rule, not the default document.

## Views (M4)

`Framework/ViewRenderer.wsc` (`WscMvc.ViewRenderer`) loads `Views/<ViewName>.html` and substitutes `{{Key}}` placeholders from a `Scripting.Dictionary` built by `Application.wsc`'s handler subs (e.g. `RunHomeHello`) from a controller's plain scalar output — controllers (`HomeController.Hello(viewName, message)`) never build the Dictionary or touch rendering themselves, keeping them primitive-only like the rest of the request-data boundary. `viewsDir` is resolved by each `Default.asp` via `Server.MapPath("../Views")` (same parent-relative pattern as `logDir`) and threaded through `Application.Run`'s new `viewsDir` parameter — `ViewRenderer.wsc` itself never references ASP intrinsics.

`Views/` is a sibling of `public/`, exactly like `Framework/`/`Controllers/`, so it is unreachable over HTTP by the same physical-separation guarantee (verified: `GET /Views/Home.html` → `404`) — no additional deny rule was needed.

Substitution is a single deterministic left-to-right scan; template content is never executed as VBScript or evaluated as an expression (SPEC §8). Placeholder values are HTML-encoded (`&`, `<`, `>`, `"`, `'`) for text context only — **attribute/URL/JS-context encoding is not implemented**; this is a deliberate scope limit (SPEC §8 requires "separate, explicit handling" per context, and no current view needs anything but text context), not an oversight. An unterminated `{{`, a placeholder with no matching dictionary key, an invalid view name, or a missing template file are all safe errors (`Err.Raise` with a generic, path-free message) that flow into `Application.wsc`'s existing central 500 handling, which already discards `Err.Description` entirely.

`Views/Home.html` is exactly `{{Message}}`; `HomeController` supplies `message = "Hello from WSC-MVC!"`, a string with no HTML-special characters, so `/hello`'s response body is byte-identical to the pre-M4 (M1-era) contract — the rendering pipeline was proven end-to-end without changing observable behavior. `Views/Layout.html` and a layout convention are deferred until a second view actually needs shared chrome (IMPLEMENTATION_PLAN M4 explicitly sequences "add layout after renderer works").

## Deferred to later milestones (do not implement early)

Per SPEC §3 non-goals and IMPLEMENTATION_PLAN M4+: HTML views/templates, ADODB, auth. A more robust logging mechanism (if complete coverage under concurrent load is ever required) is deferred to M6 — see the concurrency finding in `docs/DECISIONS.md`.
Per SPEC §3 non-goals and IMPLEMENTATION_PLAN M5+: ADODB, auth. A more robust logging mechanism (if complete coverage under concurrent load is ever required) is deferred to M6 — see the concurrency finding in `docs/DECISIONS.md`. A layout convention (`Views/Layout.html`) and non-text-context encoding are deferred until a concrete view needs them — see the Views section above.

+ 10
- 0
docs/DECISIONS.md Dosyayı Görüntüle

@@ -117,3 +117,13 @@ Route path validation is intentionally conservative for M3. ASP/IIS has already
The response contract gained an `allowHeader` out parameter from `Application.Run`. `Default.asp` is still the only layer that touches ASP `Response`; it emits the `Allow` header only when the framework returns one. This keeps routing/framework code ASP-intrinsic-free while still allowing correct HTTP behavior for app-routed `405 Method Not Allowed` responses.

IIS/Classic ASP on the local Windows 11 test host intercepted `PUT`/`DELETE` requests before they reached the application, returning IIS's own `Allow: GET, HEAD, OPTIONS, TRACE`. Therefore HTTP integration tests assert framework 405 behavior using `POST /hello`, which Classic ASP does deliver to the app and which returns the framework's `Allow: GET`. The full Router/Application unsupported-method contract, including `DELETE /self-test -> Allow: GET, POST`, is covered by WSH component tests and the self-test controller's direct `Router.Match` checks. This matches the M3 scope: support GET and POST initially; distinguish unsupported methods where the request reaches the framework.

## M4 — Views: ViewRenderer, controller/render split (2026-09-19)

Added `Framework/ViewRenderer.wsc` (`WscMvc.ViewRenderer`) and `Views/Home.html`. Design choice: controllers (`HomeController.Hello`) stay primitive-only (return `viewName`, `message` as plain strings) and `Application.wsc`'s handler subs own building the `Scripting.Dictionary` and calling `ViewRenderer.Render` — matches the existing pattern of keeping business-logic components decoupled from any object more complex than what they strictly need to hand back, and avoids deciding a controller-to-Dictionary contract before a second controller actually needs one. `viewsDir` is threaded through as a new `Application.Run` parameter (not through `RequestContext`, which would have cascaded into every `ctx.Initialize` call site across both `Default.asp` files, `SelfTestController.wsc` (5 call sites), and the WSH tests, for a value that is pure Framework wiring rather than genuine per-request data) — smallest-footprint choice given `Application.Run`'s signature is already established as changeable pre-release (see the M2/M3 CLSID-stability note above).

`/hello`'s existing exact-body contract (`"Hello from WSC-MVC!"`, fixed since M1/SPEC §4) was deliberately preserved rather than changed to prove the render pipeline: `Views/Home.html` is exactly `{{Message}}`, and the message string has no HTML-special characters, so HTML-encoding is a no-op and the response is byte-identical pre- and post-M4 — verified directly (`Content-Length: 19` unchanged) rather than assumed. This let the M1 acceptance test double as a live M4 regression check instead of requiring a new observable behavior to prove the slice works.

No experimental surprises this milestone (unlike M0-M3, which each surfaced a real WSC/VBScript defect) — returning a newly-created object through a `Sub`'s `ByRef` out-parameter (`ctrl.Hello viewName, message`, both scalars in this case, but the same mechanism was also exercised for `Set data = CreateObject(...)` inside `RunHomeHello`) behaved exactly as ordinary VBScript `ByRef` semantics predict, and `Err.Raise` inside a WSC method, caught by the caller's existing `On Error Resume Next` + `Err.Number` pattern, worked identically to the naturally-thrown errors (`CreateObject` failures) already relied on elsewhere. Confirmed via the WSH suite's dedicated `ViewRenderer` contract tests (encoding, missing template, unresolved placeholder, invalid view name) before trusting it in `Application.wsc`.

Confirmed `Views/` is unreachable over HTTP purely from being a sibling of `public/` (same guarantee already used for `Framework/`/`Controllers/`) — no new `web.config` rule was needed: `GET /Views/Home.html` → `404` on both sites.

+ 65
- 0
docs/TEST-RESULTS.md Dosyayı Görüntüle

@@ -354,3 +354,68 @@ Result: **PASS** — `ok=true`, checks included `request_context_contract`, `cor
`tests/run-self-test.sh` local note: first failed under local bash because the Windows checkout had CRLF line endings in the `.sh` file (`set: pipefail\r: invalid option name`). Added `.gitattributes` (`*.sh text eol=lf`) and normalized the script. After that, local WSL/bash still could not reach Windows IIS on `localhost:8091`/gateway IP from this environment (`curl: (7) Failed to connect`), so the bash wrapper is **NOT RUN/PASS locally**. The same HTTP JSON contract was verified with PowerShell above; the bash wrapper remains intended for a CLI that can reach the test-app URL, as in earlier VM/tailnet evidence.

M3 gate status: **PASS** for framework and IIS GET/POST behavior. PUT/DELETE HTTP probes are not used as app-route assertions on this host because IIS intercepts them before Classic ASP; WSH/self-test Router checks cover the unsupported-method framework contract directly.

## M4 — Views: separate templates, text-context encoding, default-deny (2026-09-19)

Host under test: `DESKTOP-80D128R`, Microsoft Windows 11 Pro 10.0.26200, 64-bit — same local IIS sites as M3 (`WscMvc` on `:8090`, `WscMvcTests` on `:8091`), already running from this checkout.

Added `Framework/ViewRenderer.wsc` (`WscMvc.ViewRenderer`, `{4948DF84-5DC6-448A-9F1B-EB596C28842B}`) and `Views/Home.html` (`{{Message}}`). `Controllers/HomeController.wsc`'s `Hello` now returns scalar view data (`viewName`, `message`) instead of a finished body string; `Framework/Application.wsc`'s `Run` gained a `viewsDir` parameter and `RunHomeHello` now builds a `Scripting.Dictionary` and calls `ViewRenderer.Render`. Both `Default.asp` files resolve `viewsDir = Server.MapPath("../Views")`, the same parent-relative pattern already used for `logDir`.

Test-first check before implementation:

```
cscript //nologo tests\Test-Components.vbs
```

Result before M4 implementation: **FAIL**, as expected for the new contract:

```
FAIL: Application.Run raised error on /hello - Wrong number of arguments or invalid property assignment
FAIL: could not create WscMvc.ViewRenderer - ActiveX component can't create object
RESULT: FAILURE
```

Registration after implementation:

```
powershell -ExecutionPolicy Bypass -File tools\Register-Components.ps1
```

Result: **PASS** — registered `RequestContext`, `Router`, new `ViewRenderer`, `Application`, `HomeController`, `SelfTestController`.

WSH component contract:

```
cscript //nologo tests\Test-Components.vbs
```

Result: **PASS** (full output, ViewRenderer-specific lines):

```
PASS: Application.Run(/hello) body [unchanged: "Hello from WSC-MVC!" — regression proof the render pipeline preserves M1's exact-body contract]
PASS: ViewRenderer HTML-encodes placeholder value
PASS: ViewRenderer passes through template with no placeholders (data=Nothing)
PASS: ViewRenderer.Render on missing template raises a path-free error
PASS: ViewRenderer.Render raises on unresolved placeholder
PASS: ViewRenderer.Render rejects an invalid view name
RESULT: ALL PASS
```

HTTP integration:

```
powershell -ExecutionPolicy Bypass -File tests\Test-Http.ps1 -BaseUrl http://localhost:8090 -TestBaseUrl http://localhost:8091
```

Result: **PASS**, all 14 existing checks including `GET /hello body` (unchanged) and `GET /Framework/Application.wsc unreachable`.

Direct verification that `Views/` is unreachable over HTTP (physical separation, same guarantee as `Framework/`/`Controllers/` — `Views/` is a sibling of `public/`, never inside it):

```
curl -i http://localhost:8090/hello -> 200, "Hello from WSC-MVC!" (Content-Length: 19, unchanged from pre-M4)
curl -o /dev/null -w "%{http_code}" http://localhost:8090/Views/Home.html -> 404
```

`tests/run-self-test.sh http://localhost:8091`: **NOT RUN locally** — this host's local Python is a Microsoft Store execution-alias stub, so the script's `json.tool` pretty-print step cannot run. The same JSON contract was already verified directly (`curl http://localhost:8091/self-test` → `{"ok":true,...}`, all 5 checks passing) and via `Test-Http.ps1` above; this is a convenience-script limitation on this specific host, not a framework regression — matches the pre-existing NOT RUN note for this same script under M3.

M4 gate status: **PASS** for the vertical slice implemented (safe separate template loading from a physically non-served directory; deterministic `{{Key}}` substitution; default HTML-text-context encoding verified against a real escaping case; missing-template and unresolved-placeholder failures verified path-free and safe; `/hello`'s existing exact-body contract verified unchanged end-to-end through IIS). `Views/Layout.html` and a layout convention are deferred (IMPLEMENTATION_PLAN M4 sequences "add layout after renderer works" as a second step) — not yet needed since there is still only one view. Attribute/URL/JS-context encoding remains explicitly out of scope until a view needs it (documented on `ViewRenderer.wsc` and in `docs/ARCHITECTURE.md`).

+ 5
- 4
public/Default.asp Dosyayı Görüntüle

@@ -1,15 +1,16 @@
<%@ Language="VBScript" %>
<% Option Explicit %>
<%
Dim route, httpMethod, logDir, ctx, app, applicationName, statusLine, contentType, body, allowHeader
Dim route, httpMethod, logDir, viewsDir, ctx, app, applicationName, statusLine, contentType, body, allowHeader

route = Request.QueryString("route")
httpMethod = Request.ServerVariables("REQUEST_METHOD")
applicationName = "production"
' logs/ deliberately lives outside the served "public" webroot (IIS site
' physical path = public/), so a parent-relative MapPath is required here -
' logs/ and Views/ deliberately live outside the served "public" webroot (IIS
' site physical path = public/), so parent-relative MapPath is required here -
' requires enableParentPaths=true for this site. See docs/DECISIONS.md.
logDir = Server.MapPath("../logs")
viewsDir = Server.MapPath("../Views")

Set ctx = Nothing
On Error Resume Next
@@ -57,7 +58,7 @@ body = ""
allowHeader = ""

On Error Resume Next
app.Run ctx, applicationName, statusLine, contentType, body, allowHeader
app.Run ctx, applicationName, viewsDir, statusLine, contentType, body, allowHeader
If Err.Number <> 0 Then
Err.Clear
On Error Goto 0


+ 1
- 1
test-app/Controllers/SelfTestController.wsc Dosyayı Görüntüle

@@ -87,7 +87,7 @@ Sub RunSelfTest(logDir, body)
Set ctxUnknown = CreateObject("WscMvc.RequestContext")
ctxUnknown.Initialize "/hello", "GET", logDir
unkStatus = "" : unkType = "" : unkBody = "" : unkAllow = ""
app.Run ctxUnknown, "tests", unkStatus, unkType, unkBody, unkAllow
app.Run ctxUnknown, "tests", "", unkStatus, unkType, unkBody, unkAllow
If Err.Number <> 0 Then
unkDetail = Err.Description
Err.Clear


+ 5
- 4
test-app/public/Default.asp Dosyayı Görüntüle

@@ -5,15 +5,16 @@
' set in the shared framework. Production's bootstrap selects "production".
' Keeping this value inside each app prevents direct Default.asp?route=...
' requests from crossing the application boundary.
Dim route, httpMethod, logDir, ctx, app, applicationName, statusLine, contentType, body, allowHeader
Dim route, httpMethod, logDir, viewsDir, ctx, app, applicationName, statusLine, contentType, body, allowHeader

route = Request.QueryString("route")
httpMethod = Request.ServerVariables("REQUEST_METHOD")
applicationName = "tests"
' logs/ deliberately lives outside the served "public" webroot (IIS site
' physical path = public/), so a parent-relative MapPath is required here -
' logs/ and Views/ deliberately live outside the served "public" webroot (IIS
' site physical path = public/), so parent-relative MapPath is required here -
' requires enableParentPaths=true for this site. See docs/DECISIONS.md.
logDir = Server.MapPath("../logs")
viewsDir = Server.MapPath("../Views")

Set ctx = Nothing
On Error Resume Next
@@ -61,7 +62,7 @@ body = ""
allowHeader = ""

On Error Resume Next
app.Run ctx, applicationName, statusLine, contentType, body, allowHeader
app.Run ctx, applicationName, viewsDir, statusLine, contentType, body, allowHeader
If Err.Number <> 0 Then
Err.Clear
On Error Goto 0


+ 141
- 2
tests/Test-Components.vbs Dosyayı Görüntüle

@@ -1,6 +1,6 @@
Option Explicit

Dim fso, logDir, pass
Dim fso, logDir, viewsDir, testViewsDir, pass
pass = True

Set fso = CreateObject("Scripting.FileSystemObject")
@@ -9,6 +9,17 @@ If fso.FolderExists(logDir) Then
fso.DeleteFolder logDir, True
End If

' Real Views/ used for the /hello integration path (proves the end-to-end
' rendering pipeline, not just the ViewRenderer component in isolation).
viewsDir = fso.GetParentFolderName(WScript.ScriptFullName) & "\..\Views"

' Disposable fixture directory for direct ViewRenderer contract tests below.
testViewsDir = fso.GetParentFolderName(WScript.ScriptFullName) & "\test-views"
If fso.FolderExists(testViewsDir) Then
fso.DeleteFolder testViewsDir, True
End If
fso.CreateFolder testViewsDir

Function NewContext(path, httpMethod)
Dim ctx
Set ctx = CreateObject("WscMvc.RequestContext")
@@ -20,10 +31,18 @@ Sub RunApplication(app, path, httpMethod, applicationName, statusLine, contentTy
Dim ctx
Set ctx = NewContext(path, httpMethod)
statusLine = "" : contentType = "" : body = "" : allowHeader = ""
app.Run ctx, applicationName, statusLine, contentType, body, allowHeader
app.Run ctx, applicationName, viewsDir, statusLine, contentType, body, allowHeader
Set ctx = Nothing
End Sub

Sub WriteFixture(fileName, content)
Dim stream
Set stream = fso.CreateTextFile(testViewsDir & "\" & fileName, True)
stream.Write content
stream.Close
Set stream = Nothing
End Sub

Sub CheckEqual(actual, expected, label)
If actual = expected Then
WScript.Echo "PASS: " & label
@@ -177,6 +196,123 @@ If pass Then
CheckEqual allowHeader, "", "Application.Run(malformed path) Allow header"
End If

' --- ViewRenderer contract: substitution + HTML-encoding ---
Dim renderer
On Error Resume Next
Set renderer = CreateObject("WscMvc.ViewRenderer")
If Err.Number <> 0 Then
WScript.Echo "FAIL: could not create WscMvc.ViewRenderer - " & Err.Description
pass = False
Err.Clear
End If
On Error Goto 0

If pass Then
WriteFixture "Sample.html", "<p>{{Name}}</p>"
Dim dataSample, outSample
Set dataSample = CreateObject("Scripting.Dictionary")
dataSample.Add "Name", "<script>alert(1)</script>"
outSample = ""
On Error Resume Next
renderer.Render testViewsDir, "Sample", dataSample, outSample
If Err.Number <> 0 Then
WScript.Echo "FAIL: ViewRenderer.Render raised error on Sample - " & Err.Description
pass = False
Err.Clear
End If
On Error Goto 0
If pass Then
CheckEqual outSample, "<p>&lt;script&gt;alert(1)&lt;/script&gt;</p>", "ViewRenderer HTML-encodes placeholder value"
End If
Set dataSample = Nothing
End If

' --- ViewRenderer contract: template with no placeholders passes through unchanged, data = Nothing ---
If pass Then
WriteFixture "Plain.html", "Plain text, no placeholders."
Dim outPlain
outPlain = ""
On Error Resume Next
renderer.Render testViewsDir, "Plain", Nothing, outPlain
If Err.Number <> 0 Then
WScript.Echo "FAIL: ViewRenderer.Render raised error on Plain (data=Nothing) - " & Err.Description
pass = False
Err.Clear
End If
On Error Goto 0
If pass Then
CheckEqual outPlain, "Plain text, no placeholders.", "ViewRenderer passes through template with no placeholders (data=Nothing)"
End If
End If

' --- ViewRenderer contract: missing template file is a safe, path-free error ---
If pass Then
Dim outMissing, missingOk, missingDetail
missingOk = False
outMissing = ""
On Error Resume Next
renderer.Render testViewsDir, "DoesNotExist", Nothing, outMissing
If Err.Number <> 0 Then
missingDetail = Err.Description
If InStr(1, missingDetail, testViewsDir, 1) = 0 Then
missingOk = True
End If
Err.Clear
End If
On Error Goto 0
If missingOk Then
WScript.Echo "PASS: ViewRenderer.Render on missing template raises a path-free error"
Else
WScript.Echo "FAIL: ViewRenderer.Render on missing template -> Err.Number after call, detail=[" & missingDetail & "]"
pass = False
End If
End If

' --- ViewRenderer contract: unresolved placeholder (no matching dictionary key) is a safe error ---
If pass Then
WriteFixture "Missing.html", "{{Unknown}}"
Dim dataEmpty, outMissingKey, unresolvedOk
Set dataEmpty = CreateObject("Scripting.Dictionary")
unresolvedOk = False
outMissingKey = ""
On Error Resume Next
renderer.Render testViewsDir, "Missing", dataEmpty, outMissingKey
If Err.Number <> 0 Then
unresolvedOk = True
Err.Clear
End If
On Error Goto 0
If unresolvedOk Then
WScript.Echo "PASS: ViewRenderer.Render raises on unresolved placeholder"
Else
WScript.Echo "FAIL: ViewRenderer.Render did not raise on unresolved placeholder -> got [" & outMissingKey & "]"
pass = False
End If
Set dataEmpty = Nothing
End If

' --- ViewRenderer contract: invalid view name (path traversal attempt) is rejected ---
If pass Then
Dim outTraversal, traversalOk
traversalOk = False
outTraversal = ""
On Error Resume Next
renderer.Render testViewsDir, "../secret", Nothing, outTraversal
If Err.Number <> 0 Then
traversalOk = True
Err.Clear
End If
On Error Goto 0
If traversalOk Then
WScript.Echo "PASS: ViewRenderer.Render rejects an invalid view name"
Else
WScript.Echo "FAIL: ViewRenderer.Render did not reject invalid view name -> got [" & outTraversal & "]"
pass = False
End If
End If

Set renderer = Nothing

' --- Logging: best-effort log file was written with both outcomes ---
If pass Then
Dim logPath, logContent
@@ -205,6 +341,9 @@ Set ctx2 = Nothing
If fso.FolderExists(logDir) Then
fso.DeleteFolder logDir, True
End If
If fso.FolderExists(testViewsDir) Then
fso.DeleteFolder testViewsDir, True
End If
Set fso = Nothing

If pass Then


+ 1
- 0
tools/Register-Components.ps1 Dosyayı Görüntüle

@@ -14,6 +14,7 @@ $components = @(
# docs/ARCHITECTURE.md.
(Join-Path $ProjectRoot 'Framework\RequestContext.wsc'),
(Join-Path $ProjectRoot 'Framework\Router.wsc'),
(Join-Path $ProjectRoot 'Framework\ViewRenderer.wsc'),
(Join-Path $ProjectRoot 'Framework\Application.wsc'),
(Join-Path $ProjectRoot 'Controllers\HomeController.wsc'),
(Join-Path $ProjectRoot 'test-app\Controllers\SelfTestController.wsc')


+ 1
- 0
tools/Unregister-Components.ps1 Dosyayı Görüntüle

@@ -18,6 +18,7 @@ $components = @(
@{ Path = (Join-Path $ProjectRoot 'test-app\Controllers\SelfTestController.wsc'); ProgId = 'WscMvc.SelfTestController'; ClassId = '{D2634944-4646-4C55-956E-4C05E7E10904}' },
@{ Path = (Join-Path $ProjectRoot 'Controllers\HomeController.wsc'); ProgId = 'WscMvc.HomeController'; ClassId = '{87488446-60BE-4068-8368-0B709BB68F3F}' },
@{ Path = (Join-Path $ProjectRoot 'Framework\Application.wsc'); ProgId = 'WscMvc.Application'; ClassId = '{851C7763-1638-42FE-A166-BF3DD3A96A88}' },
@{ Path = (Join-Path $ProjectRoot 'Framework\ViewRenderer.wsc'); ProgId = 'WscMvc.ViewRenderer'; ClassId = '{4948DF84-5DC6-448A-9F1B-EB596C28842B}' },
@{ Path = (Join-Path $ProjectRoot 'Framework\Router.wsc'); ProgId = 'WscMvc.Router'; ClassId = '{C92F9338-B478-4EAD-B865-892FFB1E1C51}' },
@{ Path = (Join-Path $ProjectRoot 'Framework\RequestContext.wsc'); ProgId = 'WscMvc.RequestContext'; ClassId = '{1C36FA55-34DF-4974-94B9-D657389362B2}' }
)


Yükleniyor…
İptal
Kaydet

Powered by TurnKey Linux.