From 1e3e4ce61e62e035636596abf1d99c2833ab6e26 Mon Sep 17 00:00:00 2001 From: Darin Kotter Date: Mon, 13 Apr 2026 11:09:04 -0600 Subject: [PATCH 1/3] Redirect the old settings page query param to the new one. Hook both into admin_init and the page denied hook, as the latter seems to fire most often --- includes/Settings/Settings_Page.php | 41 +++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/includes/Settings/Settings_Page.php b/includes/Settings/Settings_Page.php index a29c4fc56..1926b31ae 100644 --- a/includes/Settings/Settings_Page.php +++ b/includes/Settings/Settings_Page.php @@ -14,6 +14,7 @@ use WordPress\AI\Experiments\Experiment_Category; use WordPress\AI\Features\Feature_Category; use WordPress\AI\Features\Registry; + use function WordPress\AI\has_ai_credentials; use function WordPress\AI\has_valid_ai_credentials; @@ -27,6 +28,15 @@ */ class Settings_Page { + /** + * Legacy settings page slug. + * + * @since x.x.x + * + * @var string + */ + private const LEGACY_PAGE_SLUG = 'ai'; + /** * The settings page slug. * @@ -45,6 +55,9 @@ class Settings_Page { * @return void */ public static function init( Registry $registry ): void { + add_action( 'admin_init', array( self::class, 'maybe_redirect_legacy_page' ), 1 ); + add_action( 'admin_page_access_denied', array( self::class, 'maybe_redirect_legacy_page' ) ); + if ( function_exists( 'ai_ai_wp_admin_render_page' ) ) { add_action( 'admin_menu', @@ -92,6 +105,34 @@ static function () { } } + /** + * Redirects legacy settings page slug to the current settings route. + * + * @since x.x.x + */ + public static function maybe_redirect_legacy_page(): void { + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading query param for admin page detection only, no data processing. + if ( ! isset( $_GET['page'] ) ) { + return; + } + + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading query param for admin page detection only, no data processing. + $page = sanitize_key( wp_unslash( (string) $_GET['page'] ) ); + if ( self::LEGACY_PAGE_SLUG !== $page ) { + return; + } + + $redirect_url = add_query_arg( + 'page', + self::PAGE_SLUG, + admin_url( 'options-general.php' ) + ); + + if ( wp_safe_redirect( $redirect_url, 301, 'WordPress AI plugin' ) ) { + exit; + } + } + /** * Gets feature group metadata for the settings UI. * From 0d261c7dcd798ecbf0cb4f66d0deb38b838b6941 Mon Sep 17 00:00:00 2001 From: Darin Kotter Date: Mon, 13 Apr 2026 11:09:38 -0600 Subject: [PATCH 2/3] Update tests --- .../Includes/Settings/Settings_PageTest.php | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/tests/Integration/Includes/Settings/Settings_PageTest.php b/tests/Integration/Includes/Settings/Settings_PageTest.php index 1bfc37f0b..05f5d6868 100644 --- a/tests/Integration/Includes/Settings/Settings_PageTest.php +++ b/tests/Integration/Includes/Settings/Settings_PageTest.php @@ -238,6 +238,8 @@ public function setUp(): void { public function tearDown(): void { remove_all_filters( 'wpai_settings_feature_groups' ); remove_all_filters( 'wpai_settings_feature_metadata' ); + remove_all_filters( 'wp_redirect' ); + $_GET = array(); parent::tearDown(); } @@ -486,4 +488,43 @@ public function test_feature_without_settings_has_empty_settings_fields() { $this->assertArrayHasKey( 'settingsFields', $feature ); $this->assertSame( array(), $feature['settingsFields'], 'Feature without custom settings should have empty settingsFields' ); } + + /** + * Test that init registers an admin redirect hook for the legacy settings slug. + */ + public function test_init_registers_legacy_settings_redirect_hook() { + Settings_Page::init( $this->registry ); + + $this->assertTrue( + has_action( 'admin_init', array( Settings_Page::class, 'maybe_redirect_legacy_page' ) ) !== false + ); + $this->assertTrue( + has_action( 'admin_page_access_denied', array( Settings_Page::class, 'maybe_redirect_legacy_page' ) ) !== false + ); + } + + /** + * Test that the legacy settings slug redirects to the new slug. + */ + public function test_legacy_settings_slug_redirects_to_new_slug() { + $captured_location = null; + $captured_status = null; + + add_filter( + 'wp_redirect', + static function ( $location, $status ) use ( &$captured_location, &$captured_status ) { + $captured_location = $location; + $captured_status = $status; + return false; + }, + 10, + 2 + ); + + $_GET['page'] = 'ai'; + Settings_Page::maybe_redirect_legacy_page(); + + $this->assertSame( admin_url( 'options-general.php?page=ai-wp-admin' ), $captured_location ); + $this->assertSame( 301, $captured_status ); + } } From c6a366f2625253825cc9812a2025c4ba82f350a6 Mon Sep 17 00:00:00 2001 From: Darin Kotter Date: Mon, 13 Apr 2026 12:30:03 -0600 Subject: [PATCH 3/3] Update includes/Settings/Settings_Page.php Co-authored-by: Jeffrey Paul --- includes/Settings/Settings_Page.php | 1 + 1 file changed, 1 insertion(+) diff --git a/includes/Settings/Settings_Page.php b/includes/Settings/Settings_Page.php index 1926b31ae..134ef3ffa 100644 --- a/includes/Settings/Settings_Page.php +++ b/includes/Settings/Settings_Page.php @@ -30,6 +30,7 @@ class Settings_Page { /** * Legacy settings page slug. + * TODO: either once [0.6.0 is less than 10% of installs](https://wordpress.org/plugins/ai/advanced/) or we're in October 2026 let's remove this section in case other plugin(s) are attempting to use the `ai` page. * * @since x.x.x *