Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/scaffold-homepage-onboarding.changed.md
Original file line number Diff line number Diff line change
@@ -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)
1 change: 1 addition & 0 deletions changelog.d/wheels-welcome-production-gate.security.md
Original file line number Diff line number Diff line change
@@ -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)
22 changes: 21 additions & 1 deletion cli/lucli/Module.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -6191,7 +6191,27 @@ component extends="modules.BaseModule" {

fileWrite(
targetDir & "/app/views/main/index.cfm",
'<h1>Welcome to ' & appName & '</h1>' & nl & '<p>Your Wheels application is running. Edit this file at app/views/main/index.cfm</p>' & nl
(
'<!---' & nl &
tab & 'Starter home page: replace before production.' & nl &
tab & 'This development/first-run landing page surfaces environment' & nl &
tab & 'details (Wheels version, engine, database, environment) and CLI' & nl &
tab & 'commands. Deploy a real homepage so those are not exposed to' & nl &
tab & 'anonymous visitors.' & nl &
'--->' & nl &
'<cfoutput>' & nl &
'<h1>Welcome to ' & appName & '</h1>' & nl &
'<p>Your <strong>Wheels ##get("version")##</strong> application is running on ##application.wheels.serverName## with ##application.wheels.dataSourceName## (##get("environment")##).</p>' & nl &
nl &
'<h2>Next steps</h2>' & nl &
'<ul>' & nl &
tab & '<li><code>wheels g scaffold Post title content:text</code> &mdash; generate a model, controller, and views</li>' & nl &
tab & '<li><code>wheels migrate latest</code> &mdash; build the database schema</li>' & nl &
tab & '<li><code>wheels test</code> &mdash; run the test suite</li>' & nl &
'</ul>' & nl &
'<p><small>This page lives at <code>app/views/main/index.cfm</code>; routing is in <code>config/routes.cfm</code>.</small></p>' & nl &
'</cfoutput>' & nl
)
);
printCreated(appName & "/app/views/main/index.cfm");

Expand Down
32 changes: 31 additions & 1 deletion cli/tests/specs/e2e/ProjectScaffoldTest.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -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("<cfoutput>");
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() {
Expand Down Expand Up @@ -325,7 +335,27 @@ component extends="testbox.system.BaseSpec" {

fileWrite(
arguments.targetDir & "/app/views/main/index.cfm",
'<h1>Welcome to ' & arguments.appName & '</h1>' & nl & '<p>Your Wheels application is running. Edit this file at app/views/main/index.cfm</p>' & nl
(
'<!---' & nl &
tab & 'Starter home page: replace before production.' & nl &
tab & 'This development/first-run landing page surfaces environment' & nl &
tab & 'details (Wheels version, engine, database, environment) and CLI' & nl &
tab & 'commands. Deploy a real homepage so those are not exposed to' & nl &
tab & 'anonymous visitors.' & nl &
'--->' & nl &
'<cfoutput>' & nl &
'<h1>Welcome to ' & arguments.appName & '</h1>' & nl &
'<p>Your <strong>Wheels ##get("version")##</strong> application is running on ##application.wheels.serverName## with ##application.wheels.dataSourceName## (##get("environment")##).</p>' & nl &
nl &
'<h2>Next steps</h2>' & nl &
'<ul>' & nl &
tab & '<li><code>wheels g scaffold Post title content:text</code> &mdash; generate a model, controller, and views</li>' & nl &
tab & '<li><code>wheels migrate latest</code> &mdash; build the database schema</li>' & nl &
tab & '<li><code>wheels test</code> &mdash; run the test suite</li>' & nl &
'</ul>' & nl &
'<p><small>This page lives at <code>app/views/main/index.cfm</code>; routing is in <code>config/routes.cfm</code>.</small></p>' & nl &
'</cfoutput>' & nl
)
);
}

Expand Down
3 changes: 2 additions & 1 deletion vendor/wheels/Public.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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 "";
}
Expand Down
7 changes: 4 additions & 3 deletions vendor/wheels/tests/specs/dispatch/InvokeMethodSpec.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,8 @@ component extends="wheels.WheelsTest" {
"ai",
"guideImage",
"assets",
"mcp"
"mcp",
"index"
];

for (var handler in gatedHandlers) {
Expand All @@ -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).

});

Expand Down
Loading