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
6 changes: 6 additions & 0 deletions changelog.d/debug-bar-reload-subpath.fixed.md
Original file line number Diff line number Diff line change
@@ -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))
74 changes: 74 additions & 0 deletions vendor/wheels/Global.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
12 changes: 11 additions & 1 deletion vendor/wheels/events/onerror/cfmlerror.cfm
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,19 @@
<!--- Request Info Grid --->
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px;margin:1.5em 0;">
<cfif IsDefined("application.wheels.rewriteFile")>
<!---
Base composed from webPath (subpath-aware, issue #3344) so subfolder
installs display /myapp/... instead of /myapp/public/... — same idiom
as urlFor() and $buildDebugReloadUrl().
--->
<cfif IsDefined("application.wheels.webPath") AND Len(application.wheels.webPath)>
<cfset local.errorUrlBase = Replace(application.wheels.webPath & ListLast(cgi.script_name, "/"), "/#application.wheels.rewriteFile#", "")>
<cfelse>
<cfset local.errorUrlBase = Replace(cgi.script_name, "/#application.wheels.rewriteFile#", "")>
</cfif>
<div style="background:##181825;border:1px solid ##45475a;border-radius:6px;padding:12px 16px;">
<div style="font-size:10px;font-weight:700;color:##6c7086;text-transform:uppercase;letter-spacing:.5px;margin-bottom:4px;">URL</div>
<div style="font-family:monospace;font-size:12px;color:##cdd6f4;word-break:break-all;">http<cfif cgi.http_x_forwarded_proto EQ "https" OR cgi.server_port_secure EQ "true">s</cfif>://#EncodeForHTML(cgi.server_name)##Replace(cgi.script_name, "/#application.wheels.rewriteFile#", "")#<cfif IsDefined("request.cgi.path_info")>#EncodeForHTML(request.cgi.path_info)#<cfelse>#EncodeForHTML(cgi.path_info)#</cfif><cfif cgi.query_string IS NOT "">?#EncodeForHTML(cgi.query_string)#</cfif></div>
<div style="font-family:monospace;font-size:12px;color:##cdd6f4;word-break:break-all;">http<cfif cgi.http_x_forwarded_proto EQ "https" OR cgi.server_port_secure EQ "true">s</cfif>://#EncodeForHTML(cgi.server_name)##EncodeForHTML(local.errorUrlBase)#<cfif IsDefined("request.cgi.path_info")>#EncodeForHTML(request.cgi.path_info)#<cfelse>#EncodeForHTML(cgi.path_info)#</cfif><cfif cgi.query_string IS NOT "">?#EncodeForHTML(cgi.query_string)#</cfif></div>
</div>
</cfif>
<cfif Len(cgi.http_referer)>
Expand Down
36 changes: 12 additions & 24 deletions vendor/wheels/events/onrequestend/debug.cfm
Original file line number Diff line number Diff line change
Expand Up @@ -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))>
<cfexit>
</cfif>
<cfset local.baseReloadURL = cgi.script_name>
<!---
Base reload URL composed from webPath (subpath-aware, issue #3344) via
$buildDebugReloadUrl() in Global.cfc. Engines report path_info differently,
so prefer the normalized request.cgi copy when available.
--->
<cfif IsDefined("request.cgi.path_info")>
<cfif request.cgi.path_info IS NOT cgi.script_name>
<cfset local.baseReloadURL &= request.cgi.path_info>
</cfif>
<cfelse>
<cfif cgi.path_info IS NOT cgi.script_name>
<cfset local.baseReloadURL &= cgi.path_info>
</cfif>
</cfif>
<cfif Len(cgi.query_string)>
<cfset local.baseReloadURL &= "?" & cgi.query_string>
</cfif>
<cfset local.baseReloadURL = ReplaceNoCase(local.baseReloadURL, "/" & application.wheels.rewriteFile, "")>
<cfloop list="development,testing,maintenance,production,true" index="local.i">
<cfset local.baseReloadURL = ReplaceNoCase(
ReplaceNoCase(local.baseReloadURL, "?reload=" & local.i, ""),
"&reload=" & local.i,
""
)>
</cfloop>
<cfif local.baseReloadURL Contains "?">
<cfset local.baseReloadURL &= "&">
<cfset local.debugPathInfo = request.cgi.path_info>
<cfelse>
<cfset local.baseReloadURL &= "?">
<cfset local.debugPathInfo = cgi.path_info>
</cfif>
<cfset local.baseReloadURL &= "reload=">
<cfset local.baseReloadURL = $buildDebugReloadUrl(
scriptName = cgi.script_name,
pathInfo = local.debugPathInfo,
queryString = cgi.query_string
)>
<cfset local.gitbranch = DirectoryExists(GetDirectoryFromPath(GetBaseTemplatePath()) & ".git") ? FileRead(
GetDirectoryFromPath(GetBaseTemplatePath()) & ".git/HEAD"
) : "">
Expand Down
179 changes: 179 additions & 0 deletions vendor/wheels/tests/specs/global/BuildDebugReloadUrlSpec.cfc
Original file line number Diff line number Diff line change
@@ -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=<environment> 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=")
})

})
}
}
Loading