From c84df8b5f283734793f4e21a5f6e850c910e592d Mon Sep 17 00:00:00 2001 From: Keith Solomon Date: Thu, 7 May 2026 06:28:41 -0500 Subject: [PATCH] feat: save admin sync settings --- src/Admin/SettingsController.php | 46 ++++++++++++++ src/Plugin.php | 14 +++++ tests/Unit/Admin/SettingsControllerTest.php | 70 +++++++++++++++++++++ tests/Unit/PluginTest.php | 19 ++++++ 4 files changed, 149 insertions(+) create mode 100644 src/Admin/SettingsController.php create mode 100644 tests/Unit/Admin/SettingsControllerTest.php diff --git a/src/Admin/SettingsController.php b/src/Admin/SettingsController.php new file mode 100644 index 0000000..53ff75b --- /dev/null +++ b/src/Admin/SettingsController.php @@ -0,0 +1,46 @@ +settings_repository = $settings_repository; + } + + public function register(): void { + add_action( 'admin_post_wpcs_save_settings', array( $this, 'handleSave' ) ); + } + + public function handleSave(): void { + if ( ! current_user_can( 'manage_options' ) ) { + throw new \RuntimeException( 'You do not have permission to save WP Content Sync settings.' ); + } + + if ( ! check_admin_referer( 'wpcs_save_settings', 'wpcs_settings_nonce' ) ) { + throw new \RuntimeException( 'The settings save request could not be verified.' ); + } + + // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce verified above; Settings::fromArray sanitizes the full option payload. + $data = isset( $_POST['wpcs_settings'] ) ? wp_unslash( $_POST['wpcs_settings'] ) : array(); + $data = is_array( $data ) ? $data : array(); + + $this->settings_repository->save( Settings::fromArray( $data ) ); + + wp_safe_redirect( + add_query_arg( + array( 'wpcs_settings_saved' => '1' ), + admin_url( 'admin.php?page=wp-content-sync' ) + ) + ); + } +} diff --git a/src/Plugin.php b/src/Plugin.php index e5dfa05..b37c33c 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -9,6 +9,7 @@ namespace WPContentSync; use WPContentSync\Admin\AdminPage; use WPContentSync\Admin\FileImportController; +use WPContentSync\Admin\SettingsController; use WPContentSync\Content\ContentHandlerRegistry; use WPContentSync\Content\ContentRecordNormalizer; use WPContentSync\Content\MediaContentHandler; @@ -169,6 +170,15 @@ final class Plugin { } ); + $container->factory( + SettingsController::class, + static function () use ( $container ): SettingsController { + return new SettingsController( + $container->get( SettingsRepository::class ) + ); + } + ); + $container->factory( RestTransportClient::class, static function (): RestTransportClient { @@ -209,8 +219,12 @@ final class Plugin { /** @var RestPackageController $rest_package_controller */ $rest_package_controller = $this->container->get( RestPackageController::class ); + /** @var SettingsController $settings_controller */ + $settings_controller = $this->container->get( SettingsController::class ); + $admin_page->register(); $file_import_controller->register(); $rest_package_controller->register(); + $settings_controller->register(); } } diff --git a/tests/Unit/Admin/SettingsControllerTest.php b/tests/Unit/Admin/SettingsControllerTest.php new file mode 100644 index 0000000..dc354c7 --- /dev/null +++ b/tests/Unit/Admin/SettingsControllerTest.php @@ -0,0 +1,70 @@ + 'debug', + 'conflict_strategy' => 'manual_review', + 'sync_pairs' => array( + array( + 'name' => 'Staging', + 'source_url' => 'https://example.test', + 'destination_url' => 'https://staging.example.test', + ), + ), + ); + + ( new SettingsController( new SettingsRepository() ) )->handleSave(); + + self::assertSame( 'debug', $GLOBALS['wpcs_test_options'][ SettingsRepository::OPTION_NAME ]['logging_level'] ); + self::assertSame( 'manual_review', $GLOBALS['wpcs_test_options'][ SettingsRepository::OPTION_NAME ]['conflict_strategy'] ); + self::assertStringContainsString( 'wpcs_settings_saved=1', $GLOBALS['wpcs_redirect_location'] ); + } + + public function test_it_rejects_users_without_manage_options(): void { + $GLOBALS['wpcs_current_user_can']['manage_options'] = false; + + $this->expectException( \RuntimeException::class ); + $this->expectExceptionMessage( 'You do not have permission to save WP Content Sync settings.' ); + + ( new SettingsController( new SettingsRepository() ) )->handleSave(); + } + + public function test_it_rejects_invalid_nonces(): void { + $GLOBALS['wpcs_nonce_valid']['wpcs_save_settings']['wpcs_settings_nonce'] = false; + + $this->expectException( \RuntimeException::class ); + $this->expectExceptionMessage( 'The settings save request could not be verified.' ); + + ( new SettingsController( new SettingsRepository() ) )->handleSave(); + } +} diff --git a/tests/Unit/PluginTest.php b/tests/Unit/PluginTest.php index 5427d14..99db60a 100644 --- a/tests/Unit/PluginTest.php +++ b/tests/Unit/PluginTest.php @@ -4,6 +4,7 @@ namespace WPContentSync\Tests\Unit; use PHPUnit\Framework\TestCase; use WPContentSync\Admin\FileImportController; +use WPContentSync\Admin\SettingsController; use WPContentSync\Content\ContentHandlerRegistry; use WPContentSync\Content\ContentRecordNormalizer; use WPContentSync\Content\MediaContentHandler; @@ -51,6 +52,15 @@ class PluginTest extends TestCase { ); } + public function test_it_registers_settings_controller(): void { + $container = $this->getPluginContainer( Plugin::create() ); + + self::assertInstanceOf( + SettingsController::class, + $container->get( SettingsController::class ) + ); + } + public function test_it_registers_rest_transport_services(): void { $container = $this->getPluginContainer( Plugin::create() ); @@ -85,6 +95,15 @@ class PluginTest extends TestCase { self::assertArrayHasKey( 'rest_api_init', $GLOBALS['wpcs_test_actions'] ); } + public function test_it_hooks_settings_controller_on_register(): void { + unset( $GLOBALS['wpcs_test_actions'] ); + + $plugin = Plugin::create(); + $plugin->register(); + + self::assertArrayHasKey( 'admin_post_wpcs_save_settings', $GLOBALS['wpcs_test_actions'] ); + } + private function getPluginContainer( Plugin $plugin ): Container { $reflection = new \ReflectionClass( $plugin ); $property = $reflection->getProperty( 'container' );