From addddc3217c2316e4fde527da12c3dca538f8196 Mon Sep 17 00:00:00 2001 From: Jonny Harrus Date: Mon, 5 Oct 2026 12:57:22 +0100 Subject: [PATCH 1/3] REST API: Limit the sites endpoint to the current network. Remove cross-network functionality from WP_REST_Sites_Controller: - Drop the `network` and `network_exclude` collection parameters and the `network__in` orderby; collections are always limited to the current network. - Make the `network` schema property read-only, so sites can no longer be created on, or moved to, another network. - Replace check_network_ids() with site_in_network(), filterable via the new `rest_site_in_network` filter ( bool $in_network, WP_Site $site, int $network_id ). - Get, update and delete permission checks return `rest_unable_read_from_network` for a site that is not on the current network, before capability checks. - Simplify check_url_is_available() now that network_id is never supplied. - Remove the network permission tests, add coverage for every CRUD route against a site on a second network and for the filter, and regenerate the API fixture. See #40365. Co-Authored-By: Claude Opus 5.5 --- .../class-wp-rest-sites-controller.php | 134 ++--- .../tests/rest-api/wpRestSitesController.php | 501 ++++++++++-------- tests/qunit/fixtures/wp-api-generated.js | 31 +- 3 files changed, 321 insertions(+), 345 deletions(-) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-sites-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-sites-controller.php index 6e2b1b94c3b95..ed230df32902d 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-sites-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-sites-controller.php @@ -133,16 +133,6 @@ public function get_items_permissions_check( $request ) { return $multisite_support; } - foreach ( array( 'network', 'network_exclude' ) as $network_key ) { - if ( ! isset( $request[ $network_key ] ) ) { - continue; - } - $network_check = $this->check_network_ids( (array) $request[ $network_key ] ); - if ( is_wp_error( $network_check ) ) { - return $network_check; - } - } - if ( $this->check_edit_permission() ) { return true; } @@ -203,8 +193,6 @@ public function get_items( $request ) { 'include' => 'site__in', 'offset' => 'offset', 'order' => 'order', - 'network' => 'network__in', - 'network_exclude' => 'network__not_in', 'per_page' => 'number', 'path' => 'path__in', 'path_exclude' => 'path__not_in', @@ -230,10 +218,8 @@ public function get_items( $request ) { } } - // Without an explicit network filter, limit the results to the current network. - if ( empty( $prepared_args['network__in'] ) ) { - $prepared_args['network__in'] = array( get_current_network_id() ); - } + // Only sites on the current network are exposed. + $prepared_args['network__in'] = array( get_current_network_id() ); // WP_Site_Query tests the status columns with is_numeric(), and a boolean is not numeric. foreach ( array( 'public', 'archived', 'mature', 'spam', 'deleted' ) as $status_param ) { @@ -260,7 +246,7 @@ public function get_items( $request ) { $orderby = $request['orderby']; // Ordering by an ID list needs a list to order by. - if ( in_array( $orderby, array( 'site__in', 'network__in' ), true ) && empty( $prepared_args[ $orderby ] ) ) { + if ( 'site__in' === $orderby && empty( $prepared_args['site__in'] ) ) { $orderby = 'id'; } @@ -426,11 +412,8 @@ public function get_item_permissions_check( $request ) { return $site; } - if ( $site->network_id > 0 ) { - $network_check = $this->check_network_ids( (array) $site->network_id ); - if ( is_wp_error( $network_check ) ) { - return $network_check; - } + if ( ! $this->site_in_network( $site ) ) { + return new WP_Error( 'rest_unable_read_from_network', __( 'Sorry, you are not allowed to view sites on another network.' ), array( 'status' => rest_authorization_required_code() ) ); } $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; @@ -447,42 +430,29 @@ public function get_item_permissions_check( $request ) { } /** - * Validates that the given network IDs exist and are accessible to the current user. - * - * A user can always access the current network. Access to any other - * network requires the user to be a super admin of that network. + * Checks whether a site belongs to the current network. * * @since 7.2.0 * - * @param int[] $network_ids Array of network IDs. - * @return true|WP_Error True if all network IDs exist and are accessible, WP_Error otherwise. + * @param WP_Site $site Site object. + * @return bool Whether the site is on the current network. */ - protected function check_network_ids( array $network_ids ) { - $current_network_id = get_current_network_id(); - $user_id = get_current_user_id(); - - _prime_network_caches( $network_ids ); - foreach ( $network_ids as $network_id ) { - if ( ! get_network( $network_id ) ) { - return new WP_Error( 'rest_network_id_invalid', __( 'Invalid network ID.' ), array( 'status' => 400 ) ); - } - } - - if ( count( $network_ids ) === 1 && $current_network_id === $network_ids[0] ) { - return true; - } - - foreach ( $network_ids as $network_id ) { - if ( (int) $network_id === $current_network_id ) { - continue; - } - - if ( ! is_super_admin( $user_id, $network_id ) ) { - return new WP_Error( 'rest_cannot_view_network', __( 'Sorry, you are not allowed to access sites on this network.' ), array( 'status' => rest_authorization_required_code() ) ); - } - } + protected function site_in_network( WP_Site $site ) { + $network_id = get_current_network_id(); - return true; + /** + * Filters whether a site is treated as belonging to the current network. + * + * Returning false blocks the site from being read, updated or deleted + * via the REST API. + * + * @since 7.2.0 + * + * @param bool $in_network Whether the site belongs to the current network. + * @param WP_Site $site The site being checked. + * @param int $network_id The current network ID. + */ + return (bool) apply_filters( 'rest_site_in_network', $network_id === $site->network_id, $site, $network_id ); } /** @@ -519,14 +489,6 @@ public function create_item_permissions_check( $request ) { return $multisite_support; } - $network_id = isset( $request['network'] ) ? (int) $request['network'] : get_current_network_id(); - if ( $network_id > 0 ) { - $network_check = $this->check_network_ids( (array) $network_id ); - if ( is_wp_error( $network_check ) ) { - return $network_check; - } - } - if ( ! current_user_can( 'create_sites' ) ) { return new WP_Error( 'rest_cannot_create', __( 'Sorry, you are not allowed to create sites.' ), array( 'status' => rest_authorization_required_code() ) ); } @@ -652,12 +614,8 @@ public function update_item_permissions_check( $request ) { return $site; } - $network_id = isset( $request['network'] ) ? (int) $request['network'] : $site->network_id; - if ( $network_id > 0 ) { - $network_check = $this->check_network_ids( (array) $network_id ); - if ( is_wp_error( $network_check ) ) { - return $network_check; - } + if ( ! $this->site_in_network( $site ) ) { + return new WP_Error( 'rest_unable_read_from_network', __( 'Sorry, you are not allowed to edit sites on another network.' ), array( 'status' => rest_authorization_required_code() ) ); } if ( ! $this->check_edit_permission() ) { @@ -755,11 +713,8 @@ public function delete_item_permissions_check( $request ) { return $site; } - if ( $site->network_id > 0 ) { - $network_check = $this->check_network_ids( (array) $site->network_id ); - if ( is_wp_error( $network_check ) ) { - return $network_check; - } + if ( ! $this->site_in_network( $site ) ) { + return new WP_Error( 'rest_unable_read_from_network', __( 'Sorry, you are not allowed to delete sites on another network.' ), array( 'status' => rest_authorization_required_code() ) ); } if ( ! $this->check_delete_permission( $site ) ) { @@ -1060,13 +1015,6 @@ protected function prepare_item_for_database( $request ) { } } - if ( isset( $request['network'] ) ) { - if ( ! get_network( $request['network'] ) ) { - return new WP_Error( 'rest_network_id_invalid', __( 'Invalid network ID.' ), array( 'status' => 400 ) ); - } - $prepared_site['network_id'] = (int) $request['network']; - } - if ( isset( $request['path'] ) ) { $prepared_site['path'] = $request['path']; } @@ -1265,7 +1213,7 @@ protected function check_url_is_available( $prepared_site, $request ) { $id = (int) $request['id']; $domain = isset( $prepared_site['domain'] ) ? $prepared_site['domain'] : ''; $path = isset( $prepared_site['path'] ) ? $prepared_site['path'] : '/'; - $network_id = isset( $prepared_site['network_id'] ) ? $prepared_site['network_id'] : get_current_network_id(); + $network_id = get_current_network_id(); if ( ! empty( $id ) ) { // Updating a site: fall back to the current values for anything the request left out. @@ -1281,9 +1229,7 @@ protected function check_url_is_available( $prepared_site, $request ) { if ( ! isset( $prepared_site['path'] ) ) { $path = $current_site->path; } - if ( ! isset( $prepared_site['network_id'] ) ) { - $network_id = (int) $current_site->network_id; - } + $network_id = (int) $current_site->network_id; } $existing_site_id = domain_exists( $domain, $path, $network_id ); @@ -1320,9 +1266,10 @@ public function get_item_schema() { 'readonly' => true, ), 'network' => array( - 'description' => __( 'The site\'s network ID. Default is the current network ID.' ), + 'description' => __( 'The site\'s network ID.' ), 'type' => 'integer', 'context' => array( 'view', 'edit', 'embed' ), + 'readonly' => true, ), 'domain' => array( 'description' => __( 'Site domain.' ), @@ -1546,7 +1493,6 @@ public function get_collection_params() { 'domain_length', 'path_length', 'site__in', - 'network__in', ), ); $query_params['user'] = array( @@ -1600,24 +1546,6 @@ public function get_collection_params() { 'format' => 'date-time', ); - $query_params['network'] = array( - 'default' => array(), - 'description' => __( 'Limit result set to sites of specific network IDs.' ), - 'type' => 'array', - 'items' => array( - 'type' => 'integer', - ), - ); - - $query_params['network_exclude'] = array( - 'default' => array(), - 'description' => __( 'Ensure result set excludes specific network IDs.' ), - 'type' => 'array', - 'items' => array( - 'type' => 'integer', - ), - ); - /** * Filter collection parameters for the sites controller. * diff --git a/tests/phpunit/tests/rest-api/wpRestSitesController.php b/tests/phpunit/tests/rest-api/wpRestSitesController.php index e66dcc33ad31f..2a81d6d43ea3f 100644 --- a/tests/phpunit/tests/rest-api/wpRestSitesController.php +++ b/tests/phpunit/tests/rest-api/wpRestSitesController.php @@ -107,7 +107,7 @@ public function test_get_items() { } /** - * Without an explicit network filter, only sites on the current network are returned. + * Only sites on the current network are returned. * * @ticket 40365 * @covers ::get_items @@ -482,44 +482,6 @@ public function test_create_item_rejects_an_existing_domain_and_path() { $this->assertErrorResponse( 'rest_site_taken', $response, 400 ); } - /** - * @ticket 40365 - * @covers ::create_item - * @group ms-required - */ - public function test_create_item_allows_an_existing_domain_and_path_on_a_different_network() { - wp_set_current_user( self::$superadmin_id ); - - self::factory()->blog->create( - array( - 'domain' => WP_TESTS_DOMAIN, - 'path' => '/shared/', - ) - ); - - $network_id = self::factory()->network->create( - array( - 'domain' => 'other-network.example.org', - 'path' => '/', - ) - ); - - $request = new WP_REST_Request( 'POST', '/wp/v2/sites' ); - $request->set_param( 'domain', WP_TESTS_DOMAIN ); - $request->set_param( 'path', '/shared/' ); - $request->set_param( 'network', $network_id ); - - $response = rest_get_server()->dispatch( $request ); - - $this->assertEquals( 201, $response->get_status() ); - - $data = $response->get_data(); - - $this->assertEquals( WP_TESTS_DOMAIN, $data['domain'] ); - $this->assertEquals( '/shared/', $data['path'] ); - $this->assertEquals( $network_id, $data['network'] ); - } - /** * @ticket 40365 * @covers ::update_item @@ -648,44 +610,6 @@ public function test_check_url_is_available_on_create( $candidate_domain, $candi } } - /** - * The same domain/path is available on a different network. - * - * @ticket 40365 - * @covers ::check_url_is_available - * @group ms-required - */ - public function test_check_url_is_available_allows_the_same_domain_and_path_on_a_different_network() { - self::factory()->blog->create( - array( - 'domain' => WP_TESTS_DOMAIN, - 'path' => '/check-url-shared/', - ) - ); - - $other_network_id = self::factory()->network->create( - array( - 'domain' => 'check-url-other-network.example', - 'path' => '/', - ) - ); - - $method = $this->get_reflective_method( 'check_url_is_available' ); - $request = new WP_REST_Request( 'POST', '/wp/v2/sites' ); - - $result = $method->invoke( - $this->endpoint, - array( - 'domain' => WP_TESTS_DOMAIN, - 'path' => '/check-url-shared/', - 'network_id' => $other_network_id, - ), - $request - ); - - $this->assertTrue( $result ); - } - /** * Data provider for test_check_url_is_available_on_update(). * @@ -1135,6 +1059,7 @@ public function test_get_item_schema() { $this->assertEqualSets( $expected, array_keys( $properties ) ); $this->assertTrue( $properties['id']['readonly'] ); + $this->assertTrue( $properties['network']['readonly'] ); $this->assertTrue( $properties['registered']['readonly'] ); $this->assertTrue( $properties['blogname']['readonly'] ); $this->assertEquals( 'boolean', $properties['public']['type'] ); @@ -1364,7 +1289,28 @@ public function test_update_item_does_not_accept_the_creation_fields() { $this->assertArrayNotHasKey( 'title', $args ); $this->assertArrayNotHasKey( 'user_id', $args ); + $this->assertArrayNotHasKey( 'network', $args ); + $this->assertArrayHasKey( 'domain', $args ); + } + + /** + * The network is read-only, so creating a site does not accept it either. + * + * @ticket 40365 + * @covers ::create_item + */ + public function test_create_item_does_not_accept_the_network_field() { + $routes = rest_get_server()->get_routes(); + $args = array(); + + foreach ( $routes['/wp/v2/sites'] as $handler ) { + if ( ! empty( $handler['methods']['POST'] ) ) { + $args = $handler['args']; + } + } + $this->assertArrayHasKey( 'domain', $args ); + $this->assertArrayNotHasKey( 'network', $args ); } /** @@ -1566,7 +1512,7 @@ public function test_get_items_are_ordered_ascending() { public function test_get_items_orderby_id_list_without_a_list() { wp_set_current_user( self::$superadmin_id ); - foreach ( array( 'site__in', 'network__in' ) as $orderby ) { + foreach ( array( 'site__in' ) as $orderby ) { $request = new WP_REST_Request( 'GET', '/wp/v2/sites' ); $request->set_param( 'orderby', $orderby ); @@ -2042,283 +1988,414 @@ public function test_get_items_filter_with_includes_user() { /** * @ticket 40365 - * @covers ::get_items_permissions_check - * @covers ::check_network_ids + * @covers ::update_item_permissions_check + * @covers ::site_in_network * @group ms-required */ - public function test_get_items_permissions_check_invalid_network_id() { - wp_set_current_user( self::factory()->user->create() ); + public function test_update_item_permissions_check_allows_the_sites_own_network_by_default() { + $blog_id = self::factory()->blog->create(); + wp_set_current_user( self::$superadmin_id ); - $request = new WP_REST_Request( 'GET', '/wp/v2/sites' ); - $request->set_param( 'network', REST_TESTS_IMPOSSIBLY_HIGH_NUMBER ); + $request = new WP_REST_Request( 'PUT', '/wp/v2/sites/' . $blog_id ); + $request->set_param( 'id', $blog_id ); $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_network_id_invalid', $response, 400 ); + $this->assertEquals( 200, $response->get_status() ); } /** * @ticket 40365 - * @covers ::get_items_permissions_check - * @covers ::check_network_ids + * @covers ::delete_item_permissions_check + * @covers ::site_in_network * @group ms-required */ - public function test_get_items_permissions_check_invalid_network_exclude_id() { - wp_set_current_user( self::factory()->user->create() ); + public function test_delete_item_permissions_check_invalid_network_id() { + $blog_id = self::factory()->blog->create(); - $request = new WP_REST_Request( 'GET', '/wp/v2/sites' ); - $request->set_param( 'network_exclude', REST_TESTS_IMPOSSIBLY_HIGH_NUMBER ); + // Orphan the site from its network to simulate an invalid stored network ID. + wp_update_site( $blog_id, array( 'network_id' => REST_TESTS_IMPOSSIBLY_HIGH_NUMBER ) ); + + wp_set_current_user( self::$superadmin_id ); + + $request = new WP_REST_Request( 'DELETE', '/wp/v2/sites/' . $blog_id ); + $request->set_param( 'id', $blog_id ); $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_network_id_invalid', $response, 400 ); + $this->assertErrorResponse( 'rest_unable_read_from_network', $response, 403 ); } /** * @ticket 40365 - * @covers ::get_items_permissions_check - * @covers ::check_network_ids + * @covers ::get_item_permissions_check + * @covers ::site_in_network * @group ms-required */ - public function test_get_items_permissions_check_denies_access_to_another_network() { - wp_set_current_user( self::factory()->user->create() ); - $network_id = self::factory()->network->create(); + public function test_get_item_permissions_check_allows_a_site_on_the_current_network() { + $blog_id = self::factory()->blog->create(); + $user_id = self::factory()->user->create(); + add_user_to_blog( $blog_id, $user_id, 'subscriber' ); + wp_set_current_user( $user_id ); - $request = new WP_REST_Request( 'GET', '/wp/v2/sites' ); - $request->set_param( 'network', $network_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/sites/' . $blog_id ); + $request->set_param( 'id', $blog_id ); $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_cannot_view_network', $response, 403 ); + $this->assertEquals( 200, $response->get_status() ); + } + + /** + * Creates a site on a network other than the current one. + * + * @return int[] The other network ID and the site ID. + */ + private function create_site_on_another_network() { + $network_id = self::factory()->network->create( + array( + 'domain' => 'other-network.example.org', + 'path' => '/', + ) + ); + + $blog_id = self::factory()->blog->create( + array( + 'domain' => 'other-network.example.org', + 'path' => '/elsewhere/', + 'network_id' => $network_id, + ) + ); + + return array( $network_id, $blog_id ); } /** * @ticket 40365 - * @covers ::get_items_permissions_check - * @covers ::check_network_ids + * @covers ::get_item_permissions_check + * @covers ::site_in_network * @group ms-required */ - public function test_get_items_permissions_check_allows_filtering_by_the_current_network() { - wp_set_current_user( self::factory()->user->create() ); - - $request = new WP_REST_Request( 'GET', '/wp/v2/sites' ); - $request->set_param( 'network', get_current_network_id() ); + public function test_get_item_on_another_network_is_forbidden() { + list( , $blog_id ) = $this->create_site_on_another_network(); + wp_set_current_user( self::$superadmin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/sites/' . $blog_id ); $response = rest_get_server()->dispatch( $request ); - // The user still lacks manage_sites, but the network filter itself is not the reason. - $this->assertErrorResponse( 'rest_forbidden_context', $response, 403 ); + $this->assertErrorResponse( 'rest_unable_read_from_network', $response, 403 ); } /** * @ticket 40365 - * @covers ::create_item_permissions_check - * @covers ::check_network_ids + * @covers ::update_item_permissions_check + * @covers ::site_in_network * @group ms-required */ - public function test_create_item_permissions_check_invalid_network_id() { - wp_set_current_user( self::factory()->user->create() ); + public function test_update_item_on_another_network_is_forbidden() { + list( , $blog_id ) = $this->create_site_on_another_network(); + wp_set_current_user( self::$superadmin_id ); - $request = new WP_REST_Request( 'POST', '/wp/v2/sites' ); - $request->set_param( 'domain', WP_TESTS_DOMAIN ); - $request->set_param( 'path', '/rejected/' ); - $request->set_param( 'network', REST_TESTS_IMPOSSIBLY_HIGH_NUMBER ); + $request = new WP_REST_Request( 'PUT', '/wp/v2/sites/' . $blog_id ); + $request->set_param( 'path', '/moved/' ); $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_network_id_invalid', $response, 400 ); + + $this->assertErrorResponse( 'rest_unable_read_from_network', $response, 403 ); + $this->assertSame( '/elsewhere/', get_site( $blog_id )->path, 'The site should be left unchanged.' ); } /** * @ticket 40365 - * @covers ::create_item_permissions_check - * @covers ::check_network_ids + * @covers ::delete_item_permissions_check + * @covers ::site_in_network * @group ms-required */ - public function test_create_item_permissions_check_denies_access_to_another_network() { - wp_set_current_user( self::factory()->user->create() ); - $network_id = self::factory()->network->create(); + public function test_delete_item_on_another_network_is_forbidden() { + list( , $blog_id ) = $this->create_site_on_another_network(); + wp_set_current_user( self::$superadmin_id ); - $request = new WP_REST_Request( 'POST', '/wp/v2/sites' ); - $request->set_param( 'domain', WP_TESTS_DOMAIN ); - $request->set_param( 'path', '/rejected/' ); - $request->set_param( 'network', $network_id ); + $request = new WP_REST_Request( 'DELETE', '/wp/v2/sites/' . $blog_id ); + $request->set_param( 'force', true ); $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_cannot_view_network', $response, 403 ); + + $this->assertErrorResponse( 'rest_unable_read_from_network', $response, 403 ); + $this->assertInstanceOf( 'WP_Site', get_site( $blog_id ), 'The site should still exist.' ); } /** * @ticket 40365 - * @covers ::create_item_permissions_check - * @covers ::check_network_ids + * @covers ::create_item * @group ms-required */ - public function test_create_item_permissions_check_allows_a_super_admin_of_the_target_network() { - $user = self::factory()->user->create_and_get(); - $network_id = self::factory()->network->create(); - update_network_option( $network_id, 'site_admins', array( $user->user_login ) ); - // create_sites is checked against the current network, so grant it there too. - grant_super_admin( $user->ID ); - wp_set_current_user( $user->ID ); + public function test_create_item_ignores_network_param() { + list( $network_id ) = $this->create_site_on_another_network(); + wp_set_current_user( self::$superadmin_id ); $request = new WP_REST_Request( 'POST', '/wp/v2/sites' ); + $request->set_param( 'domain', WP_TESTS_DOMAIN ); + $request->set_param( 'path', '/ignores-network/' ); $request->set_param( 'network', $network_id ); - $this->assertTrue( $this->endpoint->create_item_permissions_check( $request ) ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertEquals( 201, $response->get_status() ); + + $data = $response->get_data(); + + $this->assertSame( get_current_network_id(), $data['network'] ); + $this->assertSame( get_current_network_id(), get_site( $data['id'] )->network_id ); } /** * @ticket 40365 - * @covers ::update_item_permissions_check - * @covers ::check_network_ids + * @covers ::update_item * @group ms-required */ - public function test_update_item_permissions_check_invalid_network_id() { - $blog_id = self::factory()->blog->create(); + public function test_update_item_ignores_network_param() { + list( $network_id ) = $this->create_site_on_another_network(); + $blog_id = self::factory()->blog->create( array( 'path' => '/stays-put/' ) ); wp_set_current_user( self::$superadmin_id ); $request = new WP_REST_Request( 'PUT', '/wp/v2/sites/' . $blog_id ); - $request->set_param( 'id', $blog_id ); - $request->set_param( 'network', REST_TESTS_IMPOSSIBLY_HIGH_NUMBER ); + $request->set_param( 'network', $network_id ); $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_network_id_invalid', $response, 400 ); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertSame( get_current_network_id(), $response->get_data()['network'] ); + $this->assertSame( get_current_network_id(), get_site( $blog_id )->network_id ); } /** * @ticket 40365 - * @covers ::update_item_permissions_check - * @covers ::check_network_ids + * @covers ::get_items * @group ms-required */ - public function test_update_item_permissions_check_denies_moving_a_site_to_another_network() { - $blog_id = self::factory()->blog->create(); - $network_id = self::factory()->network->create(); - delete_network_option( $network_id, 'site_admins' ); + public function test_get_items_ignores_network_param() { + list( $network_id, $blog_id ) = $this->create_site_on_another_network(); wp_set_current_user( self::$superadmin_id ); - $request = new WP_REST_Request( 'PUT', '/wp/v2/sites/' . $blog_id ); - $request->set_param( 'id', $blog_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/sites' ); $request->set_param( 'network', $network_id ); $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_cannot_view_network', $response, 403 ); + + $this->assertEquals( 200, $response->get_status() ); + + $data = $response->get_data(); + + $this->assertNotEmpty( $data ); + $this->assertNotContains( $blog_id, wp_list_pluck( $data, 'id' ) ); + foreach ( $data as $site ) { + $this->assertSame( get_current_network_id(), $site['network'] ); + } + } + + /** + * Data provider for the single-site permission checks. + * + * @return array[] + */ + public function data_permission_checks_on_another_network() { + return array( + 'get' => array( 'GET', 'get_item_permissions_check' ), + 'update' => array( 'PUT', 'update_item_permissions_check' ), + 'delete' => array( 'DELETE', 'delete_item_permissions_check' ), + ); } /** + * Every permission check for a single site refuses a site on another network. + * * @ticket 40365 + * @covers ::get_item_permissions_check * @covers ::update_item_permissions_check - * @covers ::check_network_ids + * @covers ::delete_item_permissions_check + * @covers ::site_in_network * @group ms-required + * @dataProvider data_permission_checks_on_another_network + * + * @param string $method HTTP method. + * @param string $permission_check Permission check method name. */ - public function test_update_item_permissions_check_allows_the_sites_own_network_by_default() { - $blog_id = self::factory()->blog->create(); + public function test_permission_checks_block_a_site_on_another_network( $method, $permission_check ) { + list( , $blog_id ) = $this->create_site_on_another_network(); wp_set_current_user( self::$superadmin_id ); - $request = new WP_REST_Request( 'PUT', '/wp/v2/sites/' . $blog_id ); + $request = new WP_REST_Request( $method, '/wp/v2/sites/' . $blog_id ); $request->set_param( 'id', $blog_id ); - $response = rest_get_server()->dispatch( $request ); - $this->assertEquals( 200, $response->get_status() ); + $result = $this->endpoint->$permission_check( $request ); + + $this->assertWPError( $result ); + $this->assertSame( 'rest_unable_read_from_network', $result->get_error_code() ); + $this->assertSame( 403, $result->get_error_data()['status'] ); } /** + * The same permission checks pass for a site on the current network. + * * @ticket 40365 + * @covers ::get_item_permissions_check + * @covers ::update_item_permissions_check * @covers ::delete_item_permissions_check - * @covers ::check_network_ids + * @covers ::site_in_network * @group ms-required + * @dataProvider data_permission_checks_on_another_network + * + * @param string $method HTTP method. + * @param string $permission_check Permission check method name. */ - public function test_delete_item_permissions_check_invalid_network_id() { - $blog_id = self::factory()->blog->create(); - - // Orphan the site from its network to simulate an invalid stored network ID. - wp_update_site( $blog_id, array( 'network_id' => REST_TESTS_IMPOSSIBLY_HIGH_NUMBER ) ); - + public function test_permission_checks_allow_a_site_on_the_current_network( $method, $permission_check ) { + $blog_id = self::factory()->blog->create( array( 'path' => '/current-network/' ) ); wp_set_current_user( self::$superadmin_id ); - $request = new WP_REST_Request( 'DELETE', '/wp/v2/sites/' . $blog_id ); + $request = new WP_REST_Request( $method, '/wp/v2/sites/' . $blog_id ); $request->set_param( 'id', $blog_id ); - $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_network_id_invalid', $response, 400 ); + $this->assertTrue( $this->endpoint->$permission_check( $request ) ); } /** + * Even a super admin of the second network is blocked from reaching its sites + * through the current network, for every method. + * * @ticket 40365 + * @covers ::get_item_permissions_check + * @covers ::update_item_permissions_check * @covers ::delete_item_permissions_check - * @covers ::check_network_ids * @group ms-required */ - public function test_delete_item_permissions_check_denies_access_to_another_network() { - $network_id = self::factory()->network->create(); - delete_network_option( $network_id, 'site_admins' ); - $blog_id = self::factory()->blog->create( array( 'site_id' => $network_id ) ); + public function test_super_admin_of_the_other_network_is_blocked() { + list( $network_id, $blog_id ) = $this->create_site_on_another_network(); + + $user = self::factory()->user->create_and_get(); + grant_super_admin( $user->ID ); + update_network_option( $network_id, 'site_admins', array( $user->user_login ) ); + wp_set_current_user( $user->ID ); + + foreach ( array( 'GET', 'PUT', 'PATCH', 'DELETE' ) as $method ) { + $request = new WP_REST_Request( $method, '/wp/v2/sites/' . $blog_id ); + if ( 'GET' !== $method && 'DELETE' !== $method ) { + $request->set_param( 'path', '/moved/' ); + } + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_unable_read_from_network', $response, 403 ); + } + + $site = get_site( $blog_id ); + $this->assertInstanceOf( 'WP_Site', $site, 'The site should still exist.' ); + $this->assertSame( '/elsewhere/', $site->path, 'The site should be left unchanged.' ); + $this->assertSame( $network_id, $site->network_id, 'The site should stay on its network.' ); + } + + /** + * Including a site from another network by ID does not expose it. + * + * @ticket 40365 + * @covers ::get_items + * @group ms-required + */ + public function test_get_items_include_does_not_expose_another_network() { + list( , $blog_id ) = $this->create_site_on_another_network(); wp_set_current_user( self::$superadmin_id ); - $request = new WP_REST_Request( 'DELETE', '/wp/v2/sites/' . $blog_id ); - $request->set_param( 'id', $blog_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/sites' ); + $request->set_param( 'include', array( $blog_id ) ); $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_cannot_view_network', $response, 403 ); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertSame( array(), $response->get_data() ); + $this->assertSame( '0', $response->get_headers()['X-WP-Total'] ); } /** + * A super admin of the second network still creates sites on the current network. + * * @ticket 40365 - * @covers ::delete_item_permissions_check - * @covers ::check_network_ids + * @covers ::create_item + * @covers ::create_item_permissions_check * @group ms-required */ - public function test_delete_item_permissions_check_allows_a_super_admin_of_the_sites_network() { - $network_id = self::factory()->network->create(); - $user = self::factory()->user->create_and_get(); - update_network_option( $network_id, 'site_admins', array( $user->user_login ) ); - // delete_sites is checked against the current network, so grant it there too. - grant_super_admin( $user->ID ); - $blog_id = self::factory()->blog->create( array( 'site_id' => $network_id ) ); + public function test_create_item_by_admin_of_another_network_lands_on_the_current_network() { + list( $network_id ) = $this->create_site_on_another_network(); + $user = self::factory()->user->create_and_get(); + grant_super_admin( $user->ID ); + update_network_option( $network_id, 'site_admins', array( $user->user_login ) ); wp_set_current_user( $user->ID ); - $request = new WP_REST_Request( 'DELETE', '/wp/v2/sites/' . $blog_id ); - $request->set_param( 'id', $blog_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/sites' ); + $request->set_param( 'domain', 'other-network.example.org' ); + $request->set_param( 'path', '/elsewhere/' ); + $request->set_param( 'network', $network_id ); + + $response = rest_get_server()->dispatch( $request ); + + // The same domain and path exist on the other network, but not on this one. + $this->assertEquals( 201, $response->get_status() ); + + $data = $response->get_data(); - $this->assertTrue( $this->endpoint->delete_item_permissions_check( $request ) ); + $this->assertSame( get_current_network_id(), $data['network'] ); + $this->assertNotSame( $network_id, get_site( $data['id'] )->network_id ); } /** + * The rest_site_in_network filter can let a site on another network through. + * * @ticket 40365 - * @covers ::get_item_permissions_check - * @covers ::check_network_ids + * @covers ::site_in_network * @group ms-required */ - public function test_get_item_permissions_check_denies_a_site_on_another_network() { + public function test_site_in_network_filter_can_allow_another_network() { + list( $network_id, $blog_id ) = $this->create_site_on_another_network(); wp_set_current_user( self::$superadmin_id ); - $network_id = self::factory()->network->create(); - delete_network_option( $network_id, 'site_admins' ); - $blog_id = self::factory()->blog->create( array( 'site_id' => $network_id ) ); - wp_set_current_user( self::factory()->user->create() ); - - $request = new WP_REST_Request( 'GET', '/wp/v2/sites/' . $blog_id ); - $request->set_param( 'id', $blog_id ); + $filter_args = array(); + add_filter( + 'rest_site_in_network', + static function ( $in_network, $site, $current_network_id ) use ( &$filter_args ) { + $filter_args = array( $in_network, $site, $current_network_id ); + return true; + }, + 10, + 3 + ); + $request = new WP_REST_Request( 'GET', '/wp/v2/sites/' . $blog_id ); $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_cannot_view_network', $response, 403 ); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertSame( $network_id, $response->get_data()['network'] ); + + list( $in_network, $site, $current_network_id ) = $filter_args; + $this->assertFalse( $in_network, 'The unfiltered value should be false for another network.' ); + $this->assertInstanceOf( 'WP_Site', $site ); + $this->assertSame( $blog_id, (int) $site->blog_id ); + $this->assertSame( get_current_network_id(), $current_network_id ); } /** + * The rest_site_in_network filter can block a site on the current network. + * * @ticket 40365 - * @covers ::get_item_permissions_check - * @covers ::check_network_ids + * @covers ::site_in_network * @group ms-required */ - public function test_get_item_permissions_check_allows_a_site_on_the_current_network() { - $blog_id = self::factory()->blog->create(); - $user_id = self::factory()->user->create(); - add_user_to_blog( $blog_id, $user_id, 'subscriber' ); - wp_set_current_user( $user_id ); + public function test_site_in_network_filter_can_block_the_current_network() { + $blog_id = self::factory()->blog->create( array( 'path' => '/filtered-out/' ) ); + wp_set_current_user( self::$superadmin_id ); - $request = new WP_REST_Request( 'GET', '/wp/v2/sites/' . $blog_id ); - $request->set_param( 'id', $blog_id ); + add_filter( 'rest_site_in_network', '__return_false' ); - $response = rest_get_server()->dispatch( $request ); - $this->assertEquals( 200, $response->get_status() ); + foreach ( array( 'GET', 'PUT', 'DELETE' ) as $method ) { + $request = new WP_REST_Request( $method, '/wp/v2/sites/' . $blog_id ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_unable_read_from_network', $response, 403 ); + } } } diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index 99580f63b4e27..b22ff76829e1c 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -10998,8 +10998,7 @@ mockedApiResponse.Schema = { "registered", "domain_length", "path_length", - "site__in", - "network__in" + "site__in" ], "required": false }, @@ -11062,24 +11061,6 @@ mockedApiResponse.Schema = { "type": "string", "format": "date-time", "required": false - }, - "network": { - "default": [], - "description": "Limit result set to sites of specific network IDs.", - "type": "array", - "items": { - "type": "integer" - }, - "required": false - }, - "network_exclude": { - "default": [], - "description": "Ensure result set excludes specific network IDs.", - "type": "array", - "items": { - "type": "integer" - }, - "required": false } } }, @@ -11088,11 +11069,6 @@ mockedApiResponse.Schema = { "POST" ], "args": { - "network": { - "description": "The site's network ID. Default is the current network ID.", - "type": "integer", - "required": false - }, "domain": { "default": "", "description": "Site domain.", @@ -11207,11 +11183,6 @@ mockedApiResponse.Schema = { "type": "integer", "required": false }, - "network": { - "description": "The site's network ID. Default is the current network ID.", - "type": "integer", - "required": false - }, "domain": { "description": "Site domain.", "type": "string", From 9b4c6db7750de10c8036b91c421098b4eea4a512 Mon Sep 17 00:00:00 2001 From: Jonny Harrus Date: Mon, 5 Oct 2026 13:01:32 +0100 Subject: [PATCH 2/3] REST API: Use operation-specific error codes for cross-network site checks. The update and delete permission checks now return `rest_unable_update_from_network` and `rest_unable_delete_from_network` respectively, instead of `rest_unable_read_from_network`, for a site that is not on the current network. Tests now assert the code for each operation. See #40365. Co-Authored-By: Claude Opus 5.5 --- .../class-wp-rest-sites-controller.php | 4 +- .../tests/rest-api/wpRestSitesController.php | 38 +++++++++++++------ 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-sites-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-sites-controller.php index ed230df32902d..e431f394492b7 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-sites-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-sites-controller.php @@ -615,7 +615,7 @@ public function update_item_permissions_check( $request ) { } if ( ! $this->site_in_network( $site ) ) { - return new WP_Error( 'rest_unable_read_from_network', __( 'Sorry, you are not allowed to edit sites on another network.' ), array( 'status' => rest_authorization_required_code() ) ); + return new WP_Error( 'rest_unable_update_from_network', __( 'Sorry, you are not allowed to edit sites on another network.' ), array( 'status' => rest_authorization_required_code() ) ); } if ( ! $this->check_edit_permission() ) { @@ -714,7 +714,7 @@ public function delete_item_permissions_check( $request ) { } if ( ! $this->site_in_network( $site ) ) { - return new WP_Error( 'rest_unable_read_from_network', __( 'Sorry, you are not allowed to delete sites on another network.' ), array( 'status' => rest_authorization_required_code() ) ); + return new WP_Error( 'rest_unable_delete_from_network', __( 'Sorry, you are not allowed to delete sites on another network.' ), array( 'status' => rest_authorization_required_code() ) ); } if ( ! $this->check_delete_permission( $site ) ) { diff --git a/tests/phpunit/tests/rest-api/wpRestSitesController.php b/tests/phpunit/tests/rest-api/wpRestSitesController.php index 2a81d6d43ea3f..7c6855a813445 100644 --- a/tests/phpunit/tests/rest-api/wpRestSitesController.php +++ b/tests/phpunit/tests/rest-api/wpRestSitesController.php @@ -2021,7 +2021,7 @@ public function test_delete_item_permissions_check_invalid_network_id() { $request->set_param( 'id', $blog_id ); $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_unable_read_from_network', $response, 403 ); + $this->assertErrorResponse( 'rest_unable_delete_from_network', $response, 403 ); } /** @@ -2098,7 +2098,7 @@ public function test_update_item_on_another_network_is_forbidden() { $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_unable_read_from_network', $response, 403 ); + $this->assertErrorResponse( 'rest_unable_update_from_network', $response, 403 ); $this->assertSame( '/elsewhere/', get_site( $blog_id )->path, 'The site should be left unchanged.' ); } @@ -2117,7 +2117,7 @@ public function test_delete_item_on_another_network_is_forbidden() { $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_unable_read_from_network', $response, 403 ); + $this->assertErrorResponse( 'rest_unable_delete_from_network', $response, 403 ); $this->assertInstanceOf( 'WP_Site', get_site( $blog_id ), 'The site should still exist.' ); } @@ -2197,9 +2197,9 @@ public function test_get_items_ignores_network_param() { */ public function data_permission_checks_on_another_network() { return array( - 'get' => array( 'GET', 'get_item_permissions_check' ), - 'update' => array( 'PUT', 'update_item_permissions_check' ), - 'delete' => array( 'DELETE', 'delete_item_permissions_check' ), + 'get' => array( 'GET', 'get_item_permissions_check', 'rest_unable_read_from_network' ), + 'update' => array( 'PUT', 'update_item_permissions_check', 'rest_unable_update_from_network' ), + 'delete' => array( 'DELETE', 'delete_item_permissions_check', 'rest_unable_delete_from_network' ), ); } @@ -2216,8 +2216,9 @@ public function data_permission_checks_on_another_network() { * * @param string $method HTTP method. * @param string $permission_check Permission check method name. + * @param string $error_code Expected error code. */ - public function test_permission_checks_block_a_site_on_another_network( $method, $permission_check ) { + public function test_permission_checks_block_a_site_on_another_network( $method, $permission_check, $error_code ) { list( , $blog_id ) = $this->create_site_on_another_network(); wp_set_current_user( self::$superadmin_id ); @@ -2227,7 +2228,7 @@ public function test_permission_checks_block_a_site_on_another_network( $method, $result = $this->endpoint->$permission_check( $request ); $this->assertWPError( $result ); - $this->assertSame( 'rest_unable_read_from_network', $result->get_error_code() ); + $this->assertSame( $error_code, $result->get_error_code() ); $this->assertSame( 403, $result->get_error_data()['status'] ); } @@ -2273,7 +2274,14 @@ public function test_super_admin_of_the_other_network_is_blocked() { update_network_option( $network_id, 'site_admins', array( $user->user_login ) ); wp_set_current_user( $user->ID ); - foreach ( array( 'GET', 'PUT', 'PATCH', 'DELETE' ) as $method ) { + $error_codes = array( + 'GET' => 'rest_unable_read_from_network', + 'PUT' => 'rest_unable_update_from_network', + 'PATCH' => 'rest_unable_update_from_network', + 'DELETE' => 'rest_unable_delete_from_network', + ); + + foreach ( $error_codes as $method => $error_code ) { $request = new WP_REST_Request( $method, '/wp/v2/sites/' . $blog_id ); if ( 'GET' !== $method && 'DELETE' !== $method ) { $request->set_param( 'path', '/moved/' ); @@ -2281,7 +2289,7 @@ public function test_super_admin_of_the_other_network_is_blocked() { $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_unable_read_from_network', $response, 403 ); + $this->assertErrorResponse( $error_code, $response, 403 ); } $site = get_site( $blog_id ); @@ -2391,11 +2399,17 @@ public function test_site_in_network_filter_can_block_the_current_network() { add_filter( 'rest_site_in_network', '__return_false' ); - foreach ( array( 'GET', 'PUT', 'DELETE' ) as $method ) { + $error_codes = array( + 'GET' => 'rest_unable_read_from_network', + 'PUT' => 'rest_unable_update_from_network', + 'DELETE' => 'rest_unable_delete_from_network', + ); + + foreach ( $error_codes as $method => $error_code ) { $request = new WP_REST_Request( $method, '/wp/v2/sites/' . $blog_id ); $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_unable_read_from_network', $response, 403 ); + $this->assertErrorResponse( $error_code, $response, 403 ); } } } From 94133a0a73d01825348acc0aaace4886505f9961 Mon Sep 17 00:00:00 2001 From: Jonny Harrus Date: Mon, 5 Oct 2026 13:08:25 +0100 Subject: [PATCH 3/3] Users: Revert the network ID parameter for super admin checks. The sites endpoint no longer checks super admin status per network, so the `$network_id` parameter added to get_super_admins() and is_super_admin(), and the tests covering it, are no longer needed. Restore both functions and tests/phpunit/tests/user/capabilities.php to their trunk versions. See #40365. Co-Authored-By: Claude Opus 5.5 --- src/wp-includes/capabilities.php | 30 +----- tests/phpunit/tests/user/capabilities.php | 106 ---------------------- 2 files changed, 5 insertions(+), 131 deletions(-) diff --git a/src/wp-includes/capabilities.php b/src/wp-includes/capabilities.php index 8a9eb293ba268..6f8b57d50b348 100644 --- a/src/wp-includes/capabilities.php +++ b/src/wp-includes/capabilities.php @@ -1155,46 +1155,26 @@ function remove_role( $role ) { * Retrieves a list of super admins. * * @since 3.0.0 - * @since 7.2.0 Added the `$network_id` parameter. * * @global array $super_admins * - * @param int|null $network_id Optional. ID of the network. Default is the current network. * @return string[] List of super admin logins. */ -function get_super_admins( $network_id = null ) { +function get_super_admins() { global $super_admins; - // Falsy values (including 0) mean "unspecified", matching get_network_option()'s own fallback. - $is_current_network = ! $network_id || get_current_network_id() === (int) $network_id; - - if ( $is_current_network && isset( $super_admins ) ) { - return $super_admins; - } - - /* - * The 'admin' fallback exists for the current network only, where it reflects the - * install's original default super admin. A network whose `site_admins` option was - * never explicitly set has no default super admins - assuming 'admin' there would let - * any user with that login be treated as a super admin of a network they were never - * granted access to. - */ - $default = $is_current_network ? array( 'admin' ) : array(); - - return get_network_option( $network_id, 'site_admins', $default ); + return $super_admins ?? get_site_option( 'site_admins', array( 'admin' ) ); } /** * Determines whether user is a site admin. * * @since 3.0.0 - * @since 7.2.0 Added the `$network_id` parameter. * - * @param int|false $user_id Optional. The ID of a user. Defaults to false, to check the current user. - * @param int|null $network_id Optional. ID of the network. Default is the current network. + * @param int|false $user_id Optional. The ID of a user. Defaults to false, to check the current user. * @return bool Whether the user is a site admin. */ -function is_super_admin( $user_id = false, $network_id = null ) { +function is_super_admin( $user_id = false ) { if ( ! $user_id ) { $user = wp_get_current_user(); } else { @@ -1206,7 +1186,7 @@ function is_super_admin( $user_id = false, $network_id = null ) { } if ( is_multisite() ) { - $super_admins = get_super_admins( $network_id ); + $super_admins = get_super_admins(); if ( is_array( $super_admins ) && in_array( $user->user_login, $super_admins, true ) ) { return true; } diff --git a/tests/phpunit/tests/user/capabilities.php b/tests/phpunit/tests/user/capabilities.php index 7d7bc597dae25..34f46dfb1eec2 100644 --- a/tests/phpunit/tests/user/capabilities.php +++ b/tests/phpunit/tests/user/capabilities.php @@ -2590,110 +2590,4 @@ public function test_role_capabilities_updated_correctly_via_update_option() { $this->assertSameSetsWithIndex( $emcee_caps, $sally_caps, 'Emcee and Sally roles should have the same capabilities after update.' ); $this->assertLessThan( $emcee_queries, $sally_queries, 'Updating roles via update_option should be more efficient than WP_Roles using the database.' ); } - - /** - * @ticket 40365 - * @group ms-required - * - * @covers ::get_super_admins - */ - public function test_get_super_admins_with_explicit_current_network_id_matches_default() { - $this->assertSameSets( get_super_admins(), get_super_admins( get_current_network_id() ) ); - } - - /** - * @ticket 40365 - * @group ms-required - * - * @covers ::get_super_admins - */ - public function test_get_super_admins_for_another_network_returns_that_networks_site_admins() { - $network_id = self::factory()->network->create(); - update_network_option( $network_id, 'site_admins', array( 'someone-else' ) ); - - $this->assertSameSets( array( 'someone-else' ), get_super_admins( $network_id ) ); - $this->assertNotSame( get_super_admins(), get_super_admins( $network_id ) ); - } - - /** - * @ticket 40365 - * @group ms-required - * - * @covers ::get_super_admins - */ - public function test_get_super_admins_for_another_network_without_a_site_admins_option_is_empty() { - $network_id = self::factory()->network->create(); - delete_network_option( $network_id, 'site_admins' ); - - // Unlike the current network, a network with no `site_admins` option has no implicit 'admin' fallback. - $this->assertSame( array(), get_super_admins( $network_id ) ); - } - - /** - * @ticket 40365 - * @group ms-required - * - * @covers ::get_super_admins - */ - public function test_get_super_admins_global_override_does_not_apply_to_other_networks() { - $network_id = self::factory()->network->create(); - $had_global_override = array_key_exists( 'super_admins', $GLOBALS ); - $old_super_admins = $had_global_override ? $GLOBALS['super_admins'] : null; - delete_network_option( $network_id, 'site_admins' ); - update_network_option( $network_id, 'site_admins', array( 'network-admin' ) ); - - $GLOBALS['super_admins'] = array( 'global-override' ); - - $this->assertSameSets( array( 'global-override' ), get_super_admins() ); - $this->assertSameSets( array( 'network-admin' ), get_super_admins( $network_id ) ); - - if ( $had_global_override ) { - $GLOBALS['super_admins'] = $old_super_admins; - } else { - unset( $GLOBALS['super_admins'] ); - } - } - - /** - * @ticket 40365 - * @group ms-required - * - * @covers ::is_super_admin - */ - public function test_is_super_admin_defaults_to_the_current_network() { - $user = self::$super_admin; - - $this->assertTrue( is_super_admin( $user->ID ) ); - $this->assertTrue( is_super_admin( $user->ID, get_current_network_id() ) ); - } - - /** - * @ticket 40365 - * @group ms-required - * - * @covers ::is_super_admin - */ - public function test_is_super_admin_for_a_network_the_user_is_not_a_super_admin_of() { - $user = self::$super_admin; - $network_id = self::factory()->network->create(); - delete_network_option( $network_id, 'site_admins' ); - - $this->assertTrue( is_super_admin( $user->ID ), 'User is expected to be a super admin of the current network.' ); - $this->assertFalse( is_super_admin( $user->ID, $network_id ), 'User is not expected to be a super admin of an unrelated network.' ); - } - - /** - * @ticket 40365 - * @group ms-required - * - * @covers ::is_super_admin - */ - public function test_is_super_admin_for_a_network_the_user_is_a_super_admin_of() { - $user = self::factory()->user->create_and_get(); - $network_id = self::factory()->network->create(); - update_network_option( $network_id, 'site_admins', array( $user->user_login ) ); - - $this->assertFalse( is_super_admin( $user->ID ), 'User is not expected to be a super admin of the current network.' ); - $this->assertTrue( is_super_admin( $user->ID, $network_id ), 'User is expected to be a super admin of the network granting them access.' ); - } }