diff --git a/changelog.d/debug-bar-reload-subpath.fixed.md b/changelog.d/debug-bar-reload-subpath.fixed.md new file mode 100644 index 0000000000..bb6002d433 --- /dev/null +++ b/changelog.d/debug-bar-reload-subpath.fixed.md @@ -0,0 +1,6 @@ +- Debug bar reload link (and the CFML error page's displayed URL) now honors the `subpath` + setting: the base URL is composed from the resolved `webPath` plus the front-controller + filename — the same idiom as `urlFor()` — instead of raw `cgi.script_name`, so subfolder + deployments emit `/myapp/posts?reload=` instead of the unroutable + `/myapp/public/index.cfm/posts?reload=`. Root installs render byte-identical to before. + Extracted into the unit-tested `$buildDebugReloadUrl()` helper in `Global.cfc` ([#3344](https://github.com/wheels-dev/wheels/issues/3344)) diff --git a/vendor/wheels/Global.cfc b/vendor/wheels/Global.cfc index 118a096c90..6ed06af54f 100644 --- a/vendor/wheels/Global.cfc +++ b/vendor/wheels/Global.cfc @@ -2699,6 +2699,80 @@ return local.$wheels; return local.base & local.relative; } + /** + * Internal function. Builds the debug bar's base reload URL (issue #3344). + * The base is composed from the resolved `webPath` plus the front-controller + * filename — the same idiom `urlFor()` uses — instead of raw + * `cgi.script_name`, so subfolder (subpath) installs emit links like + * `/myapp/posts?reload=` rather than `/myapp/public/index.cfm/posts?reload=` + * (which the user's rewrite rules don't route). The caller selects which + * path_info to pass (`request.cgi.path_info` when available, `cgi.path_info` + * otherwise — engines report it differently). `webPath` and `rewriteFile` + * default from application scope; tests pass them explicitly, and early + * boot/error paths where they're missing fall back to the raw script name + * (the pre-#3344 behavior). Pure string logic so it can be unit-tested in + * isolation. + */ + public string function $buildDebugReloadUrl( + required string scriptName, + string pathInfo = "", + string queryString = "", + string webPath, + string rewriteFile + ) { + // Resolve webPath/rewriteFile from application scope unless overridden. + // No runtime default-arg expressions (some engines evaluate those + // eagerly) — same pattern as $resolveSubpathInclude. + if (StructKeyExists(arguments, "webPath")) { + local.resolvedWebPath = arguments.webPath; + } else if (IsDefined("application.wheels.webPath")) { + local.resolvedWebPath = application.wheels.webPath; + } else { + local.resolvedWebPath = ""; + } + if (StructKeyExists(arguments, "rewriteFile")) { + local.resolvedRewriteFile = arguments.rewriteFile; + } else if (IsDefined("application.wheels.rewriteFile")) { + local.resolvedRewriteFile = application.wheels.rewriteFile; + } else { + local.resolvedRewriteFile = ""; + } + + // Base: webPath + front-controller filename (matches urlFor()); fall + // back to the raw script name when webPath isn't resolved yet. + if (Len(local.resolvedWebPath)) { + local.rv = local.resolvedWebPath & ListLast(arguments.scriptName, "/"); + } else { + local.rv = arguments.scriptName; + } + if (arguments.pathInfo != arguments.scriptName) { + local.rv &= arguments.pathInfo; + } + if (Len(arguments.queryString)) { + local.rv &= "?" & arguments.queryString; + } + if (Len(local.resolvedRewriteFile)) { + local.rv = ReplaceNoCase(local.rv, "/" & local.resolvedRewriteFile, ""); + } + local.reloadTokens = "development,testing,maintenance,production,true"; + local.iEnd = ListLen(local.reloadTokens); + for (local.i = 1; local.i <= local.iEnd; local.i++) { + local.token = ListGetAt(local.reloadTokens, local.i); + local.rv = ReplaceNoCase( + ReplaceNoCase(local.rv, "?reload=" & local.token, ""), + "&reload=" & local.token, + "" + ); + } + if (Find("?", local.rv)) { + local.rv &= "&"; + } else { + local.rv &= "?"; + } + local.rv &= "reload="; + return local.rv; + } + /** * Internal function. */ diff --git a/vendor/wheels/events/onerror/cfmlerror.cfm b/vendor/wheels/events/onerror/cfmlerror.cfm index d6509569ab..961588a704 100644 --- a/vendor/wheels/events/onerror/cfmlerror.cfm +++ b/vendor/wheels/events/onerror/cfmlerror.cfm @@ -65,9 +65,19 @@
+ + + + + +
URL
-
https://#EncodeForHTML(cgi.server_name)##Replace(cgi.script_name, "/#application.wheels.rewriteFile#", "")##EncodeForHTML(request.cgi.path_info)##EncodeForHTML(cgi.path_info)#?#EncodeForHTML(cgi.query_string)#
+
https://#EncodeForHTML(cgi.server_name)##EncodeForHTML(local.errorUrlBase)##EncodeForHTML(request.cgi.path_info)##EncodeForHTML(cgi.path_info)#?#EncodeForHTML(cgi.query_string)#
diff --git a/vendor/wheels/events/onrequestend/debug.cfm b/vendor/wheels/events/onrequestend/debug.cfm index ad7b2c944f..a72774cb84 100644 --- a/vendor/wheels/events/onrequestend/debug.cfm +++ b/vendor/wheels/events/onrequestend/debug.cfm @@ -7,33 +7,21 @@ OR (StructKeyExists(local.reqHeaders, "X-Fetch") AND local.reqHeaders["X-Fetch"] OR (StructKeyExists(url, "format") AND ListFindNoCase("json,xml,csv,pdf", url.format))> - + - - - - - - - - - - - - - - - - - + - + - + diff --git a/vendor/wheels/tests/specs/global/BuildDebugReloadUrlSpec.cfc b/vendor/wheels/tests/specs/global/BuildDebugReloadUrlSpec.cfc new file mode 100644 index 0000000000..9f2a60d3e7 --- /dev/null +++ b/vendor/wheels/tests/specs/global/BuildDebugReloadUrlSpec.cfc @@ -0,0 +1,179 @@ +component extends="wheels.WheelsTest" { + + function run() { + + g = application.wo + + describe("Tests that $buildDebugReloadUrl", () => { + + // --------------------------------------------------------------- + // Root installs (webPath = "/") — these expectations are pinned + // byte-for-byte to the output of the previous inline composition in + // vendor/wheels/events/onrequestend/debug.cfm (raw cgi.script_name + // + path_info + query string, then the rewriteFile strip and the + // reload-param scrub). They must never change. + // --------------------------------------------------------------- + + it("builds the reload URL for a root install with URL rewriting on", () => { + rv = g.$buildDebugReloadUrl( + scriptName = "/index.cfm", + pathInfo = "/posts", + queryString = "", + webPath = "/", + rewriteFile = "index.cfm" + ) + + expect(rv).toBe("/posts?reload=") + }) + + it("preserves the query string on a root install", () => { + rv = g.$buildDebugReloadUrl( + scriptName = "/index.cfm", + pathInfo = "/posts", + queryString = "page=2&sort=title", + webPath = "/", + rewriteFile = "index.cfm" + ) + + expect(rv).toBe("/posts?page=2&sort=title&reload=") + }) + + it("scrubs a leading ?reload= parameter from the query string", () => { + rv = g.$buildDebugReloadUrl( + scriptName = "/index.cfm", + pathInfo = "/posts", + queryString = "reload=true", + webPath = "/", + rewriteFile = "index.cfm" + ) + + expect(rv).toBe("/posts?reload=") + }) + + it("scrubs an &reload= parameter while keeping the rest of the query string", () => { + rv = g.$buildDebugReloadUrl( + scriptName = "/index.cfm", + pathInfo = "/posts", + queryString = "page=2&reload=development", + webPath = "/", + rewriteFile = "index.cfm" + ) + + expect(rv).toBe("/posts?page=2&reload=") + }) + + it("keeps rewriting-off root installs byte-identical to the previous inline composition", () => { + // With URL rewriting off, path_info equals script_name (Lucee) so + // nothing is appended; the rewriteFile strip leaves the bare query. + rv = g.$buildDebugReloadUrl( + scriptName = "/index.cfm", + pathInfo = "/index.cfm", + queryString = "controller=posts&action=index", + webPath = "/", + rewriteFile = "index.cfm" + ) + + expect(rv).toBe("?controller=posts&action=index&reload=") + }) + + it("handles an empty path_info (Adobe engines with rewriting off)", () => { + rv = g.$buildDebugReloadUrl( + scriptName = "/index.cfm", + pathInfo = "", + queryString = "controller=posts&action=index", + webPath = "/", + rewriteFile = "index.cfm" + ) + + expect(rv).toBe("?controller=posts&action=index&reload=") + }) + + // --------------------------------------------------------------- + // Subfolder (subpath) installs — issue #3344. The base must come + // from webPath, not raw cgi.script_name, so the emitted link never + // contains the on-disk /public/ segment or the front controller. + // --------------------------------------------------------------- + + it("honors webPath on a subfolder install (no /public/, no index.cfm)", () => { + rv = g.$buildDebugReloadUrl( + scriptName = "/wheelsproject1/public/index.cfm", + pathInfo = "/posts", + queryString = "", + webPath = "/wheelsproject1/", + rewriteFile = "index.cfm" + ) + + expect(rv).toBe("/wheelsproject1/posts?reload=") + expect(rv).notToInclude("/public/") + expect(rv).notToInclude("index.cfm") + }) + + it("honors a nested subpath webPath", () => { + rv = g.$buildDebugReloadUrl( + scriptName = "/team/site/public/index.cfm", + pathInfo = "/posts/1", + queryString = "page=2", + webPath = "/team/site/", + rewriteFile = "index.cfm" + ) + + expect(rv).toBe("/team/site/posts/1?page=2&reload=") + }) + + it("scrubs reload params on a subfolder install", () => { + rv = g.$buildDebugReloadUrl( + scriptName = "/wheelsproject1/public/index.cfm", + pathInfo = "/posts", + queryString = "reload=true", + webPath = "/wheelsproject1/", + rewriteFile = "index.cfm" + ) + + expect(rv).toBe("/wheelsproject1/posts?reload=") + }) + + // --------------------------------------------------------------- + // Defensive fallbacks (early boot / error paths). + // --------------------------------------------------------------- + + it("falls back to the raw script name when webPath is empty", () => { + rv = g.$buildDebugReloadUrl( + scriptName = "/wheelsproject1/public/index.cfm", + pathInfo = "/posts", + queryString = "", + webPath = "", + rewriteFile = "index.cfm" + ) + + // Exactly what the previous inline composition produced. + expect(rv).toBe("/wheelsproject1/public/posts?reload=") + }) + + it("skips the rewriteFile strip when rewriteFile is empty", () => { + rv = g.$buildDebugReloadUrl( + scriptName = "/index.cfm", + pathInfo = "/posts", + queryString = "", + webPath = "/", + rewriteFile = "" + ) + + expect(rv).toBe("/index.cfm/posts?reload=") + }) + + it("defaults webPath and rewriteFile from application scope when omitted", () => { + // The running test app is a root install: webPath "/" and + // rewriteFile "index.cfm", so this must match the explicit + // root-install case above. + rv = g.$buildDebugReloadUrl( + scriptName = "/index.cfm", + pathInfo = "/posts", + queryString = "" + ) + + expect(rv).toBe("/posts?reload=") + }) + + }) + } +}