From 42089da71b35001032ff4ee603a34387e8822025 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 23:19:53 +0000 Subject: [PATCH 1/3] fix(mapper): reject redundant namespace prefix in to= and controller= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Inside `.namespace("foo")` (or equivalent `.scope()` / `.package()`), writing `to="foo/dashboard##index"` instead of `to="dashboard##index"` silently produced a `foo.foo/dashboard` controller path that downstream got flattened to a `Foodashboard`-style class lookup with an opaque `Wheels.ViewNotFound` error — leaving users to chase the symptom rather than the route definition. `$match()` now detects when the parsed controller starts with the scope's package converted to slash form and throws `Wheels.MapperArgumentInvalid` at registration time. The error names the namespace and the offending value and points at the correct shorter form. Fixes #2791 Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> --- CHANGELOG.md | 1 + vendor/wheels/mapper/matching.cfc | 27 ++++++++ .../tests/specs/mapper/MatchingSpec.cfc | 67 +++++++++++++++++++ 3 files changed, 95 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc365070e2..a2f6d3e1e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ All historical references to "CFWheels" in this changelog have been preserved fo ### Fixed +- Routes registered inside `.namespace("foo")` (or equivalent `.scope()` / `.package()`) with a redundant namespace prefix in the controller path — e.g. `to="foo/dashboard##index"` instead of `to="dashboard##index"` — previously silently produced a `foo.foo/dashboard` lookup that downstream flattened to a `Foodashboard`-style class name with an opaque `Wheels.ViewNotFound` error. The Mapper now rejects this at route-registration time with `Wheels.MapperArgumentInvalid`, naming the namespace and the offending value and pointing at the correct shorter form, so users can find the bad route definition instead of chasing the symptom (#2791) - Linux `.deb` / `.rpm` packages double-nested the framework at `/opt/wheels/module/vendor/wheels/wheels/` instead of `/opt/wheels/module/vendor/wheels/`. `wheels-core-VER.zip` carries a top-level `wheels/` directory that `unzip` preserves; the nfpm `type: tree` rule then copied the entire `build/framework/` tree (wrapper and all) into the destination, leaving `Injector.cfc` one level too deep. Every fresh `wheels new` install on Ubuntu/Fedora then crashed on first request with `could not find component or class with name [wheels.Injector]`, cascading into the cryptic `The key [WO] does not exist.` error in `onError`. The brew formula handles this correctly via `(share/"wheels/framework/wheels").install Dir["*"]`; the Linux nfpm configs now pin `src` at `./build/framework/wheels/` to match. Regression spec at `vendor/wheels/tests/specs/cli/LinuxPackageStagingSpec.cfc` (#2773) - `onError` in the generated app template and demo `public/Application.cfc` now guards `application.wo` with `StructKeyExists(application, "wo")` after the recovery try/catch. When `new wheels.Injector(...)` fails during `onApplicationStart` (e.g. a stale `/wheels` mapping under Lucee Express 7), the original error is preserved via a minimal HTML fallback instead of cascading into the cryptic "The key [WO] does not exist" exception that hit "Your First 15 Minutes" tutorial users on fresh installs diff --git a/vendor/wheels/mapper/matching.cfc b/vendor/wheels/mapper/matching.cfc index cd6e184695..a50f847491 100644 --- a/vendor/wheels/mapper/matching.cfc +++ b/vendor/wheels/mapper/matching.cfc @@ -309,6 +309,33 @@ component { StructDelete(arguments, "to"); } + // Guard against the redundant namespace prefix in `to=` / `controller=` (see #2791). + // Inside `.namespace("foo")` the scope already supplies `package = "foo"`; a + // `controller="foo/dashboard"` (often written via `to="foo/dashboard##index"`) + // then gets joined with the package to form `foo.foo/dashboard`, which downstream + // flattens to a `Foodashboard`-style class lookup with an opaque error. Reject at + // registration time with a message that names both the namespace and the offending + // value so the user can find the route definition rather than chase a symptom. + if ( + StructKeyExists(arguments, "package") + && StructKeyExists(arguments, "controller") + && Find("/", arguments.controller) + ) { + local.packageAsPath = Replace(arguments.package, ".", "/", "all"); + local.prefix = local.packageAsPath & "/"; + if (Len(arguments.controller) > Len(local.prefix) && Left(arguments.controller, Len(local.prefix)) == local.prefix) { + local.stripped = Mid(arguments.controller, Len(local.prefix) + 1, Len(arguments.controller) - Len(local.prefix)); + local.actionForMsg = StructKeyExists(arguments, "action") ? arguments.action : "action"; + local.hh = "##"; + local.detail = "Got controller=""" & arguments.controller & """ (likely from to=""" & arguments.controller & local.hh & local.actionForMsg & """). The namespace prefix is added automatically — use controller=""" & local.stripped & """ (or to=""" & local.stripped & local.hh & local.actionForMsg & """) instead."; + Throw( + type = "Wheels.MapperArgumentInvalid", + message = "Route inside `.namespace('#arguments.package#')` (or equivalent `.scope()` / `.package()`) uses a redundant namespace prefix in its controller path.", + detail = local.detail + ); + } + } + // Pull route name from arguments if it exists. local.name = ""; if (StructKeyExists(arguments, "name")) { diff --git a/vendor/wheels/tests/specs/mapper/MatchingSpec.cfc b/vendor/wheels/tests/specs/mapper/MatchingSpec.cfc index 70568e7e8e..9c48a0eeab 100644 --- a/vendor/wheels/tests/specs/mapper/MatchingSpec.cfc +++ b/vendor/wheels/tests/specs/mapper/MatchingSpec.cfc @@ -197,6 +197,73 @@ component extends="wheels.WheelsTest" { expect(r[4]).toHaveKey("redirect") expect(r[5]).toHaveKey("redirect") }); + + // Guard against redundant namespace prefix in `to=` / `controller=`. + // See #2791: inside .namespace("foo"), writing to="foo/dashboard##index" + // silently produced a "foo.foo/dashboard" controller path which downstream + // got flattened to a Foodashboard class lookup with an opaque error. + it("Rejects to= with redundant namespace prefix", function(){ + expect(function() { + m.$draw() + .namespace("datapai") + .get(name = "datapaiDashboard", pattern = "dashboard", to = "datapai/dashboard##index") + .end() + .end(); + }).toThrow(type = "Wheels.MapperArgumentInvalid"); + }); + it("Rejects controller= with redundant namespace prefix", function(){ + expect(function() { + m.$draw() + .namespace("admin") + .$match(name = "dashboard", method = "get", controller = "admin/dashboard", action = "index") + .end() + .end(); + }).toThrow(type = "Wheels.MapperArgumentInvalid"); + }); + it("Rejects redundant prefix in nested namespaces", function(){ + expect(function() { + m.$draw() + .namespace("admin") + .namespace("users") + .get(name = "edit", pattern = "edit", to = "admin/users/profile##edit") + .end() + .end() + .end(); + }).toThrow(type = "Wheels.MapperArgumentInvalid"); + }); + it("Rejects redundant prefix inside .package() too", function(){ + expect(function() { + m.$draw() + .package("admin") + .get(name = "users", pattern = "users", to = "admin/users##index") + .end() + .end(); + }).toThrow(type = "Wheels.MapperArgumentInvalid"); + }); + it("Allows controllers whose name only happens to share a prefix with the namespace", function(){ + // "foobar/dashboard" inside .namespace("foo") is NOT a redundant prefix + // — "foobar" != "foo". Must not be rejected. + m.$draw() + .namespace("foo") + .get(name = "dashboard", pattern = "dashboard", to = "foobar/dashboard##index") + .end() + .end(); + r = m.getRoutes(); + expect(r).toBeArray(); + expect(ArrayLen(r) >= 1).toBeTrue(); + }); + it("Accepts the correct (non-redundant) form inside a namespace", function(){ + m.$draw() + .namespace("datapai") + .get(name = "datapaiDashboard", pattern = "dashboard", to = "dashboard##index") + .end() + .end(); + r = m.getRoutes(); + expect(r).toBeArray(); + expect(ArrayLen(r) >= 1).toBeTrue(); + expect(r[1].controller).toBe("datapai.dashboard"); + expect(r[1].action).toBe("index"); + }); }); } From fa83fc7e111c57931646dca21f9a5d35f12d9a20 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 23:41:16 +0000 Subject: [PATCH 2/3] fix(mapper): address Reviewer A/B consensus findings (round 1) - Snapshot `local.fromTo` / `local.originalTo` before the `to=` parse block so the error detail can distinguish `to=` vs direct `controller=` callers (Reviewer A nit). - Add `Len(arguments.package) > 0` to the guard's outer condition so an empty package does not yield `prefix = "/"` and spuriously reject controllers whose path starts with a slash (Reviewer A response, Reviewer B round-1 missed-issue). - Collapse multi-line block comments above the guard in `matching.cfc` and above the new `it()` group in `MatchingSpec.cfc` to one-liners to comply with CLAUDE.md style (both reviewers). - Add a spec asserting `$match()` with `package = ""` and a controller starting with `/` is not falsely rejected. Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> --- vendor/wheels/mapper/matching.cfc | 19 +++++++++++-------- .../tests/specs/mapper/MatchingSpec.cfc | 13 +++++++++---- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/vendor/wheels/mapper/matching.cfc b/vendor/wheels/mapper/matching.cfc index a50f847491..06e6547d77 100644 --- a/vendor/wheels/mapper/matching.cfc +++ b/vendor/wheels/mapper/matching.cfc @@ -303,21 +303,20 @@ component { } // Interpret "to" as "controller##action". + local.fromTo = false; + local.originalTo = ""; if (StructKeyExists(arguments, "to")) { + local.fromTo = true; + local.originalTo = arguments.to; arguments.controller = ListFirst(arguments.to, "##"); arguments.action = ListLast(arguments.to, "##"); StructDelete(arguments, "to"); } - // Guard against the redundant namespace prefix in `to=` / `controller=` (see #2791). - // Inside `.namespace("foo")` the scope already supplies `package = "foo"`; a - // `controller="foo/dashboard"` (often written via `to="foo/dashboard##index"`) - // then gets joined with the package to form `foo.foo/dashboard`, which downstream - // flattens to a `Foodashboard`-style class lookup with an opaque error. Reject at - // registration time with a message that names both the namespace and the offending - // value so the user can find the route definition rather than chase a symptom. + // Guard: reject redundant namespace prefix in to=/controller= (#2791). if ( StructKeyExists(arguments, "package") + && Len(arguments.package) > 0 && StructKeyExists(arguments, "controller") && Find("/", arguments.controller) ) { @@ -327,7 +326,11 @@ component { local.stripped = Mid(arguments.controller, Len(local.prefix) + 1, Len(arguments.controller) - Len(local.prefix)); local.actionForMsg = StructKeyExists(arguments, "action") ? arguments.action : "action"; local.hh = "##"; - local.detail = "Got controller=""" & arguments.controller & """ (likely from to=""" & arguments.controller & local.hh & local.actionForMsg & """). The namespace prefix is added automatically — use controller=""" & local.stripped & """ (or to=""" & local.stripped & local.hh & local.actionForMsg & """) instead."; + if (local.fromTo) { + local.detail = "Got controller=""" & arguments.controller & """ (from to=""" & local.originalTo & """). The namespace prefix is added automatically — use to=""" & local.stripped & local.hh & local.actionForMsg & """ instead."; + } else { + local.detail = "Got controller=""" & arguments.controller & """ (passed as controller=). The namespace prefix is added automatically — use controller=""" & local.stripped & """ (or to=""" & local.stripped & local.hh & local.actionForMsg & """) instead."; + } Throw( type = "Wheels.MapperArgumentInvalid", message = "Route inside `.namespace('#arguments.package#')` (or equivalent `.scope()` / `.package()`) uses a redundant namespace prefix in its controller path.", diff --git a/vendor/wheels/tests/specs/mapper/MatchingSpec.cfc b/vendor/wheels/tests/specs/mapper/MatchingSpec.cfc index 9c48a0eeab..ae9e0229c9 100644 --- a/vendor/wheels/tests/specs/mapper/MatchingSpec.cfc +++ b/vendor/wheels/tests/specs/mapper/MatchingSpec.cfc @@ -198,10 +198,7 @@ component extends="wheels.WheelsTest" { expect(r[5]).toHaveKey("redirect") }); - // Guard against redundant namespace prefix in `to=` / `controller=`. - // See #2791: inside .namespace("foo"), writing to="foo/dashboard##index" - // silently produced a "foo.foo/dashboard" controller path which downstream - // got flattened to a Foodashboard class lookup with an opaque error. + // Guard against redundant namespace prefix in to=/controller= (#2791). it("Rejects to= with redundant namespace prefix", function(){ expect(function() { m.$draw() @@ -264,6 +261,14 @@ component extends="wheels.WheelsTest" { expect(r[1].controller).toBe("datapai.dashboard"); expect(r[1].action).toBe("index"); }); + it("Does not falsely reject when package is empty", function(){ + m.$draw() + .$match(name = "edge", method = "get", pattern = "edge", controller = "/users", action = "index", package = "") + .end(); + r = m.getRoutes(); + expect(r).toBeArray(); + expect(ArrayLen(r) >= 1).toBeTrue(); + }); }); } From 092bde1884772f60d34669a1c39285663bd5b723 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 23:55:05 +0000 Subject: [PATCH 3/3] fix(mapper): address Reviewer A/B consensus findings (round 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - vendor/wheels/mapper/matching.cfc:328 — change local.hh = "##" to local.hh = "####" so the error-suggestion detail renders as to="dashboard##index" (source-correct CFML), not to="dashboard#index" (Reviewer A finding, Reviewer B verified). - vendor/wheels/tests/specs/mapper/MatchingSpec.cfc:241–242 — collapse the 2-line comment inside the "Allows controllers..." spec body to a single line per CLAUDE.md "one short line max" rule (both reviewers). Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> --- vendor/wheels/mapper/matching.cfc | 2 +- vendor/wheels/tests/specs/mapper/MatchingSpec.cfc | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/vendor/wheels/mapper/matching.cfc b/vendor/wheels/mapper/matching.cfc index 06e6547d77..5032c329b4 100644 --- a/vendor/wheels/mapper/matching.cfc +++ b/vendor/wheels/mapper/matching.cfc @@ -325,7 +325,7 @@ component { if (Len(arguments.controller) > Len(local.prefix) && Left(arguments.controller, Len(local.prefix)) == local.prefix) { local.stripped = Mid(arguments.controller, Len(local.prefix) + 1, Len(arguments.controller) - Len(local.prefix)); local.actionForMsg = StructKeyExists(arguments, "action") ? arguments.action : "action"; - local.hh = "##"; + local.hh = "####"; if (local.fromTo) { local.detail = "Got controller=""" & arguments.controller & """ (from to=""" & local.originalTo & """). The namespace prefix is added automatically — use to=""" & local.stripped & local.hh & local.actionForMsg & """ instead."; } else { diff --git a/vendor/wheels/tests/specs/mapper/MatchingSpec.cfc b/vendor/wheels/tests/specs/mapper/MatchingSpec.cfc index ae9e0229c9..f900b81abb 100644 --- a/vendor/wheels/tests/specs/mapper/MatchingSpec.cfc +++ b/vendor/wheels/tests/specs/mapper/MatchingSpec.cfc @@ -238,8 +238,7 @@ component extends="wheels.WheelsTest" { }).toThrow(type = "Wheels.MapperArgumentInvalid"); }); it("Allows controllers whose name only happens to share a prefix with the namespace", function(){ - // "foobar/dashboard" inside .namespace("foo") is NOT a redundant prefix - // — "foobar" != "foo". Must not be rejected. + // "foobar/dashboard" inside .namespace("foo") is not a redundant prefix — "foobar" != "foo". m.$draw() .namespace("foo") .get(name = "dashboard", pattern = "dashboard", to = "foobar/dashboard##index")