From 0c0f5e90228db779bb231bd5e3c6573d56b1607b Mon Sep 17 00:00:00 2001 From: Igor Rozum Date: Tue, 10 Nov 2026 11:15:57 -0500 Subject: [PATCH 1/2] Multisite: Send the theme and plugin editor loopback request to the network admin --- src/wp-admin/includes/file.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wp-admin/includes/file.php b/src/wp-admin/includes/file.php index 79a08bb2b9cba..105cd60d2c451 100644 --- a/src/wp-admin/includes/file.php +++ b/src/wp-admin/includes/file.php @@ -556,14 +556,14 @@ function wp_edit_theme_plugin_file( $args ) { // Attempt loopback request to editor to see if user just whitescreened themselves. if ( $plugin ) { - $url = add_query_arg( compact( 'plugin', 'file' ), admin_url( 'plugin-editor.php' ) ); + $url = add_query_arg( compact( 'plugin', 'file' ), network_admin_url( 'plugin-editor.php' ) ); } elseif ( $stylesheet ) { $url = add_query_arg( array( 'theme' => $stylesheet, 'file' => $file, ), - admin_url( 'theme-editor.php' ) + network_admin_url( 'theme-editor.php' ) ); } else { $url = admin_url(); From a2816264693547ace51af56c037d09cfa3140b20 Mon Sep 17 00:00:00 2001 From: Igor Rozum Date: Tue, 10 Nov 2026 11:15:57 -0500 Subject: [PATCH 2/2] Tests: Cover the network admin editor loopback in wp_edit_theme_plugin_file() --- .../admin/wpEditThemePluginFilePlugin.php | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/tests/phpunit/tests/admin/wpEditThemePluginFilePlugin.php b/tests/phpunit/tests/admin/wpEditThemePluginFilePlugin.php index a97fa7e37a851..8678c7ac52db2 100644 --- a/tests/phpunit/tests/admin/wpEditThemePluginFilePlugin.php +++ b/tests/phpunit/tests/admin/wpEditThemePluginFilePlugin.php @@ -368,6 +368,7 @@ public function test_should_save_non_php_file_of_active_plugin_without_loopback( * Tests that a PHP file of an active plugin is saved when both loopback checks pass. * * @ticket 65819 + * @ticket 45755 */ public function test_should_save_php_file_of_active_plugin_when_loopback_passes() { update_option( 'active_plugins', array( $this->get_plugin() ) ); @@ -377,7 +378,7 @@ public function test_should_save_php_file_of_active_plugin_when_loopback_passes( $this->assertSame( self::EDITED_PHP, $this->get_contents( self::SLUG . '.php' ), 'The new content should be written.' ); $this->assertSame( array( - wp_parse_url( admin_url( 'plugin-editor.php' ), PHP_URL_PATH ), + wp_parse_url( network_admin_url( 'plugin-editor.php' ), PHP_URL_PATH ), wp_parse_url( home_url( '/' ), PHP_URL_PATH ), ), $this->get_loopback_paths(), @@ -426,6 +427,7 @@ function ( $response, $args, $url ) use ( &$captured_headers ) { * Tests that a PHP file of a network-active plugin triggers loopback checks on multisite. * * @ticket 65819 + * @ticket 45755 * @group ms-required */ public function test_should_trigger_loopback_for_network_active_plugin() { @@ -436,7 +438,7 @@ public function test_should_trigger_loopback_for_network_active_plugin() { $this->assertSame( self::EDITED_PHP, $this->get_contents( self::SLUG . '.php' ), 'The new content should be written.' ); $this->assertSame( array( - wp_parse_url( admin_url( 'plugin-editor.php' ), PHP_URL_PATH ), + wp_parse_url( network_admin_url( 'plugin-editor.php' ), PHP_URL_PATH ), wp_parse_url( home_url( '/' ), PHP_URL_PATH ), ), $this->get_loopback_paths(), @@ -445,10 +447,29 @@ public function test_should_trigger_loopback_for_network_active_plugin() { $this->assert_scrape_key_deleted(); } + /** + * Tests that the editor loopback request is sent to the network admin on multisite. + * + * @ticket 45755 + * @group ms-required + */ + public function test_should_send_editor_loopback_to_network_admin_on_multisite() { + update_option( 'active_plugins', array( $this->get_plugin() ) ); + $this->scrape_results = array( 'true', 'true' ); + + $this->assertTrue( wp_edit_theme_plugin_file( $this->get_request( self::SLUG . '.php', self::EDITED_PHP ) ), 'The file should be saved.' ); + + $editor_path = $this->get_loopback_paths()[0]; + $this->assertSame( wp_parse_url( network_admin_url( 'plugin-editor.php' ), PHP_URL_PATH ), $editor_path, 'The editor loopback should use the network admin URL.' ); + $this->assertStringContainsString( '/wp-admin/network/', $editor_path, 'The editor loopback should target the network admin.' ); + $this->assertNotSame( wp_parse_url( admin_url( 'plugin-editor.php' ), PHP_URL_PATH ), $editor_path, 'The editor loopback should not target the site admin, which redirects.' ); + } + /** * Tests that a PHP file edit of an active plugin is reverted when a loopback check fails. * * @ticket 65819 + * @ticket 45755 * * @dataProvider data_failed_loopbacks * @@ -480,7 +501,7 @@ public function test_should_revert_php_file_of_active_plugin_when_loopback_fails */ public static function data_failed_loopbacks() { $loopback_failed = 'Unable to communicate back with site to check for fatal errors, so the PHP change was reverted. You will need to upload your PHP file change by some other means, such as by using SFTP.'; - $editor_path = wp_parse_url( admin_url( 'plugin-editor.php' ), PHP_URL_PATH ); + $editor_path = wp_parse_url( network_admin_url( 'plugin-editor.php' ), PHP_URL_PATH ); $home_path = wp_parse_url( home_url( '/' ), PHP_URL_PATH ); return array( @@ -545,7 +566,7 @@ public function record_loopback( $response, $args, $url ) { $this->assertArrayHasKey( 'wp_scrape_key', $query, 'The loopback should include the scrape key.' ); $this->assertArrayHasKey( 'wp_scrape_nonce', $query, 'The loopback should include the scrape nonce.' ); - $editor_path = wp_parse_url( admin_url( 'plugin-editor.php' ), PHP_URL_PATH ); + $editor_path = wp_parse_url( network_admin_url( 'plugin-editor.php' ), PHP_URL_PATH ); if ( wp_parse_url( $url, PHP_URL_PATH ) === $editor_path ) { $this->assertArrayHasKey( 'plugin', $query, 'The editor loopback should include the plugin query arg.' ); $this->assertSame( $this->get_plugin(), $query['plugin'], 'The editor loopback should specify the edited plugin.' );