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
162 changes: 96 additions & 66 deletions cli/lucli/templates/app/public/Application.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -225,12 +225,7 @@ component output="false" {

public boolean function onRequestStart( string targetPage ) {

if(structKeyExists(url, "format") && listFindNoCase("junit,json,txt", url.format))
{
application.contentOnly = true;
}else{
application.contentOnly = false;
}
this.$setContentOnlyForFormat();

local.lockName = "reloadLock" & this.name;

Expand All @@ -245,6 +240,39 @@ component output="false" {
// Need to setup the wheels struct up here since it's used to store debugging info below if this is a reload request.
application.wo.$initializeRequestScope();

this.$applyDebugIpAccessOverrides();

local.environmentSwitchAlreadyApplied = this.$isEnvironmentSwitchAlreadyApplied();

local.reloadAuthorized = this.$authorizeReload(local.environmentSwitchAlreadyApplied);
if (local.reloadAuthorized) {
this.$restartAppRequest(local.lockName);
return false;
}

// Run the rest of the request start code.
arguments.componentReference = "wheels.events.EventMethods";
application.wo.$simpleLock(
name = local.lockName,
execute = "$runOnRequestStart",
executeArgs = arguments,
type = "readOnly",
timeout = 180
);

return true;
}

public void function $setContentOnlyForFormat() {
if(structKeyExists(url, "format") && listFindNoCase("junit,json,txt", url.format))
{
application.contentOnly = true;
}else{
application.contentOnly = false;
}
}

public void function $applyDebugIpAccessOverrides() {
// IP-based access to public Component/debug GUI (only if allowed in settings)
if (!structKeyExists(application.wheels, "debugIPAccess")) {
application.wheels.debugIPAccess.originalEnablePublicComponent = application.wheels.enablePublicComponent;
Expand Down Expand Up @@ -286,7 +314,9 @@ component output="false" {
application.wheels.showErrorInformation = application.wheels.debugIPAccess.originalShowErrorInformation;
}
}
}

public boolean function $isEnvironmentSwitchAlreadyApplied() {
// Loop-break for URL environment switches (issue #3030): $buildRedirectUrl()
// keeps ?reload=<environment>&password=... on the post-restart redirect so the
// framework's switch code (vendor/wheels/events/onapplicationstart.cfc) can see
Expand All @@ -296,12 +326,55 @@ component output="false" {
// already active, skip the restart and serve the request normally.
// Trade-off: ?reload=<current-environment> is a no-op — use ?reload=true for a
// same-environment restart.
local.environmentSwitchAlreadyApplied = StructKeyExists(url, "reload")
return StructKeyExists(url, "reload")
&& !IsBoolean(url.reload)
&& StructKeyExists(application, "wheels")
&& StructKeyExists(application.wheels, "environment")
&& application.wheels.environment == url.reload;
}

public boolean function $reloadRateLimited(required string clientIp) {
// Same per-IP store and window as wheels/events/onapplicationstart.cfc, so
// warm-path and cold-start attempts count against one shared bucket.
if (!StructKeyExists(application, "$reloadRateLimit")) {
application.$reloadRateLimit = {};
}
local.reloadRateLimited = false;
if (StructKeyExists(application.$reloadRateLimit, arguments.clientIp)) {
local.reloadRateLimitEntry = application.$reloadRateLimit[arguments.clientIp];
if (local.reloadRateLimitEntry.count >= 5 && DateDiff("n", local.reloadRateLimitEntry.firstAttempt, Now()) < 5) {
local.reloadRateLimited = true;
}
if (DateDiff("n", local.reloadRateLimitEntry.firstAttempt, Now()) >= 5) {
StructDelete(application.$reloadRateLimit, arguments.clientIp);
}
}
return local.reloadRateLimited;
}

public void function $recordReloadRefusalReason(required boolean reloadAuthorized) {
// Record WHY a requested reload did not fire so the framework's debug
// bar can render a development-only notice instead of a silent no-op
// (issue #3311). Recording is environment-agnostic — a request-scope
// flag, no output; the message text and the development-environment
// gate live framework-side in vendor/wheels/events/onrequestend/debug.cfm
// so wording can improve without template drift. Wrong-password and
// rate-limited attempts deliberately collapse into one generic reason
// so the notice adds no oracle on top of $secureCompare().
if (!arguments.reloadAuthorized && StructKeyExists(request, "wheels")) {
local.reloadPasswordConfigured = StructKeyExists(application.wheels, "reloadPassword")
&& Len(application.wheels.reloadPassword);
if (!local.reloadPasswordConfigured) {
request.wheels.reloadRefusedReason = "emptyPassword";
} else if (!StructKeyExists(url, "password")) {
request.wheels.reloadRefusedReason = "missingPasswordParam";
} else {
request.wheels.reloadRefusedReason = "refused";
}
}
}

public boolean function $authorizeReload(required boolean environmentSwitchAlreadyApplied) {
// Reload application properly using applicationStop() if requested.
// SECURITY (issue #3062): the gate FAILS CLOSED. A URL-based reload requires a
// non-empty configured reloadPassword AND a matching password parameter — an
Expand All @@ -311,25 +384,11 @@ component output="false" {
// attempts are logged to wheels_security.log with the trusted client IP and
// feed the same per-IP rate limit as the cold-start path (5 failed attempts
// within 5 minutes locks the source out).
local.reloadRequested = StructKeyExists(url, "reload") && !local.environmentSwitchAlreadyApplied;
local.reloadRequested = StructKeyExists(url, "reload") && !arguments.environmentSwitchAlreadyApplied;
local.reloadAuthorized = false;
if (local.reloadRequested && StructKeyExists(application, "wheels") && StructKeyExists(application, "wo")) {
// Same per-IP store and window as wheels/events/onapplicationstart.cfc, so
// warm-path and cold-start attempts count against one shared bucket.
local.reloadClientIp = application.wo.$trustedClientIp();
if (!StructKeyExists(application, "$reloadRateLimit")) {
application.$reloadRateLimit = {};
}
local.reloadRateLimited = false;
if (StructKeyExists(application.$reloadRateLimit, local.reloadClientIp)) {
local.reloadRateLimitEntry = application.$reloadRateLimit[local.reloadClientIp];
if (local.reloadRateLimitEntry.count >= 5 && DateDiff("n", local.reloadRateLimitEntry.firstAttempt, Now()) < 5) {
local.reloadRateLimited = true;
}
if (DateDiff("n", local.reloadRateLimitEntry.firstAttempt, Now()) >= 5) {
StructDelete(application.$reloadRateLimit, local.reloadClientIp);
}
}
local.reloadRateLimited = this.$reloadRateLimited(local.reloadClientIp);
if (
!local.reloadRateLimited
&& StructKeyExists(application.wheels, "reloadPassword")
Expand Down Expand Up @@ -359,51 +418,22 @@ component output="false" {
// Fail silently if logging fails
}
}
// Record WHY a requested reload did not fire so the framework's debug
// bar can render a development-only notice instead of a silent no-op
// (issue #3311). Recording is environment-agnostic — a request-scope
// flag, no output; the message text and the development-environment
// gate live framework-side in vendor/wheels/events/onrequestend/debug.cfm
// so wording can improve without template drift. Wrong-password and
// rate-limited attempts deliberately collapse into one generic reason
// so the notice adds no oracle on top of $secureCompare().
if (!local.reloadAuthorized && StructKeyExists(request, "wheels")) {
local.reloadPasswordConfigured = StructKeyExists(application.wheels, "reloadPassword")
&& Len(application.wheels.reloadPassword);
if (!local.reloadPasswordConfigured) {
request.wheels.reloadRefusedReason = "emptyPassword";
} else if (!StructKeyExists(url, "password")) {
request.wheels.reloadRefusedReason = "missingPasswordParam";
} else {
request.wheels.reloadRefusedReason = "refused";
}
}
}
if (local.reloadAuthorized) {
application.wo.$debugPoint("total,reload");
if (StructKeyExists(url, "lock") && !url.lock) {
this.$handleRestartAppRequest();
} else {
// Case-exact "Application" — see the matching comment in onSessionStart().
// A lowercase reference turns every authorized reload into an HTTP 500 on
// Adobe CF + case-sensitive filesystems (issue #3053 follow-up).
local.executeArgs = {"componentReference" = "Application"};
application.wo.$simpleLock(name = local.lockName, execute = "$handleRestartAppRequest", type = "exclusive", timeout = 180, executeArgs = local.executeArgs);
}
return false;
this.$recordReloadRefusalReason(local.reloadAuthorized);
}
return local.reloadAuthorized;
}

// Run the rest of the request start code.
arguments.componentReference = "wheels.events.EventMethods";
application.wo.$simpleLock(
name = local.lockName,
execute = "$runOnRequestStart",
executeArgs = arguments,
type = "readOnly",
timeout = 180
);

return true;
public void function $restartAppRequest(required string lockName) {
application.wo.$debugPoint("total,reload");
if (StructKeyExists(url, "lock") && !url.lock) {
this.$handleRestartAppRequest();
} else {
// Case-exact "Application" — see the matching comment in onSessionStart().
// A lowercase reference turns every authorized reload into an HTTP 500 on
// Adobe CF + case-sensitive filesystems (issue #3053 follow-up).
local.executeArgs = {"componentReference" = "Application"};
application.wo.$simpleLock(name = arguments.lockName, execute = "$handleRestartAppRequest", type = "exclusive", timeout = 180, executeArgs = local.executeArgs);
}
}

public boolean function onRequest( string targetPage ) {
Expand Down
115 changes: 65 additions & 50 deletions vendor/wheels/events/onerror/cfmlerror.cfm
Original file line number Diff line number Diff line change
@@ -1,3 +1,64 @@
<cfscript>
function $cfmlErrorTagContext(required struct exception) {
if (
StructKeyExists(arguments.exception, "cause")
&& StructKeyExists(arguments.exception.cause, "tagContext")
&& ArrayLen(arguments.exception.cause.tagContext)
) {
return Duplicate(arguments.exception.cause.tagContext);
} else if (
StructKeyExists(arguments.exception, "rootCause")
&& StructKeyExists(arguments.exception.rootCause, "tagContext")
&& ArrayLen(arguments.exception.rootCause.tagContext)
) {
return Duplicate(arguments.exception.rootCause.tagContext);
} else if (
StructKeyExists(arguments.exception, "tagContext")
&& ArrayLen(arguments.exception.tagContext)
) {
return Duplicate(arguments.exception.tagContext);
}
return [];
}

function $cfmlErrorNormalizePath(required string path) {
local.norm = arguments.path;
local.norm = ReReplace(local.norm, "[" & Chr(92) & "(.*?)" & Chr(92) & "]", "." & Chr(92) & "1", "all");
local.norm = ReReplace(local.norm, "^" & Chr(92) & ".", "", "one");
return local.norm;
}

function $cfmlErrorSanitizeScope(required struct scope, required string skip, required string scopeName) {
local.hide = "wheels";
local.sanitizedScope = Duplicate(arguments.scope);
for (local.j in ListToArray(arguments.skip)) {
local.normalizedPath = $cfmlErrorNormalizePath(local.j);
if (local.normalizedPath CONTAINS "." AND ListFirst(local.normalizedPath, ".") EQ arguments.scopeName) {
local.relativePath = ListRest(local.normalizedPath, ".");
local.keyList = ListToArray(local.relativePath, ".");
local.ref = local.sanitizedScope;
local.depth = ArrayLen(local.keyList);
for (local.k = 1; local.k LTE local.depth; local.k++) {
local.key = local.keyList[local.k];
if (local.k EQ local.depth) {
if (StructKeyExists(local.ref, local.key)) {
StructDelete(local.ref, local.key);
}
} else {
if (StructKeyExists(local.ref, local.key) AND IsStruct(local.ref[local.key])) {
local.ref = local.ref[local.key];
} else {
break;
}
}
}
} else if (ListFindNoCase(arguments.skip, arguments.scopeName)) {
local.hide = ListAppend(local.hide, local.j);
}
}
return {hide = local.hide, sanitizedScope = local.sanitizedScope};
}
</cfscript>
<cfoutput>
<!--- cfformat-ignore-start --->
<div class="ui container" style="padding-bottom:2em;">
Expand All @@ -23,24 +84,7 @@
</h1>

<!--- Tag Context / Location --->
<cfif
StructKeyExists(arguments.exception, "cause")
&& StructKeyExists(arguments.exception.cause, "tagContext")
&& ArrayLen(arguments.exception.cause.tagContext)
>
<cfset local.tagContext = Duplicate(arguments.exception.cause.tagContext)>
<cfelseif
StructKeyExists(arguments.exception, "rootCause")
&& StructKeyExists(arguments.exception.rootCause, "tagContext")
&& ArrayLen(arguments.exception.rootCause.tagContext)
>
<cfset local.tagContext = Duplicate(arguments.exception.rootCause.tagContext)>
<cfelseif
StructKeyExists(arguments.exception, "tagContext")
&& ArrayLen(arguments.exception.tagContext)
>
<cfset local.tagContext = Duplicate(arguments.exception.tagContext)>
</cfif>
<cfset local.tagContext = $cfmlErrorTagContext(arguments.exception)>

<cfif StructKeyExists(local, "tagContext") AND ArrayLen(local.tagContext)>
<div style="margin:1.5em 0;">
Expand Down Expand Up @@ -140,38 +184,9 @@
<cfset local.scope = local.scopeMap[local.i]>
<cfif IsStruct(local.scope)>
<cfset local.scopeIdx = local.scopeIdx + 1>
<cfset local.hide = "wheels">
<cfset local.sanitizedScope = duplicate(local.scope)>

<cfloop list="#local.skip#" index="local.j">
<!--- Normalize the key path --->
<cfset local.normalizedPath = $normalizePath(local.j)>
<cfif local.normalizedPath CONTAINS "." AND ListFirst(local.normalizedPath, ".") EQ local.scopeName>
<!--- Get nested path relative to the scope --->
<cfset local.relativePath = ListRest(local.normalizedPath, ".")>
<cfset local.keyList = ListToArray(local.relativePath, ".")>

<!--- Walk into the sanitized struct and mask the nested key --->
<cfset local.ref = local.sanitizedScope>
<cfset local.depth = ArrayLen(local.keyList)>
<cfloop from="1" to="#local.depth#" index="local.k">
<cfset local.key = local.keyList[local.k]>
<cfif local.k EQ local.depth>
<cfif StructKeyExists(local.ref, local.key)>
<cfset StructDelete(local.ref, local.key)>
</cfif>
<cfelse>
<cfif StructKeyExists(local.ref, local.key) AND isStruct(local.ref[local.key])>
<cfset local.ref = local.ref[local.key]>
<cfelse>
<cfbreak>
</cfif>
</cfif>
</cfloop>
<cfelseif ListFindNoCase(local.skip, local.scopeName)>
<cfset local.hide = ListAppend(local.hide, local.j)>
</cfif>
</cfloop>
<cfset local.scopeMask = $cfmlErrorSanitizeScope(local.scope, local.skip, local.scopeName)>
<cfset local.hide = local.scopeMask.hide>
<cfset local.sanitizedScope = local.scopeMask.sanitizedScope>

<div style="margin-bottom:8px;">
<div onclick="var el=document.getElementById('scope-#LCase(local.scopeName)#');el.style.display=el.style.display==='none'?'block':'none';this.querySelector('svg').style.transform=el.style.display==='none'?'':'rotate(90deg)';" style="cursor:pointer;display:flex;align-items:center;gap:8px;padding:10px 16px;background:##181825;border:1px solid ##45475a;border-radius:6px;user-select:none;">
Expand Down
Loading
Loading