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.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
- `WheelsTest` auto-bind missed user-defined global helpers added via `include` in `app/global/functions.cfm`. The pseudo-constructor used `getMetaData(application.wo).functions`, which only enumerates methods declared directly on the CFC and skips symbols merged in via `cfinclude`. Specs that called custom helpers (e.g. `can()`, `hasRole()`) had to manually rebind each one in `beforeAll()`. The auto-bind now iterates `application.wo` as a struct and binds every UDF via `isCustomFunction()`, preserving the existing public-only filter for declared methods (#2790)
- Model layer SELECT clause builder now routes column identifiers through the adapter's `$quoteIdentifier`, so reserved-word column names (e.g. `key`, `order`, `group`) survive on every supported dialect instead of breaking `findAll` / `findOne` / dynamic finders with cryptic SQL syntax errors. The WHERE / ORDER BY paths already quoted columns; `$createSQLFieldList` and the empty-pagination column-list extraction in `read.cfc` now match.
- `wheels packages install <name>` is now a transparent alias for `wheels packages add <name>` on every caller path that actually reaches `Module.cfc` (stdio MCP server, scripted in-process clients, spec suite). Previously the dispatch layer's `case "install":` branch printed a yellow warning to stdout and returned `""` without installing anything — so even though `PackagesMainCli.install()` itself had been a true alias for `add()` since #2729, any caller routing through the CLI module's verb dispatch silently no-op'd. The shell-facing `wheels packages install <name>` form is still intercepted by LuCLI's built-in extension installer upstream of module dispatch and remains broken on that path (and is documented as such in the module-owned help text), but MCP tool calls and programmatic callers now behave identically to `add`. Both branches now share a single fall-through body so the validation, error shape, and install behavior cannot drift apart again.
Expand Down
30 changes: 30 additions & 0 deletions vendor/wheels/mapper/matching.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -303,12 +303,42 @@ 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: reject redundant namespace prefix in to=/controller= (#2791).
if (
StructKeyExists(arguments, "package")
&& Len(arguments.package) > 0
&& 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 = "####";
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.",
detail = local.detail
);
}
}

// Pull route name from arguments if it exists.
local.name = "";
if (StructKeyExists(arguments, "name")) {
Expand Down
71 changes: 71 additions & 0 deletions vendor/wheels/tests/specs/mapper/MatchingSpec.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,77 @@ component extends="wheels.WheelsTest" {
expect(r[4]).toHaveKey("redirect")
expect(r[5]).toHaveKey("redirect")
});

// Guard against redundant namespace prefix in to=/controller= (#2791).
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".
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");
});
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();
});
});
}

Expand Down
Loading