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-404-guard.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Scaffolded controllers now return **404** for a key that matches no record, including a soft-deleted one. Previously `show`, `edit`, `update` and `delete` called `findByKey` bare, and a missing row made the action operate on an empty value — the view threw and the user saw a 500. A `requireRecord` before-filter now throws `Wheels.RecordNotFound`, which the framework maps to 404
28 changes: 28 additions & 0 deletions cli/lucli/templates/codegen/CRUDContent.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@
*/
function config() {
super.config();
// 404 rather than a 500 when the key matches nothing. findByKey returns a
// non-object for BOTH a missing key and a soft-deleted row, and the
// actions below then operate on that empty value — `post.title` in the
// view, or `.update()` here, throws.
//
// A BEFORE filter that deliberately does NOT assign the record: the
// actions keep their conventional single-finder body, which the
// `--belongsTo` parent wiring depends on — it only rewrites `show()`
// when the finder is the whole body, and skips a customized one.
// Loading twice is cheap and keeps that contract intact.
filters(through="requireRecord", only="show,edit,update,delete");
}

/**
Expand Down Expand Up @@ -81,4 +92,21 @@
redirectTo(route="|ObjectNamePlural|");
}

/**
* Guard: abort with a 404 when the key matches no record.
*
* The message is a plain string on purpose. The scaffold's parent wiring
* scans this file with ScaffoldSource, which refuses interpolated strings
* (`##...##`) wholesale — a single `##params.key##` here would silently turn
* off `include=` wiring for every `--belongsTo` child of this model.
**/
private function requireRecord() {
if (!IsObject(model("|ObjectNameSingularC|").findByKey(key=params.key))) {
Throw(
type = "Wheels.RecordNotFound",
message = "|ObjectNameSingularC| not found for the requested key."
);
}
}

}
34 changes: 34 additions & 0 deletions cli/lucli/tests/specs/services/ScaffoldSpec.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,40 @@ component extends="wheels.wheelstest.system.BaseSpec" {
expect(content).toInclude("function delete()");
});

it("guards every key-loading action against a missing record", () => {
// findByKey returns a non-object for BOTH a missing key and a
// soft-deleted row. Without a guard the action renders against
// that empty value and the view throws a 500 where a 404
// belongs — reproduced on a soft-deleted post before the fix.
var result = scaffold.generateScaffold(
name = "Notefile",
properties = [{name = "title", type = "string"}],
force = true
);
expect(result.success).toBeTrue();
var content = fileRead(tempRoot & "/app/controllers/Notefiles.cfc");

// A BEFORE filter covering all four key-loading actions.
expect(content).toInclude('filters(through="requireRecord", only="show,edit,update,delete")');
expect(content).toInclude("private function requireRecord()");
expect(content).toInclude('if (!IsObject(model("Notefile").findByKey(key=params.key))) {');
expect(content).toInclude('type = "Wheels.RecordNotFound"');

// The guard must NOT live inside show(): ScaffoldSource only
// rewrites show() when its finder is the whole body, so an
// inline guard there disables `--belongsTo` include= wiring.
var showBody = ReMatchNoCase("function show\(\) \{[^}]+\}", content);
expect(ArrayLen(showBody)).toBe(1);
expect(showBody[1]).notToInclude("IsObject");

// And the generated file must still SCAN. ScaffoldSource fails
// closed on any interpolated string, so a single `##...##` in
// the guard's message would silently switch off the parent
// wiring for every child of this model. Pin it at the source.
var scanned = new cli.lucli.services.ScaffoldSource().scan(content);
expect(scanned.valid).toBeTrue(scanned.reason);
});

it("emits a full CRUD controller spec with model().create() test data", () => {
var result = scaffold.generateScaffold(
name = "Chronicle",
Expand Down
Loading