From 083854563ca463dd6c8fed9e4e2c885402c53b57 Mon Sep 17 00:00:00 2001 From: Peter Amiri Date: Wed, 22 Apr 2026 05:34:37 -0700 Subject: [PATCH] refactor(router): move framework browser-test fixtures and routes out of app (#2138, #2135) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue #2138: Three `BrowserTest*.cfc` controllers, their view folders, and a stray `UserNotificationsMailer.cfc` demo were living in the dogfood app's `app/` directory but are framework-internal, not app-level code. Anyone copying this repo as a starting template or inspecting it as reference would see fixtures they did not write. Issue #2135: The same leak showed up in `config/routes.cfm` as a `/_browser` scope with six routes — internal test infrastructure sitting in the one file a developer is supposed to own. Fix both in one pass, since they share the same files and routes: - Move the three fixture controllers and their view folders into `vendor/wheels/public/browser-fixtures/{controllers,views}/`, alongside a minimal `Controller.cfc` base that provides a `$renderBrowserFixtureView` helper. Fixtures now render via explicit `cfinclude` + `renderText` so they do not depend on the normal Wheels view-path resolver (Wheels' `viewPath` setting is a single string pinned to `/app/views`). - Add `vendor/wheels/public/browser-fixtures/routes.cfm` holding the `/_browser/*` route scope with the same named routes as before (`browserTestHome`, `browserTestLogin`, `browserTestAuthenticate`, `browserTestDashboard`, `browserTestLogout`, `browserTestLoginAs`). - Auto-mount from `vendor/wheels/Global.cfc::$lockedLoadRoutes`, mirroring how `/wheels/public/routes.cfm` (the framework GUI) is already mounted. Gated by (a) `environment ∈ {testing, development}` and (b) a new opt-in setting `loadBrowserTestFixtures` that defaults to `false`. - Enable the setting in this dogfood app via `config/settings.cfm` so browser specs that hit the real HTTP server keep working. - Delete the six `/_browser` routes from `config/routes.cfm`; the file now matches the clean lucli scaffold template. - Delete the stray `app/mailers/UserNotificationsMailer.cfc` plus its view — demo-only files that are already duplicated under `examples/tweet/` and `examples/starter-app/`. Core test suite: 3250 pass, 0 fail, 0 error on Lucee 7 + SQLite. Manual verification confirms `/_browser/home`, `/_browser/login`, `/_browser/login-as`, and the full login → dashboard → logout redirect flow all return 200 when the setting is on, and 404 when it is off. Upgrade-guide entry added: apps that defined their own `/_browser/*` routes of their own in 4.0-snapshot need to opt in to the new setting or declare their own routes — the framework's routes register before `config/routes.cfm` so same-named app routes still win. Closes #2138 Closes #2135 Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 2 + app/controllers/BrowserTestHome.cfc | 19 - app/controllers/BrowserTestLogin.cfc | 17 - app/controllers/BrowserTestSessions.cfc | 22 - app/mailers/UserNotificationsMailer.cfc | 39 -- app/views/browsertesthome/dashboard.cfm | 8 - app/views/browsertesthome/index.cfm | 5 - app/views/browsertesthome/layout.cfm | 12 - app/views/browsertestlogin/create.cfm | 4 - app/views/browsertestlogin/layout.cfm | 12 - app/views/browsertestsessions/layout.cfm | 12 - app/views/browsertestsessions/new.cfm | 14 - .../usernotificationsmailer/sendEmail.cfm | 20 - config/routes.cfm | 37 +- config/settings.cfm | 26 +- vendor/wheels/Global.cfc | 514 ++++++++++-------- vendor/wheels/events/init/views.cfm | 81 +-- .../controllers/BrowserTestHome.cfc | 33 ++ .../controllers/BrowserTestLogin.cfc | 23 + .../controllers/BrowserTestSessions.cfc | 29 + .../controllers/Controller.cfc | 51 ++ .../wheels/public/browser-fixtures/routes.cfm | 28 + .../views/browsertesthome/dashboard.cfm | 10 + .../views/browsertesthome/index.cfm | 5 + .../views/browsertesthome/layout.cfm | 12 + .../views/browsertestlogin/create.cfm | 4 + .../views/browsertestlogin/layout.cfm | 12 + .../views/browsertestsessions/layout.cfm | 12 + .../views/browsertestsessions/new.cfm | 14 + .../v4-0-0-snapshot/upgrading/3x-to-4x.mdx | 12 + 30 files changed, 616 insertions(+), 473 deletions(-) delete mode 100644 app/controllers/BrowserTestHome.cfc delete mode 100644 app/controllers/BrowserTestLogin.cfc delete mode 100644 app/controllers/BrowserTestSessions.cfc delete mode 100644 app/mailers/UserNotificationsMailer.cfc delete mode 100644 app/views/browsertesthome/dashboard.cfm delete mode 100644 app/views/browsertesthome/index.cfm delete mode 100644 app/views/browsertesthome/layout.cfm delete mode 100644 app/views/browsertestlogin/create.cfm delete mode 100644 app/views/browsertestlogin/layout.cfm delete mode 100644 app/views/browsertestsessions/layout.cfm delete mode 100644 app/views/browsertestsessions/new.cfm delete mode 100644 app/views/usernotificationsmailer/sendEmail.cfm create mode 100644 vendor/wheels/public/browser-fixtures/controllers/BrowserTestHome.cfc create mode 100644 vendor/wheels/public/browser-fixtures/controllers/BrowserTestLogin.cfc create mode 100644 vendor/wheels/public/browser-fixtures/controllers/BrowserTestSessions.cfc create mode 100644 vendor/wheels/public/browser-fixtures/controllers/Controller.cfc create mode 100644 vendor/wheels/public/browser-fixtures/routes.cfm create mode 100644 vendor/wheels/public/browser-fixtures/views/browsertesthome/dashboard.cfm create mode 100644 vendor/wheels/public/browser-fixtures/views/browsertesthome/index.cfm create mode 100644 vendor/wheels/public/browser-fixtures/views/browsertesthome/layout.cfm create mode 100644 vendor/wheels/public/browser-fixtures/views/browsertestlogin/create.cfm create mode 100644 vendor/wheels/public/browser-fixtures/views/browsertestlogin/layout.cfm create mode 100644 vendor/wheels/public/browser-fixtures/views/browsertestsessions/layout.cfm create mode 100644 vendor/wheels/public/browser-fixtures/views/browsertestsessions/new.cfm diff --git a/CHANGELOG.md b/CHANGELOG.md index d3b6d0c26c..010059dcc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -140,6 +140,8 @@ All historical references to "CFWheels" in this changelog have been preserved fo ### Fixed +- Framework-internal browser-test fixture controllers, views, and the `/_browser/*` routes no longer leak into application-level files. Moved from `app/controllers/BrowserTest*.cfc`, `app/views/browsertest*/`, and `config/routes.cfm` into `vendor/wheels/public/browser-fixtures/`, auto-mounted by `$lockedLoadRoutes` when environment is `testing` or `development` and the new opt-in setting `loadBrowserTestFixtures=true` is set. Apps upgrading from a 4.0 snapshot that had custom `/_browser/*` routes must opt in explicitly or re-declare them in `config/routes.cfm`. (#2135, #2138) +- Stray `app/mailers/UserNotificationsMailer.cfc` demo removed from the framework repo root (byte-identical copies remain in the example apps under `examples/tweet/` and `examples/starter-app/`). (#2138) - View lookup after `renderText()` / `renderWith()` no longer breaks subsequent partial rendering (#1991) - Scaffolded apps from `wheels new` now boot correctly (#2096) - `wheels stats` crash on Lucee 7 — private `sprintf()` helper called `Left(result, 0)` when the format string started with a placeholder. Lucee 7 throws where Lucee 6 returned empty silently. Added a ternary guard per the project's cross-engine compatibility pattern. diff --git a/app/controllers/BrowserTestHome.cfc b/app/controllers/BrowserTestHome.cfc deleted file mode 100644 index ae9c6f0916..0000000000 --- a/app/controllers/BrowserTestHome.cfc +++ /dev/null @@ -1,19 +0,0 @@ -component extends="Controller" { - - function config() { - filters(through="$requireLogin", except="index"); - } - - function index() { - } - - function dashboard() { - user = {email: session.userEmail ?: ""}; - } - - private function $requireLogin() { - if (!structKeyExists(session, "userId")) { - redirectTo(route="browserTestLogin"); - } - } -} diff --git a/app/controllers/BrowserTestLogin.cfc b/app/controllers/BrowserTestLogin.cfc deleted file mode 100644 index 3de4c69ebe..0000000000 --- a/app/controllers/BrowserTestLogin.cfc +++ /dev/null @@ -1,17 +0,0 @@ -component extends="Controller" { - - function config() { - } - - function create() { - if (!listFindNoCase("testing,development", application.$wheels.environment)) { - throw( - type="Wheels.BrowserTestSecurityError", - message="loginAs endpoint is only available in testing/development environments" - ); - } - - session.userId = 1; - session.userEmail = params.identifier; - } -} diff --git a/app/controllers/BrowserTestSessions.cfc b/app/controllers/BrowserTestSessions.cfc deleted file mode 100644 index 034ad2e565..0000000000 --- a/app/controllers/BrowserTestSessions.cfc +++ /dev/null @@ -1,22 +0,0 @@ -component extends="Controller" { - - function new() { - flashError = flash("error") ?: ""; - } - - function create() { - if (params.email == "alice@example.com" && params.password == "secret") { - session.userId = 1; - session.userEmail = params.email; - redirectTo(route="browserTestDashboard"); - } else { - flashInsert(error="Invalid credentials"); - redirectTo(route="browserTestLogin"); - } - } - - function destroy() { - structClear(session); - redirectTo(route="browserTestLogin"); - } -} diff --git a/app/mailers/UserNotificationsMailer.cfc b/app/mailers/UserNotificationsMailer.cfc deleted file mode 100644 index 781619a8d5..0000000000 --- a/app/mailers/UserNotificationsMailer.cfc +++ /dev/null @@ -1,39 +0,0 @@ -/** - * UserNotificationsMailer - Handles email notifications - */ -component extends="wheels.Mailer" { - - /** - * Constructor - Configure default settings - */ - function config() { - } - - /** - * Send Send Email email - * @to.hint Recipient email address - * @subject.hint Email subject line - * @data.hint Additional data to pass to the view - */ - function sendEmail( - required string to, - string subject = "Send Email", - struct data = {} - ) { - // Prepare email data - local.emailData = duplicate(arguments.data); - local.emailData.to = arguments.to; - local.emailData.subject = arguments.subject; - - // Set email properties - to(arguments.to); - subject(arguments.subject); - - // Render email template - template("/usernotificationsmailer/sendEmail"); - - // Send the email - return deliver(); - } - -} \ No newline at end of file diff --git a/app/views/browsertesthome/dashboard.cfm b/app/views/browsertesthome/dashboard.cfm deleted file mode 100644 index 9125f9ad4f..0000000000 --- a/app/views/browsertesthome/dashboard.cfm +++ /dev/null @@ -1,8 +0,0 @@ - - -

Dashboard

-

Welcome, #encodeForHTML(user.email)#

-
- -
-
diff --git a/app/views/browsertesthome/index.cfm b/app/views/browsertesthome/index.cfm deleted file mode 100644 index 1e45b0bdd0..0000000000 --- a/app/views/browsertesthome/index.cfm +++ /dev/null @@ -1,5 +0,0 @@ - -

Home

-

Welcome to the browser test fixture app.

-Log in -
diff --git a/app/views/browsertesthome/layout.cfm b/app/views/browsertesthome/layout.cfm deleted file mode 100644 index 71f54fd49c..0000000000 --- a/app/views/browsertesthome/layout.cfm +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - Browser Test Fixture - - - #contentForLayout()# - - - diff --git a/app/views/browsertestlogin/create.cfm b/app/views/browsertestlogin/create.cfm deleted file mode 100644 index 3d9b6f9bec..0000000000 --- a/app/views/browsertestlogin/create.cfm +++ /dev/null @@ -1,4 +0,0 @@ - - -

Logged in as #encodeForHTML(params.identifier ?: "unknown")#

-
diff --git a/app/views/browsertestlogin/layout.cfm b/app/views/browsertestlogin/layout.cfm deleted file mode 100644 index 71f54fd49c..0000000000 --- a/app/views/browsertestlogin/layout.cfm +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - Browser Test Fixture - - - #contentForLayout()# - - - diff --git a/app/views/browsertestsessions/layout.cfm b/app/views/browsertestsessions/layout.cfm deleted file mode 100644 index 71f54fd49c..0000000000 --- a/app/views/browsertestsessions/layout.cfm +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - Browser Test Fixture - - - #contentForLayout()# - - - diff --git a/app/views/browsertestsessions/new.cfm b/app/views/browsertestsessions/new.cfm deleted file mode 100644 index 864dbc2d2a..0000000000 --- a/app/views/browsertestsessions/new.cfm +++ /dev/null @@ -1,14 +0,0 @@ - - -

Log in

- -
#flashError#
-
-
- - - - - -
-
diff --git a/app/views/usernotificationsmailer/sendEmail.cfm b/app/views/usernotificationsmailer/sendEmail.cfm deleted file mode 100644 index 02e5bf3633..0000000000 --- a/app/views/usernotificationsmailer/sendEmail.cfm +++ /dev/null @@ -1,20 +0,0 @@ - - - -

Send Email

- -

Hello,

- -

- This is the email template for the sendEmail action. - Customize this template with your email content. -

- - - - - - -

Best regards,
Your Team

- -
\ No newline at end of file diff --git a/config/routes.cfm b/config/routes.cfm index f653170a01..193361273b 100755 --- a/config/routes.cfm +++ b/config/routes.cfm @@ -1,28 +1,15 @@ - - // Use this file to add routes to your application and point the root route to a controller action. - // Don't forget to issue a reload request (e.g. reload=true) after making changes. - // See https://wheels.dev/3.1.0/guides/handling-requests-with-controllers/routing for more info. - - mapper() - // CLI-Appends-Here - - // Browser test fixture routes (loginAs endpoint is env-gated in controller) - .scope(path="/_browser") - .get(name="browserTestHome", pattern="/home", to="BrowserTestHome##index") - .get(name="browserTestLogin", pattern="/login", to="BrowserTestSessions##new") - .post(name="browserTestAuthenticate", pattern="/login", to="BrowserTestSessions##create") - .get(name="browserTestDashboard", pattern="/dashboard", to="BrowserTestHome##dashboard") - .post(name="browserTestLogout", pattern="/logout", to="BrowserTestSessions##destroy") - .get(name="browserTestLoginAs", pattern="/login-as", to="BrowserTestLogin##create") - .end() - - // The "wildcard" call below enables automatic mapping of "controller/action" type routes. - // This way you don't need to explicitly add a route every time you create a new action in a controller. - .wildcard() - - // The root route below is the one that will be called on your application's home page (e.g. http://127.0.0.1/). - //.root(to = "home##index", method = "get") - .root(method = "get") +// Use this file to add routes to your application and point the root route to a controller action. +// Don't forget to issue a reload request (e.g. reload=true) after making changes. +// See https://wheels.dev/3.1.0/guides/handling-requests-with-controllers/routing for more info. + +mapper() + // CLI-Appends-Here + // The "wildcard" call below enables automatic mapping of "controller/action" type routes. + // This way you don't need to explicitly add a route every time you create a new action in a controller. + .wildcard() + // The root route below is the one that will be called on your application's home page (e.g. http://127.0.0.1/). + // .root(to = "home##index", method = "get") + .root(method = "get") .end(); diff --git a/config/settings.cfm b/config/settings.cfm index 4d944e635f..cf7ecb547f 100644 --- a/config/settings.cfm +++ b/config/settings.cfm @@ -1,32 +1,36 @@ - /* +/* Use this file to configure your application. You can also use the environment specific files (e.g. /config/production/settings.cfm) to override settings set here. Don't forget to issue a reload request (e.g. reload=true) after making changes. See https://wheels.dev/3.1.0/guides/working-with-wheels/configuration-and-defaults for more info. */ - /* +/* You can change the "wheels.dev" value from the two functions below to set your datasource. You can change the the value for the "dataSourceName" to set a default datasource to be used throughout your application. You can also change the value for the "coreTestDataSourceName" to set your testing datasource. You can also uncomment the 2 "set" functions below them to set the username and password for the datasource. */ - set(coreTestDataSourceName="wheels-dev"); - set(dataSourceName="wheels-dev"); - // set(dataSourceUserName=""); - // set(dataSourcePassword=""); +set(coreTestDataSourceName = "wheels-dev"); +set(dataSourceName = "wheels-dev"); +// set(dataSourceUserName=""); +// set(dataSourcePassword=""); - /* +/* If you comment out the following line, Wheels will try to determine the URL rewrite capabilities automatically. The "URLRewriting" setting can bet set to "on", "partial" or "off". To run with "partial" rewriting, the "cgi.path_info" variable needs to be supported by the web server. To run with rewriting set to "on", you need to apply the necessary rewrite rules on the web server first. */ - set(URLRewriting="On"); +set(URLRewriting = "On"); - // Reload your application with ?reload=true&password=wheels.dev - set(reloadPassword="wheels-dev"); +// Reload your application with ?reload=true&password=wheels.dev +set(reloadPassword = "wheels-dev"); - // CLI-Appends-Here +// Opt in to the framework's browser-test fixture routes (`/_browser/*`). +// Only mounted when environment is `testing` or `development`. See issues #2135, #2138. +set(loadBrowserTestFixtures = true); + +// CLI-Appends-Here diff --git a/vendor/wheels/Global.cfc b/vendor/wheels/Global.cfc index 20ed24d941..40bdd9e41c 100644 --- a/vendor/wheels/Global.cfc +++ b/vendor/wheels/Global.cfc @@ -1,11 +1,18 @@ component output="false" { - public any function $doubleCheckedLock(required string name, required string condition, required string execute, struct conditionArgs = "#StructNew()#", struct executeArgs = "#StructNew()#", numeric timeout=30){ + public any function $doubleCheckedLock( + required string name, + required string condition, + required string execute, + struct conditionArgs = "#StructNew()#", + struct executeArgs = "#StructNew()#", + numeric timeout = 30 + ) { local.rv = $invoke(method = arguments.condition, invokeArgs = arguments.conditionArgs); - if(IsBoolean(local.rv) AND NOT local.rv){ + if (IsBoolean(local.rv) AND NOT local.rv) { lock timeout="#arguments.timeout#" name="#arguments.name#" { local.rv = $invoke(method = arguments.condition, invokeArgs = arguments.conditionArgs); - if(IsBoolean(local.rv) AND NOT local.rv){ + if (IsBoolean(local.rv) AND NOT local.rv) { local.rv = $invoke(method = arguments.execute, invokeArgs = arguments.executeArgs) } } @@ -13,28 +20,38 @@ component output="false" { return local.rv; } - public any function $simpleLock(required string name, required string type, required string execute, struct executeArgs = "#StructNew()#", numeric timeout=30){ - if(StructKeyExists(arguments, "object")){ + public any function $simpleLock( + required string name, + required string type, + required string execute, + struct executeArgs = "#StructNew()#", + numeric timeout = 30 + ) { + if (StructKeyExists(arguments, "object")) { lock name="#arguments.name#" type="#arguments.type#" timeout="#arguments.timeout#" { - local.rv = $invoke(component = "#arguments.object#", method = "#arguments.execute#", argumentCollection = "#arguments.executeArgs#"); + local.rv = $invoke( + component = "#arguments.object#", + method = "#arguments.execute#", + argumentCollection = "#arguments.executeArgs#" + ); } - }else{ + } else { arguments.executeArgs.$locked = true; - lock name="#arguments.name#" type="#arguments.type#" timeout="#arguments.timeout#"{ + lock name="#arguments.name#" type="#arguments.type#" timeout="#arguments.timeout#" { local.rv = $invoke(method = "#arguments.execute#", argumentCollection = "#arguments.executeArgs#"); } } - if(StructKeyExists(local, "rv")){ + if (StructKeyExists(local, "rv")) { return local.rv; } } - public struct function $image(){ - local.rv = {}; + public struct function $image() { + local.rv = {}; if (arguments.action == "info") { local.rv = $engineAdapter().imageInfo(arguments.source); } else if ($engineAdapter().isBoxLang()) { - throw( + Throw( type = "Wheels.Image.UnsupportedAction", message = "The `$image()` function in BoxLang currently supports only the 'info' action." ); @@ -47,79 +64,79 @@ component output="false" { return local.rv; } - public void function $mail(){ - if(StructKeyExists(arguments, "mailparts")){ - local.mailparts = arguments.mailparts; - StructDelete(arguments, "mailparts"); - } - if(StructKeyExists(arguments, "mailparams")){ - local.mailparams = arguments.mailparams; - StructDelete(arguments, "mailparams"); - } - if(StructKeyExists(arguments, "tagContent")){ - local.tagContent = arguments.tagContent; - StructDelete(arguments, "tagContent"); - } - cfmail(attributeCollection="#arguments#"){ - if(StructKeyExists(local, "mailparams")){ - for(local.i in local.mailparams){ - cfmailparam(attributeCollection="#local.i#"); - } - } - if(StructKeyExists(local, "mailparts")){ - for(local.i in local.mailparts){ - local.innerTagContent = local.i.tagContent; - StructDelete(local.i, "tagContent"); - cfmailpart(attributeCollection="#local.i#"){ - writeOutput(local.innerTagContent) + public void function $mail() { + if (StructKeyExists(arguments, "mailparts")) { + local.mailparts = arguments.mailparts; + StructDelete(arguments, "mailparts"); + } + if (StructKeyExists(arguments, "mailparams")) { + local.mailparams = arguments.mailparams; + StructDelete(arguments, "mailparams"); + } + if (StructKeyExists(arguments, "tagContent")) { + local.tagContent = arguments.tagContent; + StructDelete(arguments, "tagContent"); + } + cfmail(attributeCollection = "#arguments#") { + if (StructKeyExists(local, "mailparams")) { + for (local.i in local.mailparams) { + cfmailparam(attributeCollection = "#local.i#"); + } + } + if (StructKeyExists(local, "mailparts")) { + for (local.i in local.mailparts) { + local.innerTagContent = local.i.tagContent; + StructDelete(local.i, "tagContent"); + cfmailpart(attributeCollection = "#local.i#") { + WriteOutput(local.innerTagContent) } - } - } - if(StructKeyExists(local, "tagContent")){ - writeOutput(local.tagContent) - } - } + } + } + if (StructKeyExists(local, "tagContent")) { + WriteOutput(local.tagContent) + } + } } - public any function $cache(){ - // If cache is found only the function is aborted, not page. ---> + public any function $cache() { + // If cache is found only the function is aborted, not page. ---> variables.$instance.reCache = false; - cfcache(attributeCollection="#arguments#"); + cfcache(attributeCollection = "#arguments#"); variables.$instance.reCache = true; } - public any function $content(){ - cfcontent(attributeCollection="#arguments#"); + public any function $content() { + cfcontent(attributeCollection = "#arguments#"); } - public void function $header(){ + public void function $header() { // Adobe CF 2025 removed statusText attribute - remove it if present - if (structKeyExists(arguments, "statusText")) { + if (StructKeyExists(arguments, "statusText")) { local.args = {}; for (local.key in arguments) { if (local.key != "statusText") { local.args[local.key] = arguments[local.key]; } } - cfheader(attributeCollection="#local.args#"); + cfheader(attributeCollection = "#local.args#"); } else { - cfheader(attributeCollection="#arguments#"); + cfheader(attributeCollection = "#arguments#"); } } - public void function $include(required string template){ - include "#LCase(arguments.template)#"; + public void function $include(required string template) { + include "#LCase(arguments.template)#"; } - public void function $includeAndOutput(required string template){ - include "#LCase(arguments.template)#"; + public void function $includeAndOutput(required string template) { + include "#LCase(arguments.template)#"; } - public string function $includeAndReturnOutput(required string $template){ - // Make it so the developer can reference passed in arguments in the loc scope if they prefer. - if(StructKeyExists(arguments, "$type") AND arguments.$type IS "partial"){ + public string function $includeAndReturnOutput(required string $template) { + // Make it so the developer can reference passed in arguments in the loc scope if they prefer. + if (StructKeyExists(arguments, "$type") AND arguments.$type IS "partial") { local = arguments; - } + } // Include the template and return the result. // Variable is set to $wheels to limit chances of it being overwritten in the included template. // cfformat-ignore-start @@ -127,72 +144,72 @@ component output="false" { include "#LCase(arguments.$template)#" }; // cfformat-ignore-end - return local.$wheels; +return local.$wheels; } - public any function $directory(){ - local.rv = ""; - arguments.name = "rv"; - cfdirectory(attributeCollection="#arguments#"); + public any function $directory() { + local.rv = ""; + arguments.name = "rv"; + cfdirectory(attributeCollection = "#arguments#"); return local.rv; } - public any function $file(){ - cffile(attributeCollection="#arguments#"); + public any function $file() { + cffile(attributeCollection = "#arguments#"); } - public any function $cfinvoke(required string component, required string method, struct invokeArguments){ - cfinvoke - component="#arguments.component#" - method="#arguments.method#" - returnVariable="#arguments.returnVariable#" - argumentCollection="#arguments.invokeArguments#"; - return local.rv; + public any function $cfinvoke(required string component, required string method, struct invokeArguments) { + cfinvoke + component = "#arguments.component#" + method = "#arguments.method#" + returnVariable = "#arguments.returnVariable#" + argumentCollection = "#arguments.invokeArguments#"; + return local.rv; } - public any function $invoke(){ - arguments.returnVariable = "local.rv"; - if(StructKeyExists(arguments, "componentReference")){ - arguments.component = arguments.componentReference; - StructDelete(arguments, "componentReference"); - }else if(NOT StructKeyExists(variables, arguments.method)){ - // this is done so that we can call dynamic methods via "onMissingMethod" on the object (we need to pass in the object for this so it can call methods on the "this" scope instead) - arguments.component = this; - } - if(StructKeyExists(arguments, "invokeArgs")){ - arguments.argumentCollection = arguments.invokeArgs; - if(StructCount(arguments.argumentCollection) IS NOT ListLen(StructKeyList(arguments.argumentCollection))){ - // work-around for fasthashremoved cf8 bug - arguments.argumentCollection = StructNew(); - for(local.i in StructKeyList(arguments.invokeArgs)){ - arguments.argumentCollection[local.i] = arguments.invokeArgs[local.i]; - } - } + public any function $invoke() { + arguments.returnVariable = "local.rv"; + if (StructKeyExists(arguments, "componentReference")) { + arguments.component = arguments.componentReference; + StructDelete(arguments, "componentReference"); + } else if (NOT StructKeyExists(variables, arguments.method)) { + // this is done so that we can call dynamic methods via "onMissingMethod" on the object (we need to pass in the object for this so it can call methods on the "this" scope instead) + arguments.component = this; + } + if (StructKeyExists(arguments, "invokeArgs")) { + arguments.argumentCollection = arguments.invokeArgs; + if (StructCount(arguments.argumentCollection) IS NOT ListLen(StructKeyList(arguments.argumentCollection))) { + // work-around for fasthashremoved cf8 bug + arguments.argumentCollection = StructNew(); + for (local.i in StructKeyList(arguments.invokeArgs)) { + arguments.argumentCollection[local.i] = arguments.invokeArgs[local.i]; + } + } - if(StructKeyExists(arguments.invokeArgs, "componentReference")){ - arguments.component = arguments.invokeArgs.componentReference; - } + if (StructKeyExists(arguments.invokeArgs, "componentReference")) { + arguments.component = arguments.invokeArgs.componentReference; + } - StructDelete(arguments, "invokeArgs"); - } - cfinvoke(attributeCollection="#arguments#"); - if(StructKeyExists(local, "rv")){ - return local.rv; - } + StructDelete(arguments, "invokeArgs"); + } + cfinvoke(attributeCollection = "#arguments#"); + if (StructKeyExists(local, "rv")) { + return local.rv; + } } - public void function $location(boolean delay = false){ - StructDelete(arguments, "$args", false); - if(NOT arguments.delay){ - StructDelete(arguments, "delay", false); - cflocation(attributeCollection="#arguments#"); - } + public void function $location(boolean delay = false) { + StructDelete(arguments, "$args", false); + if (NOT arguments.delay) { + StructDelete(arguments, "delay", false); + cflocation(attributeCollection = "#arguments#"); + } } - public void function $htmlhead(){ - cfhtmlhead(attributeCollection="#arguments#"); + public void function $htmlhead() { + cfhtmlhead(attributeCollection = "#arguments#"); } public any function $dbinfo() { @@ -241,7 +258,7 @@ component output="false" { AND i.type_desc IN ('CLUSTERED', 'NONCLUSTERED') ORDER BY i.name, ic.key_ordinal "; - local.rv = $query(sql=local.sql, datasource=arguments.datasource); + local.rv = $query(sql = local.sql, datasource = arguments.datasource); return local.rv; } @@ -267,13 +284,13 @@ component output="false" { AND ai.INDEX_TYPE != 'LOB' ORDER BY ai.INDEX_NAME, ac.COLUMN_POSITION "; - local.rv = $query(sql=local.sql, datasource=arguments.datasource); + local.rv = $query(sql = local.sql, datasource = arguments.datasource); return local.rv; } } if ( - structKeyExists(arguments, "type") && + StructKeyExists(arguments, "type") && arguments.type eq "index" && $get("adapterName") eq "SQLiteModel" ) { @@ -345,11 +362,11 @@ component output="false" { // Override name for test mode if ( arguments.type IS "version" AND - structKeyExists(url, "controller") AND - structKeyExists(url, "action") AND - structKeyExists(url, "view") AND - structKeyExists(url, "type") AND - structKeyExists(url, "adapter") + StructKeyExists(url, "controller") AND + StructKeyExists(url, "action") AND + StructKeyExists(url, "view") AND + StructKeyExists(url, "type") AND + StructKeyExists(url, "adapter") ) { if (url.controller IS "wheels" AND url.action IS "wheels" AND url.view IS "tests" AND url.type IS "core") { QuerySetCell(local.rv, "driver_name", url.adapter); @@ -359,28 +376,28 @@ component output="false" { return local.rv; } - public any function $wddx(required any input, string action = "cfml2wddx", boolean useTimeZoneInfo = true){ - arguments.output = "local.output"; - cfwddx(attributeCollection="#arguments#"); - if(StructKeyExists(local, "output")){ - return local.output; - } + public any function $wddx(required any input, string action = "cfml2wddx", boolean useTimeZoneInfo = true) { + arguments.output = "local.output"; + cfwddx(attributeCollection = "#arguments#"); + if (StructKeyExists(local, "output")) { + return local.output; + } } - public any function $zip(){ + public any function $zip() { $engineAdapter().prepareZipArgs(arguments); - cfzip(attributeCollection="#arguments#"); + cfzip(attributeCollection = "#arguments#"); } - public any function $query(required string sql){ + public any function $query(required string sql) { StructDelete(arguments, "name"); // allow the use of query of queries, caveat: Query must be called query. Eg: SELECT * from query - if(StructKeyExists(arguments, "query") && IsQuery(arguments.query)){ + if (StructKeyExists(arguments, "query") && IsQuery(arguments.query)) { var query = Duplicate(arguments.query); } - local.rv = queryExecute(PreserveSingleQuotes(arguments.sql), [], arguments); + local.rv = QueryExecute(PreserveSingleQuotes(arguments.sql), [], arguments); // some sql statements may not return a value - if(StructKeyExists(local, "rv")){ + if (StructKeyExists(local, "rv")) { return local.rv; } } @@ -407,7 +424,7 @@ component output="false" { * @name The environment variable name to look up. * @default Value to return if the variable is not found. */ - public any function env(required string name, any default="") { + public any function env(required string name, any default = "") { if (StructKeyExists(application, "env") && StructKeyExists(application.env, arguments.name)) { return application.env[arguments.name]; } @@ -444,7 +461,10 @@ component output="false" { !Len(arguments.functionName) && IsDefined("request.wheels.tenant.config") && StructKeyExists(request.wheels.tenant.config, arguments.name) - && !ListFindNoCase("encryptionAlgorithm,encryptionSecretKey,encryptionEncoding,CSRFProtection,csrfStore,reloadPassword,obfuscateUrls", arguments.name) + && !ListFindNoCase( + "encryptionAlgorithm,encryptionSecretKey,encryptionEncoding,CSRFProtection,csrfStore,reloadPassword,obfuscateUrls", + arguments.name + ) ) { return request.wheels.tenant.config[arguments.name]; } @@ -525,10 +545,7 @@ component output="false" { */ public void function switchTenant(required struct tenant, boolean force = false) { if (!StructKeyExists(arguments.tenant, "dataSource") || !Len(arguments.tenant.dataSource)) { - Throw( - type = "Wheels.InvalidTenant", - message = "The tenant struct must contain a non-empty `dataSource` key." - ); + Throw(type = "Wheels.InvalidTenant", message = "The tenant struct must contain a non-empty `dataSource` key."); } if (!StructKeyExists(request, "wheels")) { request.wheels = {}; @@ -602,7 +619,7 @@ component output="false" { if (IsNumeric(arguments.cache)) { local.cache = arguments.cache; } - local.listArray = [0,0,0,0]; + local.listArray = [0, 0, 0, 0]; local.dateParts = "d,h,n,s"; local.datePartsArray = ListToArray(local.dateParts); local.iEnd = ArrayLen(local.datePartsArray); @@ -611,12 +628,7 @@ component output="false" { local.listArray[local.i] = local.cache; } } - local.rv = CreateTimespan( - local.listArray[1], - local.listArray[2], - local.listArray[3], - local.listArray[4] - ); + local.rv = CreateTimespan(local.listArray[1], local.listArray[2], local.listArray[3], local.listArray[4]); return local.rv; } @@ -751,12 +763,11 @@ component output="false" { local.method = arguments.method; local.component = ListChangeDelims(arguments.path, ".", "/") & "." & ListChangeDelims(arguments.fileName, ".", "/"); local.argumentCollection = arguments; - if(local.method EQ 'init'){ + if (local.method EQ 'init') { local.rv = application.wheelsdi.getInstance(name = "#local.component#", initArguments = local.argumentCollection); - } - else{ + } else { local.instance = application.wheelsdi.getInstance(name = "#local.component#"); - local.rv = invoke(local.instance, local.method, local.argumentCollection); + local.rv = Invoke(local.instance, local.method, local.argumentCollection); } return local.rv; } @@ -997,16 +1008,16 @@ component output="false" { * @name The registered service name to resolve. */ public any function service(required string name) { - if (!isDefined("application.wheelsdi")) { - throw( - type="Wheels.DI.NotInitialized", - message="The DI container has not been initialized. Ensure your application has started properly." + if (!IsDefined("application.wheelsdi")) { + Throw( + type = "Wheels.DI.NotInitialized", + message = "The DI container has not been initialized. Ensure your application has started properly." ); } if (!application.wheelsdi.containsInstance(arguments.name)) { - throw( - type="Wheels.DI.ServiceNotFound", - message="No service registered with the name '#arguments.name#'. Check your config/services.cfm registrations." + Throw( + type = "Wheels.DI.ServiceNotFound", + message = "No service registered with the name '#arguments.name#'. Check your config/services.cfm registrations." ); } return application.wheelsdi.getInstance(arguments.name); @@ -1019,10 +1030,10 @@ component output="false" { * [category: Miscellaneous Functions] */ public any function injector() { - if (!isDefined("application.wheelsdi")) { - throw( - type="Wheels.DI.NotInitialized", - message="The DI container has not been initialized. Ensure your application has started properly." + if (!IsDefined("application.wheelsdi")) { + Throw( + type = "Wheels.DI.NotInitialized", + message = "The DI container has not been initialized. Ensure your application has started properly." ); } return application.wheelsdi; @@ -1055,11 +1066,7 @@ component output="false" { string adapter = "" ) { local.engine = $getChannelEngine(arguments.adapter); - return local.engine.publish( - channel = arguments.channel, - event = arguments.event, - data = arguments.data - ); + return local.engine.publish(channel = arguments.channel, event = arguments.event, data = arguments.data); } /** @@ -1203,12 +1210,7 @@ component output="false" { /** * Internal function. */ - public string function $prependUrl( - required string path, - string host = "", - string protocol = "", - numeric port = 0 - ) { + public string function $prependUrl(required string path, string host = "", string protocol = "", numeric port = 0) { local.rv = arguments.path; if (arguments.port != 0) { // use the port that was passed in by the developer @@ -1250,6 +1252,26 @@ component output="false" { // load wheels internal gui routes // TODO skip this if mode != development|testing? $include(template = "/wheels/public/routes.cfm"); + // Browser-test fixture routes — opt-in, only mounted in testing/development. + // See `vendor/wheels/public/browser-fixtures/routes.cfm` and issues #2135, #2138. + // The fixture controllers live at `vendor/wheels/public/browser-fixtures/controllers/` + // and render their own views via explicit `$include`, so only `controllerPath` + // needs to be extended (viewPath is single-string and left alone). + if ( + StructKeyExists(application[local.appKey], "loadBrowserTestFixtures") + && application[local.appKey].loadBrowserTestFixtures + && StructKeyExists(application[local.appKey], "environment") + && ListFindNoCase("testing,development", application[local.appKey].environment) + ) { + local.fixtureControllerPath = "/wheels/public/browser-fixtures/controllers"; + if (!ListFindNoCase(application[local.appKey].controllerPath, local.fixtureControllerPath)) { + application[local.appKey].controllerPath = ListAppend( + application[local.appKey].controllerPath, + local.fixtureControllerPath + ); + } + $include(template = "/wheels/public/browser-fixtures/routes.cfm"); + } // load developer routes next $include(template = "/config/routes.cfm"); // set lookup info for the named routes @@ -1974,7 +1996,10 @@ component output="false" { if (StructKeyExists(server, "lucee") || StructKeyExists(server, "boxlang")) { return GetPageContext().getResponse().getStatus(); } - return GetPageContext().getFusionContext().getResponse().getStatus(); + return GetPageContext() + .getFusionContext() + .getResponse() + .getStatus(); } /** @@ -1997,12 +2022,16 @@ component output="false" { if (StructKeyExists(server, "boxlang")) { local.header = local.response.getRequest().getHeader("Content-Type"); } else { - local.header = local.response.containsHeader("Content-Type") ? local.response.getHeader("Content-Type") : javacast("null", ""); + local.header = local.response.containsHeader("Content-Type") ? local.response.getHeader("Content-Type") : Javacast( + "null", + "" + ); } if (!IsNull(local.header)) { local.rv = local.header; } - } catch (any e) {} + } catch (any e) { + } return local.rv; } @@ -2138,13 +2167,23 @@ component output="false" { * Checks both application.wheels (post-init) and application.$wheels (during init). */ public any function $engineAdapter() { - if (StructKeyExists(application, "wheels") && IsStruct(application.wheels) && StructKeyExists(application.wheels, "engineAdapter")) { + if ( + StructKeyExists(application, "wheels") && IsStruct(application.wheels) && StructKeyExists( + application.wheels, + "engineAdapter" + ) + ) { return application.wheels.engineAdapter; } - if (StructKeyExists(application, "$wheels") && IsStruct(application.$wheels) && StructKeyExists(application.$wheels, "engineAdapter")) { + if ( + StructKeyExists(application, "$wheels") && IsStruct(application.$wheels) && StructKeyExists( + application.$wheels, + "engineAdapter" + ) + ) { return application.$wheels.engineAdapter; } - throw(type="Wheels.EngineAdapterNotInitialized", message="Engine adapter has not been initialized yet."); + Throw(type = "Wheels.EngineAdapterNotInitialized", message = "Engine adapter has not been initialized yet."); } /** @@ -2152,8 +2191,18 @@ component output="false" { * Used by functions that may be called before onApplicationStart completes. */ public boolean function $hasEngineAdapter() { - return (StructKeyExists(application, "wheels") && IsStruct(application.wheels) && StructKeyExists(application.wheels, "engineAdapter")) - || (StructKeyExists(application, "$wheels") && IsStruct(application.$wheels) && StructKeyExists(application.$wheels, "engineAdapter")); + return ( + StructKeyExists(application, "wheels") && IsStruct(application.wheels) && StructKeyExists( + application.wheels, + "engineAdapter" + ) + ) + || ( + StructKeyExists(application, "$wheels") && IsStruct(application.$wheels) && StructKeyExists( + application.$wheels, + "engineAdapter" + ) + ); } // ====================================================================== @@ -2332,7 +2381,10 @@ component output="false" { for (local.i = 1; local.i <= local.iEnd; local.i++) { local.arg = local.requiredKeysArray[local.i]; if (!StructKeyExists(arguments.args, local.arg)) { - Throw(type = "Wheels.IncorrectArguments", message = "The `#local.arg#` argument is required but not passed in."); + Throw( + type = "Wheels.IncorrectArguments", + message = "The `#local.arg#` argument is required but not passed in." + ); } } } @@ -2410,20 +2462,30 @@ component output="false" { local.s = Trim(val); // Match patterns loosely so they work for plain dates too - local.patternAMPM = '^\d{1,2}/\d{1,2}/\d{4}(\s+\d{1,2}:\d{2}(\s*(AM|PM))?)?$'; - local.patternISO = '^\d{4}-\d{2}-\d{2}([ T]\d{2}:\d{2}(:\d{2})?)?$'; + local.patternAMPM = '^\d{1,2}/\d{1,2}/\d{4}(\s+\d{1,2}:\d{2}(\s*(AM|PM))?)?$'; + local.patternISO = '^\d{4}-\d{2}-\d{2}([ T]\d{2}:\d{2}(:\d{2})?)?$'; local.patternSlash = '^\s*\d{1,2}/\d{1,2}/\d{4}\s*$'; // Day name or other verbose formats are ignored to avoid false positives - if (ReFindNoCase(local.patternAMPM, local.s) OR ReFindNoCase(local.patternISO, local.s) OR ReFindNoCase(local.patternSlash, local.s)) { + if ( + ReFindNoCase(local.patternAMPM, local.s) OR ReFindNoCase(local.patternISO, local.s) OR ReFindNoCase( + local.patternSlash, + local.s + ) + ) { // Promote to datetime so the datetime branch will run below detectedType = "datetime"; } } // Pre-process date strings with AM/PM that may be parsed differently per engine - if ($engineAdapter().isBoxLang() && IsSimpleValue(arguments.value) && ReFindNoCase("^\d{1,2}/\d{1,2}/\d{4} \d{1,2}:\d{2} (AM|PM)$", arguments.value)) { + if ( + $engineAdapter().isBoxLang() && IsSimpleValue(arguments.value) && ReFindNoCase( + "^\d{1,2}/\d{1,2}/\d{4} \d{1,2}:\d{2} (AM|PM)$", + arguments.value + ) + ) { // Manually parse DD/MM/YYYY format to avoid engine-specific interpretation local.parts = ListToArray(arguments.value, " "); local.datePart = local.parts[1]; @@ -2433,8 +2495,8 @@ component output="false" { local.dateComponents = ListToArray(local.datePart, "/"); local.timeComponents = ListToArray(local.timePart, ":"); - local.day = Val(local.dateComponents[1]); // First = day (DD/MM/YYYY) - local.month = Val(local.dateComponents[2]); // Second = month + local.day = Val(local.dateComponents[1]); // First = day (DD/MM/YYYY) + local.month = Val(local.dateComponents[2]); // Second = month local.year = Val(local.dateComponents[3]); local.hour = Val(local.timeComponents[1]); local.minute = Val(local.timeComponents[2]); @@ -2452,7 +2514,6 @@ component output="false" { switch (detectedType) { case "array": return ArrayToList(val); - case "struct": local.kList = ListSort(StructKeyList(val), "textnocase", "asc"); local.out = ""; @@ -2460,10 +2521,8 @@ component output="false" { local.out = ListAppend(local.out, local.k & "=" & val[local.k]); } return local.out; - case "binary": return ToString(val); - case "float": case "integer": if (!Len(val)) { @@ -2473,13 +2532,11 @@ component output="false" { return "1"; } return Val(val); - case "boolean": if (Len(val)) { return (val IS true) ? "true" : "false"; } return ""; - case "datetime": // If it's already a date object, canonicalize if (IsDate(val)) { @@ -2500,8 +2557,8 @@ component output="false" { // 1) ISO YYYY-MM-DD[ hh[:mm[:ss]]] if (ReFind("(?i)^(\\d{4})-(\\d{2})-(\\d{2})(?:[ T](\\d{1,2}):(\\d{2})(?::(\\d{2}))?)?$", local.s2)) { - local.parts = REReplace(local.s2, "^(\\d{4})-(\\d{2})-(\\d{2}).*$", "\\1-\\2-\\3", "all"); - local.timePart = REReplace(local.s2, ".*[ T](\\d{1,2}:\\d{2}(?::\\d{2})?).*$", "\\1", "all"); + local.parts = ReReplace(local.s2, "^(\\d{4})-(\\d{2})-(\\d{2}).*$", "\\1-\\2-\\3", "all"); + local.timePart = ReReplace(local.s2, ".*[ T](\\d{1,2}:\\d{2}(?::\\d{2})?).*$", "\\1", "all"); if (Len(local.timePart) AND local.timePart NEQ local.s2) { // has time local.dt = ParseDateTime(local.parts & " " & local.timePart); @@ -2510,7 +2567,11 @@ component output="false" { } } else { // date only - local.dt = CreateDate(Val(ListGetAt(local.parts,1,"-")), Val(ListGetAt(local.parts,2,"-")), Val(ListGetAt(local.parts,3,"-"))); + local.dt = CreateDate( + Val(ListGetAt(local.parts, 1, "-")), + Val(ListGetAt(local.parts, 2, "-")), + Val(ListGetAt(local.parts, 3, "-")) + ); return DateFormat(local.dt, "yyyy-mm-dd") & " 00:00:00"; } } @@ -2520,14 +2581,16 @@ component output="false" { local.comps = ListToArray(local.s2, "/"); local.d1 = Val(local.comps[1]); local.d2 = Val(local.comps[2]); - local.y = Val(local.comps[3]); + local.y = Val(local.comps[3]); // Heuristic: if day part > 12 then it's DD/MM/YYYY if (d1 > 12) { - local.day = d1; local.month = d2; + local.day = d1; + local.month = d2; } else if (d2 > 12) { // likely MM/DD/YYYY - local.month = d1; local.day = d2; + local.month = d1; + local.day = d2; } else { // ambiguous -> use adapter to determine date format preference local.ambiguousDate = $engineAdapter().parseAmbiguousSlashDate(d1, d2, y); @@ -2553,7 +2616,6 @@ component output="false" { } // If we reach here, parsing failed — return original string to allow comparison return val; - default: // Default: return raw value as string (no conversion) return val; @@ -2813,7 +2875,7 @@ component output="false" { application[local.appKey].mixins = application[local.appKey].PluginObj.getMixins(); application[local.appKey].pluginMiddleware = application[local.appKey].PluginObj.getPluginMiddleware(); // Invoke register(container) on ServiceProviderInterface plugins before activation - if (isDefined("application.wheelsdi") && ArrayLen(application[local.appKey].PluginObj.getServiceProviders())) { + if (IsDefined("application.wheelsdi") && ArrayLen(application[local.appKey].PluginObj.getServiceProviders())) { application[local.appKey].PluginObj.$invokeServiceProviderRegister(application.wheelsdi); // Boot after all register() calls complete — plugins can now resolve services application[local.appKey].PluginObj.$invokeServiceProviderBoot(application[local.appKey]); @@ -2860,7 +2922,7 @@ component output="false" { } // Invoke ServiceProvider register/boot if DI container exists - if (isDefined("application.wheelsdi") && ArrayLen(application[local.appKey].PackageLoaderObj.getServiceProviders())) { + if (IsDefined("application.wheelsdi") && ArrayLen(application[local.appKey].PackageLoaderObj.getServiceProviders())) { application[local.appKey].PackageLoaderObj.$invokeServiceProviderRegister(application.wheelsdi); application[local.appKey].PackageLoaderObj.$invokeServiceProviderBoot(application[local.appKey]); } @@ -2869,7 +2931,10 @@ component output="false" { /** * NB: url rewriting files need to be removed from here. */ - public string function $buildReleaseZip(string version = application.wheels.version, string directory = ExpandPath("/")) { + public string function $buildReleaseZip( + string version = application.wheels.version, + string directory = ExpandPath("/") + ) { local.name = "wheels-" & LCase(Replace(arguments.version, " ", "-", "all")); local.name = Replace(local.name, "alpha-", "alpha."); local.name = Replace(local.name, "beta-", "beta."); @@ -2913,13 +2978,13 @@ component output="false" { // Entries without "/" → treat as webroot (/public) paths for (local.i in local.include) { if (FileExists(ExpandPath(local.i))) { - if(left(local.i,1) neq "/" && left(local.i,2) neq ".."){ + if (Left(local.i, 1) neq "/" && Left(local.i, 2) neq "..") { $zip(file = local.path, source = ExpandPath(local.i), prefix = "/public"); } else { $zip(file = local.path, source = ExpandPath(local.i)); } } else if (DirectoryExists(ExpandPath(local.i))) { - if(left(local.i,1) neq "/" && left(local.i,2) neq ".."){ + if (Left(local.i, 1) neq "/" && Left(local.i, 2) neq "..") { $zip(file = local.path, source = ExpandPath(local.i), prefix = "/public/#local.i#"); } else { $zip(file = local.path, source = ExpandPath(local.i), prefix = local.i); @@ -2956,7 +3021,7 @@ component output="false" { */ public string function generateUUID() { // Use Java UUID generator for a 36-character format - return createObject("java", "java.util.UUID").randomUUID().toString(); + return CreateObject("java", "java.util.UUID").randomUUID().toString(); } /** @@ -3167,8 +3232,8 @@ component output="false" { local.insideFunction = false; local.bracketCount = 0; - for (local.i = 1; i <= len(arguments.list); i++) { - local.char = mid(arguments.list, i, 1); + for (local.i = 1; i <= Len(arguments.list); i++) { + local.char = Mid(arguments.list, i, 1); // Check if we are entering or exiting a function's parentheses if (local.char == "(") { @@ -3186,7 +3251,7 @@ component output="false" { // Split based on commas outside functions if (local.char == arguments.splitBy && !local.insideFunction) { - arrayAppend(local.rv, trim(local.temp)); + ArrayAppend(local.rv, Trim(local.temp)); local.temp = ""; } else { local.temp &= local.char; @@ -3194,8 +3259,8 @@ component output="false" { } // Append the final segment - if (len(trim(local.temp))) { - arrayAppend(local.rv, trim(local.temp)); + if (Len(Trim(local.temp))) { + ArrayAppend(local.rv, Trim(local.temp)); } return local.rv; @@ -3211,8 +3276,8 @@ component output="false" { */ public string function $normalizePath(required string path) { local.norm = arguments.path; - local.norm = reReplace(local.norm, "\[(.*?)\]", ".\1", "all"); - local.norm = reReplace(local.norm, "^\.", "", "one"); + local.norm = ReReplace(local.norm, "\[(.*?)\]", ".\1", "all"); + local.norm = ReReplace(local.norm, "^\.", "", "one"); return local.norm; } @@ -3360,7 +3425,7 @@ component output="false" { // Set Origin, Content-Type, X-Auth-Token, X-Requested-By, X-Requested-With Allow Headers $header(name = "Access-Control-Allow-Headers", value = arguments.allowHeaders); - // Either Look up Route specific allowed methods, or just use default + // Either Look up Route specific allowed methods, or just use default if (arguments.allowMethodsByRoute) { local.permittedMethods = []; @@ -3457,9 +3522,18 @@ component output="false" { // Check Model interface (requires at least one model to be loaded) try { local.modelMethods = [ - "findAll", "findOne", "findByKey", "count", "exists", - "save", "valid", "update", "delete", - "hasMany", "belongsTo", "hasOne", + "findAll", + "findOne", + "findByKey", + "count", + "exists", + "save", + "valid", + "update", + "delete", + "hasMany", + "belongsTo", + "hasOne", "validatesPresenceOf" ]; if (StructKeyExists(application.wheels, "models") && !StructIsEmpty(application.wheels.models)) { @@ -3478,11 +3552,18 @@ component output="false" { // Check Controller interface try { local.controllerMethods = [ - "renderView", "renderPartial", "renderText", "redirectTo", - "linkTo", "urlFor", "startFormTag", "endFormTag", - "filters", "verifies" + "renderView", + "renderPartial", + "renderText", + "redirectTo", + "linkTo", + "urlFor", + "startFormTag", + "endFormTag", + "filters", + "verifies" ]; - local.params = {controller: "wheels", action: "wheels"}; + local.params = {controller = "wheels", action = "wheels"}; local.testController = controller(name = "wheels", params = local.params); for (local.m in local.controllerMethods) { if (!StructKeyExists(local.testController, local.m)) { @@ -3505,4 +3586,5 @@ component output="false" { // User-defined global functions include "/app/global/functions.cfm"; + } diff --git a/vendor/wheels/events/init/views.cfm b/vendor/wheels/events/init/views.cfm index 55e1685b8e..8a63c18b32 100644 --- a/vendor/wheels/events/init/views.cfm +++ b/vendor/wheels/events/init/views.cfm @@ -1,43 +1,50 @@ - // Asset path settings. - // assetPaths can be struct with two keys, http and https, if no https struct key, http is used for secure and non-secure. - // Example: {http="asset0.domain1.com,asset2.domain1.com,asset3.domain1.com", https="secure.domain1.com"} - application.$wheels.assetQueryString = false; - application.$wheels.assetPaths = false; - if (application.$wheels.environment != "development") { - application.$wheels.assetQueryString = true; - } +// Asset path settings. +// assetPaths can be struct with two keys, http and https, if no https struct key, http is used for secure and non-secure. +// Example: {http="asset0.domain1.com,asset2.domain1.com,asset3.domain1.com", https="secure.domain1.com"} +application.$wheels.assetQueryString = false; +application.$wheels.assetPaths = false; +if (application.$wheels.environment != "development") { + application.$wheels.assetQueryString = true; +} - // Configurable paths. - application.$wheels.eventPath = "/app/events"; - application.$wheels.filePath = "files"; - application.$wheels.imagePath = "images"; - application.$wheels.javascriptPath = "javascripts"; - application.$wheels.modelPath = "/app/models"; - application.$wheels.pluginPath = "/plugins"; - application.$wheels.pluginComponentPath = "/plugins"; - application.$wheels.packagePath = "/vendor"; - application.$wheels.enablePackagesComponent = true; - application.$wheels.stylesheetPath = "stylesheets"; - application.$wheels.viewPath = "/app/views"; - application.$wheels.controllerPath = "/app/controllers"; +// Configurable paths. +application.$wheels.eventPath = "/app/events"; +application.$wheels.filePath = "files"; +application.$wheels.imagePath = "images"; +application.$wheels.javascriptPath = "javascripts"; +application.$wheels.modelPath = "/app/models"; +application.$wheels.pluginPath = "/plugins"; +application.$wheels.pluginComponentPath = "/plugins"; +application.$wheels.packagePath = "/vendor"; +application.$wheels.enablePackagesComponent = true; +application.$wheels.stylesheetPath = "stylesheets"; +application.$wheels.viewPath = "/app/views"; +application.$wheels.controllerPath = "/app/controllers"; - // Vite asset pipeline settings. - application.$wheels.viteDevServerUrl = "http://localhost:5173"; - application.$wheels.viteBuildPath = "build"; - application.$wheels.viteManifestFile = ".vite/manifest.json"; - application.$wheels.viteDevMode = (application.$wheels.environment == "development"); - application.$wheels.viteStrictManifest = true; +// Browser-test fixture routes (opt-in, only mounted in testing/development). +// When `true`, `$lockedLoadRoutes` includes `/wheels/public/browser-fixtures/routes.cfm` +// and appends the fixture controller/view directories to the resolver search path. +// Default `false` — apps must opt in explicitly via `set(loadBrowserTestFixtures=true)` +// in `config/settings.cfm` (or a testing-specific override). See issues #2135, #2138. +application.$wheels.loadBrowserTestFixtures = false; - // Test framework settings. - application.$wheels.validateTestPackageMetaData = true; - application.$wheels.restoreTestRunnerApplicationScope = true; +// Vite asset pipeline settings. +application.$wheels.viteDevServerUrl = "http://localhost:5173"; +application.$wheels.viteBuildPath = "build"; +application.$wheels.viteManifestFile = ".vite/manifest.json"; +application.$wheels.viteDevMode = (application.$wheels.environment == "development"); +application.$wheels.viteStrictManifest = true; - // Form helper settings. - // When true, object-bound form helpers (textField, emailField, select, etc.) also emit a - // `data-auto-id` attribute alongside the auto-derived `id`. The `id` uses the historical - // dash convention (e.g. `post-title`) and `data-auto-id` uses the underscore convention - // (e.g. `post_title`) favored by Rails/Laravel-style browser test selectors. Only emitted - // when the id is auto-derived from objectName + property; a user-supplied `id` suppresses it. - application.$wheels.formHelperDataAutoId = true; +// Test framework settings. +application.$wheels.validateTestPackageMetaData = true; +application.$wheels.restoreTestRunnerApplicationScope = true; + +// Form helper settings. +// When true, object-bound form helpers (textField, emailField, select, etc.) also emit a +// `data-auto-id` attribute alongside the auto-derived `id`. The `id` uses the historical +// dash convention (e.g. `post-title`) and `data-auto-id` uses the underscore convention +// (e.g. `post_title`) favored by Rails/Laravel-style browser test selectors. Only emitted +// when the id is auto-derived from objectName + property; a user-supplied `id` suppresses it. +application.$wheels.formHelperDataAutoId = true; diff --git a/vendor/wheels/public/browser-fixtures/controllers/BrowserTestHome.cfc b/vendor/wheels/public/browser-fixtures/controllers/BrowserTestHome.cfc new file mode 100644 index 0000000000..0fba5c8c67 --- /dev/null +++ b/vendor/wheels/public/browser-fixtures/controllers/BrowserTestHome.cfc @@ -0,0 +1,33 @@ +/** + * Browser-test fixture controller — framework-internal. + * + * Mounted via `$lockedLoadRoutes` when `loadBrowserTestFixtures=true` + * and environment is `testing` or `development`. See issues #2135, #2138. + * + * Views live beside this file at `vendor/wheels/public/browser-fixtures/views/`. + * They are rendered via explicit `cfinclude` rather than the normal Wheels + * view-path resolver because the framework's `viewPath` setting is a + * single string pinned to `/app/views`. + */ +component extends="Controller" { + + function config() { + filters(through = "$requireLogin", except = "index"); + } + + function index() { + $renderBrowserFixtureView(action = "index"); + } + + function dashboard() { + variables.user = {email = session.userEmail ?: ""}; + $renderBrowserFixtureView(action = "dashboard"); + } + + private function $requireLogin() { + if (!StructKeyExists(session, "userId")) { + redirectTo(route = "browserTestLogin"); + } + } + +} diff --git a/vendor/wheels/public/browser-fixtures/controllers/BrowserTestLogin.cfc b/vendor/wheels/public/browser-fixtures/controllers/BrowserTestLogin.cfc new file mode 100644 index 0000000000..a0630addbc --- /dev/null +++ b/vendor/wheels/public/browser-fixtures/controllers/BrowserTestLogin.cfc @@ -0,0 +1,23 @@ +/** + * Browser-test fixture controller — framework-internal. + * Env-gated `loginAs` endpoint for browser specs. Issues #2135, #2138. + */ +component extends="Controller" { + + function config() { + } + + function create() { + if (!ListFindNoCase("testing,development", application.wheels.environment)) { + Throw( + type = "Wheels.BrowserTestSecurityError", + message = "loginAs endpoint is only available in testing/development environments" + ); + } + + session.userId = 1; + session.userEmail = params.identifier; + $renderBrowserFixtureView(action = "create"); + } + +} diff --git a/vendor/wheels/public/browser-fixtures/controllers/BrowserTestSessions.cfc b/vendor/wheels/public/browser-fixtures/controllers/BrowserTestSessions.cfc new file mode 100644 index 0000000000..0ef9909e86 --- /dev/null +++ b/vendor/wheels/public/browser-fixtures/controllers/BrowserTestSessions.cfc @@ -0,0 +1,29 @@ +/** + * Browser-test fixture controller — framework-internal. + * See `Controller.cfc` in this directory for rendering helper docs. + * Issues #2135, #2138. + */ +component extends="Controller" { + + function new() { + variables.flashError = flash("error") ?: ""; + $renderBrowserFixtureView(action = "new"); + } + + function create() { + if (params.email == "alice@example.com" && params.password == "secret") { + session.userId = 1; + session.userEmail = params.email; + redirectTo(route = "browserTestDashboard"); + } else { + flashInsert(error = "Invalid credentials"); + redirectTo(route = "browserTestLogin"); + } + } + + function destroy() { + StructClear(session); + redirectTo(route = "browserTestLogin"); + } + +} diff --git a/vendor/wheels/public/browser-fixtures/controllers/Controller.cfc b/vendor/wheels/public/browser-fixtures/controllers/Controller.cfc new file mode 100644 index 0000000000..388a3b89c8 --- /dev/null +++ b/vendor/wheels/public/browser-fixtures/controllers/Controller.cfc @@ -0,0 +1,51 @@ +/** + * Base controller for the browser-test fixture controllers in this + * directory. Resolves when `$lockedLoadRoutes` appends + * `/wheels/public/browser-fixtures/controllers` to the controller + * search path, so `extends="Controller"` in BrowserTestHome / + * BrowserTestLogin / BrowserTestSessions finds this stub. + * + * Provides a small `$renderBrowserFixtureView()` helper that renders a + * fixture view + the shared fixture layout via explicit `cfinclude`s. + * The fixtures cannot use the normal Wheels view-path resolver because + * the framework's `viewPath` setting is a single string pinned to + * `/app/views` and these fixture views live under + * `/wheels/public/browser-fixtures/views/`. + * + * Mirrors `vendor/wheels/tests/_assets/controllers/Controller.cfc` in + * spirit (minimal stub that delegates to `wheels.Controller`). + */ +component extends="wheels.Controller" { + + /** + * Renders `/wheels/public/browser-fixtures/views//.cfm` + * wrapped in the fixture folder's shared `layout.cfm`, and short- + * circuits the normal Wheels view-rendering pipeline via `renderText`. + * + * Callers pass the view `action` (filename without extension). The + * controller's `params.controller` is used for the folder name, + * matching Wheels' own view-folder convention. The layout is a plain + * CFM file that references a local `contentForLayout` variable + * populated here before the layout include. + */ + private void function $renderBrowserFixtureView(required string action) { + var folder = LCase(variables.params.controller); + var viewsBase = "/wheels/public/browser-fixtures/views/" & folder; + + savecontent variable="local.contentForLayout" { + include template="#viewsBase#/#arguments.action#.cfm"; + } + + // The fixture layouts reference `contentForLayout` as a local variable + // (not the framework helper) to avoid depending on the normal Wheels + // layout pipeline. + var contentForLayout = local.contentForLayout; + + savecontent variable="local.page" { + include template="#viewsBase#/layout.cfm"; + } + + renderText(local.page); + } + +} diff --git a/vendor/wheels/public/browser-fixtures/routes.cfm b/vendor/wheels/public/browser-fixtures/routes.cfm new file mode 100644 index 0000000000..0d25f7adb1 --- /dev/null +++ b/vendor/wheels/public/browser-fixtures/routes.cfm @@ -0,0 +1,28 @@ + +/** + * Browser-test fixture routes + * + * Mounted by `vendor/wheels/Global.cfc::$lockedLoadRoutes` when: + * - `application.wheels.environment` is `testing` or `development`, AND + * - `application.wheels.loadBrowserTestFixtures` is `true` (opt-in) + * + * Provides the `/_browser/*` routes used by the browser-testing DSL + * (`wheels.wheelstest.BrowserTest`) for loginAs / logout / dashboard + * happy-path specs. The fixture controllers + views live alongside this + * file at `vendor/wheels/public/browser-fixtures/{controllers,views}/`; + * the framework's controller/view resolver appends those directories to + * the search path when the fixtures are active. + * + * Must come before `.wildcard()` in the app's own route table. + */ +mapper() + .scope(path = "/_browser") + .get(name = "browserTestHome", pattern = "/home", to = "BrowserTestHome##index") + .get(name = "browserTestLogin", pattern = "/login", to = "BrowserTestSessions##new") + .post(name = "browserTestAuthenticate", pattern = "/login", to = "BrowserTestSessions##create") + .get(name = "browserTestDashboard", pattern = "/dashboard", to = "BrowserTestHome##dashboard") + .post(name = "browserTestLogout", pattern = "/logout", to = "BrowserTestSessions##destroy") + .get(name = "browserTestLoginAs", pattern = "/login-as", to = "BrowserTestLogin##create") + .end() + .end(); + diff --git a/vendor/wheels/public/browser-fixtures/views/browsertesthome/dashboard.cfm b/vendor/wheels/public/browser-fixtures/views/browsertesthome/dashboard.cfm new file mode 100644 index 0000000000..07c3ad4635 --- /dev/null +++ b/vendor/wheels/public/browser-fixtures/views/browsertesthome/dashboard.cfm @@ -0,0 +1,10 @@ + + +

Dashboard

+

+ Welcome, #EncodeForHTML(user.email)# +

+
+ +
+
diff --git a/vendor/wheels/public/browser-fixtures/views/browsertesthome/index.cfm b/vendor/wheels/public/browser-fixtures/views/browsertesthome/index.cfm new file mode 100644 index 0000000000..05af95d9ec --- /dev/null +++ b/vendor/wheels/public/browser-fixtures/views/browsertesthome/index.cfm @@ -0,0 +1,5 @@ + +

Home

+

Welcome to the browser test fixture app.

+ Log in +
diff --git a/vendor/wheels/public/browser-fixtures/views/browsertesthome/layout.cfm b/vendor/wheels/public/browser-fixtures/views/browsertesthome/layout.cfm new file mode 100644 index 0000000000..2925096bc6 --- /dev/null +++ b/vendor/wheels/public/browser-fixtures/views/browsertesthome/layout.cfm @@ -0,0 +1,12 @@ + + + + + + Browser Test Fixture + + + #contentForLayout# + + + diff --git a/vendor/wheels/public/browser-fixtures/views/browsertestlogin/create.cfm b/vendor/wheels/public/browser-fixtures/views/browsertestlogin/create.cfm new file mode 100644 index 0000000000..7c520de346 --- /dev/null +++ b/vendor/wheels/public/browser-fixtures/views/browsertestlogin/create.cfm @@ -0,0 +1,4 @@ + + +

Logged in as #EncodeForHTML(params.identifier ?: "unknown")#

+
diff --git a/vendor/wheels/public/browser-fixtures/views/browsertestlogin/layout.cfm b/vendor/wheels/public/browser-fixtures/views/browsertestlogin/layout.cfm new file mode 100644 index 0000000000..2925096bc6 --- /dev/null +++ b/vendor/wheels/public/browser-fixtures/views/browsertestlogin/layout.cfm @@ -0,0 +1,12 @@ + + + + + + Browser Test Fixture + + + #contentForLayout# + + + diff --git a/vendor/wheels/public/browser-fixtures/views/browsertestsessions/layout.cfm b/vendor/wheels/public/browser-fixtures/views/browsertestsessions/layout.cfm new file mode 100644 index 0000000000..2925096bc6 --- /dev/null +++ b/vendor/wheels/public/browser-fixtures/views/browsertestsessions/layout.cfm @@ -0,0 +1,12 @@ + + + + + + Browser Test Fixture + + + #contentForLayout# + + + diff --git a/vendor/wheels/public/browser-fixtures/views/browsertestsessions/new.cfm b/vendor/wheels/public/browser-fixtures/views/browsertestsessions/new.cfm new file mode 100644 index 0000000000..992d16ff15 --- /dev/null +++ b/vendor/wheels/public/browser-fixtures/views/browsertestsessions/new.cfm @@ -0,0 +1,14 @@ + + +

Log in

+ +
#flashError#
+
+
+ + + + + +
+
diff --git a/web/sites/guides/src/content/docs/v4-0-0-snapshot/upgrading/3x-to-4x.mdx b/web/sites/guides/src/content/docs/v4-0-0-snapshot/upgrading/3x-to-4x.mdx index 254b9bb92e..b6fe55925b 100644 --- a/web/sites/guides/src/content/docs/v4-0-0-snapshot/upgrading/3x-to-4x.mdx +++ b/web/sites/guides/src/content/docs/v4-0-0-snapshot/upgrading/3x-to-4x.mdx @@ -245,6 +245,18 @@ See [Packages](/v4-0-0-snapshot/digging-deeper/packages/) for manifest fields, p **CHANGELOG:** `Deprecated: In-dev-server HTTP MCP endpoint at /wheels/mcp — superseded by the LuCLI stdio MCP server (wheels mcp wheels).` Migrate with `wheels mcp setup --force`. +### Browser-test fixture routes (`/_browser/*`) moved into the framework + +**CHANGELOG:** `Fixed: Framework-internal browser-test fixture controllers, views, and /_browser/* routes no longer leak into application-level files.` (#2135, #2138) + +The `/_browser/home`, `/_browser/login`, `/_browser/dashboard`, `/_browser/login-as`, and related fixture routes used by Wheels' browser-testing DSL (`wheels.wheelstest.BrowserTest`) used to live in application-level files — `config/routes.cfm` plus `app/controllers/BrowserTest*.cfc` and `app/views/browsertest*/`. As of 4.0 they ship as framework internals under `vendor/wheels/public/browser-fixtures/` and are auto-mounted by the framework's own route loader. + +**What you need to do depends on whether your app relied on these routes:** + +- **You never used `/_browser/*` yourself** — no action. These routes were only ever meaningful when you ran Wheels' own browser specs against the dev server, and Wheels-generated app scaffolds (`wheels new`) never included them. Your `config/routes.cfm` stays as-is. +- **You ran Wheels browser specs against a running dev server** — opt in by adding `set(loadBrowserTestFixtures=true);` to `config/settings.cfm` (or to your `testing`/`development` env override). The framework then mounts the `/_browser/*` routes automatically. The setting defaults to `false` and production/maintenance/staging environments always skip the mount. +- **You defined your own `/_browser/*` routes in `config/routes.cfm`** (rare — Wheels 4.0 snapshot only) — either keep them (your routes win), or delete them and opt in via `loadBrowserTestFixtures=true` to use the framework copies. The framework registers before `config/routes.cfm` is loaded, so any same-named app route overrides the fixture. + ## Removed in 4.0 **CHANGELOG** → Removed: