Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/3030-reload-envswitch-redirect.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- URL environment switching (`?reload=<environment>&password=...`) now works through the app template's `applicationStop()` reload flow: the restart redirect preserves `reload` + `password` for environment switches (plain `?reload=true` still strips everything), the configured `reloadPassword` is handed across the restart via a single-use server-scope entry so the framework's switch code can verify it on the cold start, and the reload gate skips the restart once the requested environment is active so the redirect chain always terminates. `allowEnvironmentSwitchViaUrl` is enforced pre-restart: when switching is disallowed — `set(allowEnvironmentSwitchViaUrl=false)` or the framework's production/testing/maintenance auto-disable — the parameters are stripped and the request degrades to a plain restart, preserving the existing hardening (the framework cannot enforce the flag itself after `applicationStop()` destroys its carryover state). Applied to all four `public/Application.cfc` copies (CLI app template, repo demo app, starter-app and tweet examples). Trade-off: `?reload=<current-environment>` is now a no-op — use `?reload=true` for a same-environment restart (#3030)
114 changes: 113 additions & 1 deletion cli/lucli/templates/app/public/Application.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,29 @@ component output="false" {

function onApplicationStart() {
application.env = duplicate(this.env);

// Consume the single-use reload-password handoff left by
// $handleRestartAppRequest() for environment-switch restarts (issue #3030).
// The framework's switch code in wheels/events/onapplicationstart.cfc runs
// before config/settings.cfm is loaded and gets the configured password via
// carryover from application.wheels.reloadPassword — which applicationStop()
// destroys. Seeding this.wheels.reloadPassword here restores that carryover
// on the post-restart cold start ($init copies this.wheels into
// application.wheels before the carryover check).
local.handoffKey = "$wheelsReloadPasswordHandoff_" & this.name;
if (StructKeyExists(server, local.handoffKey)) {
local.handoff = server[local.handoffKey];
StructDelete(server, local.handoffKey);
if (
IsStruct(local.handoff)
&& StructKeyExists(local.handoff, "reloadPassword")
&& StructKeyExists(local.handoff, "expiresAt")
&& DateCompare(Now(), local.handoff.expiresAt) < 0
) {
this.wheels.reloadPassword = local.handoff.reloadPassword;
}
}

application.wheelsdi = new wheels.Injector("wheels.Bindings");

/* wheels/global object */
Expand Down Expand Up @@ -220,9 +243,25 @@ component output="false" {
}
}

// 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
// them on the request that starts the new application. When that redirected
// request arrives here the switch has already been applied, so firing another
// applicationStop() would redirect forever. If the requested environment is
// 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")
&& !IsBoolean(url.reload)
&& StructKeyExists(application, "wheels")
&& StructKeyExists(application.wheels, "environment")
&& application.wheels.environment == url.reload;

// Reload application properly using applicationStop() if requested.
if (
StructKeyExists(url, "reload")
&& !local.environmentSwitchAlreadyApplied
&& (
!StructKeyExists(application, "wheels") || !StructKeyExists(application.wheels, "reloadPassword")
|| !Len(application.wheels.reloadPassword)
Expand Down Expand Up @@ -377,6 +416,44 @@ component output="false" {

public void function $handleRestartAppRequest() {
local.redirectUrl = this.$buildRedirectUrl();

// Environment-switch restarts (?reload=<environment>) need the configured
// reloadPassword available when the NEW application starts: the switch code
// in wheels/events/onapplicationstart.cfc runs before config/settings.cfm is
// loaded and normally reads the password via carryover from the live
// application scope, which applicationStop() destroys. Hand it across the
// restart via a single-use, short-lived server-scope entry consumed by
// onApplicationStart() (issue #3030). The value is the app's own configured
// password (the request's password was already verified against it by the
// reload gate), and the server scope is only reachable by code running on
// this engine — the same trust domain as config/settings.cfm itself.
// Skipped when allowEnvironmentSwitchViaUrl is explicitly disabled (covers
// both set(allowEnvironmentSwitchViaUrl=false) and the framework's
// production/testing/maintenance auto-disable): after applicationStop()
// the framework cannot enforce the flag itself — its revert in
// wheels/events/onapplicationstart.cfc needs carryover state the restart
// destroys — so this pre-restart gate is the only place the off-switch
// holds. A missing flag counts as allowed, matching the framework's
// carryover default.
if (
StructKeyExists(url, "reload")
&& !IsBoolean(url.reload)
&& Len(url.reload)
&& StructKeyExists(url, "password")
&& StructKeyExists(application, "wheels")
&& StructKeyExists(application.wheels, "reloadPassword")
&& Len(application.wheels.reloadPassword)
&& (
!StructKeyExists(application.wheels, "allowEnvironmentSwitchViaUrl")
|| application.wheels.allowEnvironmentSwitchViaUrl
)
) {
server["$wheelsReloadPasswordHandoff_" & this.name] = {
reloadPassword: application.wheels.reloadPassword,
expiresAt: DateAdd("n", 1, Now())
};
}

applicationStop();
location(url = local.redirectUrl, addToken = false);
}
Expand All @@ -390,6 +467,41 @@ component output="false" {
local.url = cgi.script_name;
}

// For a plain restart (?reload=true) every reload-related parameter is
// stripped so the redirected request cannot trigger another restart. For an
// environment switch (?reload=<environment>) the framework needs URL.reload
// and URL.password present on the request that starts the new application
// (vendor/wheels/events/onapplicationstart.cfc), so those two survive the
// redirect; the restart loop is broken in onRequestStart instead, which
// skips the restart once the requested environment is active (issue #3030).
// Only preserve when the switch can actually be applied (a non-empty
// reloadPassword is configured and the request carries a password) —
// otherwise the new application could never switch and the preserved
// parameters would redirect forever. The same goes for
// allowEnvironmentSwitchViaUrl: when switching is explicitly disallowed
// (set(allowEnvironmentSwitchViaUrl=false) or the framework's
// production/testing/maintenance auto-disable) the parameters are
// stripped and the request degrades to a plain restart — the framework
// cannot enforce the flag on the post-applicationStop() cold start, so
// it must be enforced here, pre-restart. A missing flag counts as
// allowed, matching the framework's carryover default.
local.stripParams = "reload,password,lock";
if (
StructKeyExists(url, "reload")
&& !IsBoolean(url.reload)
&& Len(url.reload)
&& StructKeyExists(url, "password")
&& StructKeyExists(application, "wheels")
&& StructKeyExists(application.wheels, "reloadPassword")
&& Len(application.wheels.reloadPassword)
&& (
!StructKeyExists(application.wheels, "allowEnvironmentSwitchViaUrl")
|| application.wheels.allowEnvironmentSwitchViaUrl
)
) {
local.stripParams = "lock";
}

if (StructKeyExists(cgi, "query_string") && Len(cgi.query_string)) {
local.oldQueryString = ListToArray(cgi.query_string, "&");
local.newQueryString = [];
Expand All @@ -399,7 +511,7 @@ component output="false" {
local.keyValue = local.oldQueryString[local.i];
local.key = ListFirst(local.keyValue, "=");

if (!ListFindNoCase("reload,password,lock", local.key)) {
if (!ListFindNoCase(local.stripParams, local.key)) {
ArrayAppend(local.newQueryString, local.keyValue);
}
}
Expand Down
113 changes: 112 additions & 1 deletion examples/starter-app/public/Application.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,28 @@ component output="false" {
include "../config/app.cfm";

function onApplicationStart() {
// Consume the single-use reload-password handoff left by
// $handleRestartAppRequest() for environment-switch restarts (issue #3030).
// The framework's switch code in wheels/events/onapplicationstart.cfc runs
// before config/settings.cfm is loaded and gets the configured password via
// carryover from application.wheels.reloadPassword — which applicationStop()
// destroys. Seeding this.wheels.reloadPassword here restores that carryover
// on the post-restart cold start ($init copies this.wheels into
// application.wheels before the carryover check).
local.handoffKey = "$wheelsReloadPasswordHandoff_" & this.name;
if (StructKeyExists(server, local.handoffKey)) {
local.handoff = server[local.handoffKey];
StructDelete(server, local.handoffKey);
if (
IsStruct(local.handoff)
&& StructKeyExists(local.handoff, "reloadPassword")
&& StructKeyExists(local.handoff, "expiresAt")
&& DateCompare(Now(), local.handoff.expiresAt) < 0
) {
this.wheels.reloadPassword = local.handoff.reloadPassword;
}
}

application.wheelsdi = new wheels.Injector("wheels.Bindings");

/* wheels/global object */
Expand Down Expand Up @@ -199,9 +221,25 @@ component output="false" {
}
}

// 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
// them on the request that starts the new application. When that redirected
// request arrives here the switch has already been applied, so firing another
// applicationStop() would redirect forever. If the requested environment is
// 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")
&& !IsBoolean(url.reload)
&& StructKeyExists(application, "wheels")
&& StructKeyExists(application.wheels, "environment")
&& application.wheels.environment == url.reload;

// Reload application properly using applicationStop() if requested.
if (
StructKeyExists(url, "reload")
&& !local.environmentSwitchAlreadyApplied
&& (
!StructKeyExists(application, "wheels") || !StructKeyExists(application.wheels, "reloadPassword")
|| !Len(application.wheels.reloadPassword)
Expand Down Expand Up @@ -315,6 +353,44 @@ component output="false" {

public void function $handleRestartAppRequest() {
local.redirectUrl = this.$buildRedirectUrl();

// Environment-switch restarts (?reload=<environment>) need the configured
// reloadPassword available when the NEW application starts: the switch code
// in wheels/events/onapplicationstart.cfc runs before config/settings.cfm is
// loaded and normally reads the password via carryover from the live
// application scope, which applicationStop() destroys. Hand it across the
// restart via a single-use, short-lived server-scope entry consumed by
// onApplicationStart() (issue #3030). The value is the app's own configured
// password (the request's password was already verified against it by the
// reload gate), and the server scope is only reachable by code running on
// this engine — the same trust domain as config/settings.cfm itself.
// Skipped when allowEnvironmentSwitchViaUrl is explicitly disabled (covers
// both set(allowEnvironmentSwitchViaUrl=false) and the framework's
// production/testing/maintenance auto-disable): after applicationStop()
// the framework cannot enforce the flag itself — its revert in
// wheels/events/onapplicationstart.cfc needs carryover state the restart
// destroys — so this pre-restart gate is the only place the off-switch
// holds. A missing flag counts as allowed, matching the framework's
// carryover default.
if (
StructKeyExists(url, "reload")
&& !IsBoolean(url.reload)
&& Len(url.reload)
&& StructKeyExists(url, "password")
&& StructKeyExists(application, "wheels")
&& StructKeyExists(application.wheels, "reloadPassword")
&& Len(application.wheels.reloadPassword)
&& (
!StructKeyExists(application.wheels, "allowEnvironmentSwitchViaUrl")
|| application.wheels.allowEnvironmentSwitchViaUrl
)
) {
server["$wheelsReloadPasswordHandoff_" & this.name] = {
reloadPassword: application.wheels.reloadPassword,
expiresAt: DateAdd("n", 1, Now())
};
}

applicationStop();
location(url = local.redirectUrl, addToken = false);
}
Expand All @@ -329,6 +405,41 @@ component output="false" {
local.url = cgi.script_name;
}

// For a plain restart (?reload=true) every reload-related parameter is
// stripped so the redirected request cannot trigger another restart. For an
// environment switch (?reload=<environment>) the framework needs URL.reload
// and URL.password present on the request that starts the new application
// (vendor/wheels/events/onapplicationstart.cfc), so those two survive the
// redirect; the restart loop is broken in onRequestStart instead, which
// skips the restart once the requested environment is active (issue #3030).
// Only preserve when the switch can actually be applied (a non-empty
// reloadPassword is configured and the request carries a password) —
// otherwise the new application could never switch and the preserved
// parameters would redirect forever. The same goes for
// allowEnvironmentSwitchViaUrl: when switching is explicitly disallowed
// (set(allowEnvironmentSwitchViaUrl=false) or the framework's
// production/testing/maintenance auto-disable) the parameters are
// stripped and the request degrades to a plain restart — the framework
// cannot enforce the flag on the post-applicationStop() cold start, so
// it must be enforced here, pre-restart. A missing flag counts as
// allowed, matching the framework's carryover default.
local.stripParams = "reload,password,lock";
if (
StructKeyExists(url, "reload")
&& !IsBoolean(url.reload)
&& Len(url.reload)
&& StructKeyExists(url, "password")
&& StructKeyExists(application, "wheels")
&& StructKeyExists(application.wheels, "reloadPassword")
&& Len(application.wheels.reloadPassword)
&& (
!StructKeyExists(application.wheels, "allowEnvironmentSwitchViaUrl")
|| application.wheels.allowEnvironmentSwitchViaUrl
)
) {
local.stripParams = "lock";
}

// Process query string parameters, removing reload-related ones
if (StructKeyExists(cgi, "query_string") && Len(cgi.query_string)) {
local.oldQueryString = ListToArray(cgi.query_string, "&");
Expand All @@ -340,7 +451,7 @@ component output="false" {
local.key = ListFirst(local.keyValue, "=");

// Remove reload-related parameters
if (!ListFindNoCase("reload,password,lock", local.key)) {
if (!ListFindNoCase(local.stripParams, local.key)) {
ArrayAppend(local.newQueryString, local.keyValue);
}
}
Expand Down
Loading
Loading