diff --git a/changelog.d/scaffold-homepage-onboarding.changed.md b/changelog.d/scaffold-homepage-onboarding.changed.md new file mode 100644 index 0000000000..809dbba29a --- /dev/null +++ b/changelog.d/scaffold-homepage-onboarding.changed.md @@ -0,0 +1 @@ +- `wheels new` generates a richer default home page: a runtime status line (Wheels version, engine, database, environment) plus a Next-steps command guide, replacing the bare two-line placeholder — surfacing the onboarding content from the redesigned framework welcome page where users actually land (#2098) diff --git a/changelog.d/wheels-welcome-production-gate.security.md b/changelog.d/wheels-welcome-production-gate.security.md new file mode 100644 index 0000000000..dba3ce0153 --- /dev/null +++ b/changelog.d/wheels-welcome-production-gate.security.md @@ -0,0 +1 @@ +- The `/wheels` welcome page now defense-in-depth gates itself with `$blockInProduction()` like every other `Public` handler, so it no longer renders outside `development` when `enablePublicComponent` is manually enabled — closing a version/engine/database/environment disclosure gap (reverses the #2233 exception) diff --git a/cli/lucli/Module.cfc b/cli/lucli/Module.cfc index 4667e3f72c..359c329c60 100644 --- a/cli/lucli/Module.cfc +++ b/cli/lucli/Module.cfc @@ -6191,7 +6191,27 @@ component extends="modules.BaseModule" { fileWrite( targetDir & "/app/views/main/index.cfm", - '

Welcome to ' & appName & '

' & nl & '

Your Wheels application is running. Edit this file at app/views/main/index.cfm

' & nl + ( + '' & nl & + '' & nl & + '

Welcome to ' & appName & '

' & nl & + '

Your Wheels ##get("version")## application is running on ##application.wheels.serverName## with ##application.wheels.dataSourceName## (##get("environment")##).

' & nl & + nl & + '

Next steps

' & nl & + '' & nl & + '

This page lives at app/views/main/index.cfm; routing is in config/routes.cfm.

' & nl & + '
' & nl + ) ); printCreated(appName & "/app/views/main/index.cfm"); diff --git a/cli/tests/specs/e2e/ProjectScaffoldTest.cfc b/cli/tests/specs/e2e/ProjectScaffoldTest.cfc index 3e7a54deb8..675b1c1ad6 100644 --- a/cli/tests/specs/e2e/ProjectScaffoldTest.cfc +++ b/cli/tests/specs/e2e/ProjectScaffoldTest.cfc @@ -214,6 +214,16 @@ component extends="testbox.system.BaseSpec" { var content = fileRead(path); expect(content).toInclude("Welcome to testapp"); + // Runtime expressions must survive generation as single-hash + // CFML (## -> # in the fileWrite string), not be evaluated at + // scaffold time. Locks in the escaping shared with Module.cfc. + expect(content).toInclude('##get("version")##'); + expect(content).toInclude('##application.wheels.serverName##'); + expect(content).toInclude(""); + expect(content).toInclude("Next steps"); + expect(content).toInclude("wheels g scaffold"); + expect(content).toInclude("wheels migrate latest"); + expect(content).toInclude("wheels test"); }); it("generates base Controller.cfc in app/controllers/", function() { @@ -325,7 +335,27 @@ component extends="testbox.system.BaseSpec" { fileWrite( arguments.targetDir & "/app/views/main/index.cfm", - '

Welcome to ' & arguments.appName & '

' & nl & '

Your Wheels application is running. Edit this file at app/views/main/index.cfm

' & nl + ( + '' & nl & + '' & nl & + '

Welcome to ' & arguments.appName & '

' & nl & + '

Your Wheels ##get("version")## application is running on ##application.wheels.serverName## with ##application.wheels.dataSourceName## (##get("environment")##).

' & nl & + nl & + '

Next steps

' & nl & + '' & nl & + '

This page lives at app/views/main/index.cfm; routing is in config/routes.cfm.

' & nl & + '
' & nl + ) ); } diff --git a/vendor/wheels/Public.cfc b/vendor/wheels/Public.cfc index c3d45f7146..512d2be508 100644 --- a/vendor/wheels/Public.cfc +++ b/vendor/wheels/Public.cfc @@ -30,7 +30,7 @@ component output="false" displayName="Internal GUI" extends="wheels.Global" { /** * Defense-in-depth: unless the current environment is `development`, * short-circuit the handler with a 404 response before any view is - * included. Called as the first statement of every non-`index` handler in + * included. Called as the first statement of every handler in * this component. */ public void function $blockInProduction() { @@ -356,6 +356,7 @@ component output="false" displayName="Internal GUI" extends="wheels.Global" { This is just a proof of concept */ function index() { + $blockInProduction(); include "/wheels/public/views/congratulations.cfm"; return ""; } diff --git a/vendor/wheels/tests/specs/dispatch/InvokeMethodSpec.cfc b/vendor/wheels/tests/specs/dispatch/InvokeMethodSpec.cfc index 06a7b8e0c9..7f848b0253 100644 --- a/vendor/wheels/tests/specs/dispatch/InvokeMethodSpec.cfc +++ b/vendor/wheels/tests/specs/dispatch/InvokeMethodSpec.cfc @@ -47,9 +47,10 @@ component extends="wheels.WheelsTest" { // (since #2903 the gate is a development-only allowlist), so // the only thing we're testing is "did the receiver survive // the dispatch?" If it didn't, the call throws before the - // include statement runs. (This spec invokes the ungated - // index() handler, so the production-only early-return below - // is belt-and-suspenders.) + // include statement runs. (This spec invokes index(), which now + // calls $blockInProduction() too (a development-only no-op per + // the allowlist), so the production-only early-return below is + // belt-and-suspenders.) if ( StructKeyExists(application, "wheels") && StructKeyExists(application.wheels, "environment") diff --git a/vendor/wheels/tests/specs/security/PublicComponentProductionSpec.cfc b/vendor/wheels/tests/specs/security/PublicComponentProductionSpec.cfc index 8e84c856bc..6c219d9ca4 100644 --- a/vendor/wheels/tests/specs/security/PublicComponentProductionSpec.cfc +++ b/vendor/wheels/tests/specs/security/PublicComponentProductionSpec.cfc @@ -150,7 +150,8 @@ component extends="wheels.WheelsTest" { "ai", "guideImage", "assets", - "mcp" + "mcp", + "index" ]; for (var handler in gatedHandlers) { @@ -170,17 +171,14 @@ component extends="wheels.WheelsTest" { })(handler); } - it("index() is NOT gated (congratulations page stays discoverable)", () => { - // The issue explicitly says leave index() reachable in dev/testing. - // In production enablePublicComponent=false already hides it at - // the dispatch layer, so no per-handler block is needed. - var pattern = "function\s+index\s*\([^)]*\)\s*\{\s*\$blockInProduction\s*\(\s*\)\s*;"; - var matched = REFindNoCase(pattern, source) > 0; - expect(matched).toBeFalse( - "index() should not call $blockInProduction() — it's the congratulations " - & "page and the issue (##2233) explicitly keeps it discoverable." - ); - }); + // index() (the congratulations/welcome page at the /wheels namespace + // root) is now gated like every other handler above. #2233 originally + // left it ungated so the welcome page stayed reachable in dev/testing, + // relying on enablePublicComponent=false to hide /wheels in production. + // Reversed because the redesigned page surfaces version/engine/db/ + // environment (#2098/#2272), the same class of detail the gated handlers + // protect, so it now defense-in-depth gates itself too. Development still + // renders (the gate is an allowlist: only "development" passes). });