diff --git a/CHANGELOG.md b/CHANGELOG.md
index 45302a9174e..d22d1f36a38 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@
- Fixed a bug where named image transforms generated by the `craft` asset transformer were stored and indexed by their parameters rather than their handle. ([#20588](https://github.com/craftcms/cms/issues/20588), [#20594](https://github.com/craftcms/cms/pull/20594))
- Fixed an error that occurred when transforming an asset that its asset transformer can’t transform, such as a video, via GraphQL or the transform arguments of `asset.getWidth()`, `getHeight()`, `getFormat()`, and `getMimeType()`. The asset’s own URL, dimensions, format, and MIME type are returned instead. ([#20588](https://github.com/craftcms/cms/issues/20588), [#20594](https://github.com/craftcms/cms/pull/20594))
- Fixed a bug where `asset.getUrl()` and `asset.url` returned `null` for assets that their asset transformer can’t transform, when a transform was set, rather than the asset’s own URL. ([#20588](https://github.com/craftcms/cms/issues/20588), [#20594](https://github.com/craftcms/cms/pull/20594))
+- Fixed a bug where saving plugin settings with “Save and continue editing” or Command/Ctrl + S could redirect to the front-end homepage. ([#20619](https://github.com/craftcms/cms/pull/20619))
- GraphQL transform arguments now accept `immediately` again, which asks the asset transformer to generate the transform before returning its URL. ([#20588](https://github.com/craftcms/cms/issues/20588), [#20594](https://github.com/craftcms/cms/pull/20594))
- Asset transform drivers now receive whether the transform should be generated immediately in `$request->immediately`, including legacy image transformers. ([#20588](https://github.com/craftcms/cms/issues/20588), [#20594](https://github.com/craftcms/cms/pull/20594))
- Added `CraftCms\Cms\Asset\Data\AssetTransformRequest::$immediately` and `$namedTransform`. ([#20588](https://github.com/craftcms/cms/issues/20588), [#20594](https://github.com/craftcms/cms/pull/20594))
diff --git a/src/Http/Controllers/PluginsController.php b/src/Http/Controllers/PluginsController.php
index b56b47ab8bc..b2f018d7f06 100644
--- a/src/Http/Controllers/PluginsController.php
+++ b/src/Http/Controllers/PluginsController.php
@@ -180,6 +180,15 @@ public function saveSettings(Request $request, string $handle): Response
abort_if(is_null($plugin), 404, 'Plugin not found.');
+ // Save and continue editing posts no `redirect`. Return to the settings
+ // page, as Craft 5's form did, rather than to the Referer, which browsers
+ // may trim to the site's origin. Legacy forms that post the
+ // `plugins/save-plugin-settings` action from their own pages still go
+ // back to them.
+ $settingsUrl = $request->expectsJson() || $request->isActionRequest()
+ ? null
+ : Url::cpUrl("settings/plugins/{$handle}");
+
$requestClass = $plugin->getSettingsRequestClass();
$settings = $request->input('settings', []);
@@ -198,7 +207,7 @@ public function saveSettings(Request $request, string $handle): Response
throw ValidationException::withMessages($errors);
}
- return $this->asSuccess(t('Plugin settings saved.'));
+ return $this->asSuccess(t('Plugin settings saved.'), redirect: $this->getPostedRedirectUrl() ?? $settingsUrl);
}
public function renderSettingsUi(Request $request, string $handle): JsonResponse
diff --git a/tests/Feature/Http/Controllers/PluginsControllerTest.php b/tests/Feature/Http/Controllers/PluginsControllerTest.php
index b1d9744f275..c1b80d3fdc7 100644
--- a/tests/Feature/Http/Controllers/PluginsControllerTest.php
+++ b/tests/Feature/Http/Controllers/PluginsControllerTest.php
@@ -290,6 +290,16 @@ public function getReadOnlySettingsResponse(): mixed
expect($settings->bar)->toBe('baz');
})->with(['JSON' => true, 'form' => false]);
+test('saveSettings without a posted redirect returns to the settings page, not the Referer', function () {
+ // Save and continue editing posts no `redirect`, and some browsers send
+ // only the origin as the Referer.
+ $this->withHeader('Referer', url('/'))
+ ->post(action([PluginsController::class, 'saveSettings'], ['test-plugin']), [
+ 'settings' => ['foo' => 'bar', 'bar' => 'baz'],
+ ])
+ ->assertRedirect(cp_url('settings/plugins/test-plugin'));
+});
+
test('saveSettings returns settings model validation errors for JSON requests without saving', function () {
$url = action([PluginsController::class, 'saveSettings'], ['test-plugin']);
app(ProjectConfig::class)->set('plugins.test-plugin.settings', ['foo' => 'saved value', 'bar' => 'saved bar']);