diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-sidebars-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-sidebars-controller.php index 2aad57ad54..fbc70fe331 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-sidebars-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-sidebars-controller.php @@ -18,6 +18,14 @@ */ class WP_REST_Sidebars_Controller extends WP_REST_Controller { + /** + * Tracks whether {@see retrieve_widgets()} has been called in the current request. + * + * @since 5.9.0 + * @var bool + */ + protected $widgets_retrieved = false; + /** * Sidebars controller constructor. * @@ -86,6 +94,19 @@ class WP_REST_Sidebars_Controller extends WP_REST_Controller { * @return true|WP_Error True if the request has read access, WP_Error object otherwise. */ public function get_items_permissions_check( $request ) { + $this->retrieve_widgets(); + foreach ( wp_get_sidebars_widgets() as $id => $widgets ) { + $sidebar = $this->get_sidebar( $id ); + + if ( ! $sidebar ) { + continue; + } + + if ( $this->check_read_permission( $sidebar ) ) { + return true; + } + } + return $this->do_permissions_check(); } @@ -95,12 +116,14 @@ class WP_REST_Sidebars_Controller extends WP_REST_Controller { * @since 5.8.0 * * @param WP_REST_Request $request Full details about the request. - * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + * @return WP_REST_Response Response object on success. */ public function get_items( $request ) { - retrieve_widgets(); + $this->retrieve_widgets(); + + $data = array(); + $permissions_check = $this->do_permissions_check(); - $data = array(); foreach ( wp_get_sidebars_widgets() as $id => $widgets ) { $sidebar = $this->get_sidebar( $id ); @@ -108,6 +131,10 @@ class WP_REST_Sidebars_Controller extends WP_REST_Controller { continue; } + if ( is_wp_error( $permissions_check ) && ! $this->check_read_permission( $sidebar ) ) { + continue; + } + $data[] = $this->prepare_response_for_collection( $this->prepare_item_for_response( $sidebar, $request ) ); @@ -125,9 +152,28 @@ class WP_REST_Sidebars_Controller extends WP_REST_Controller { * @return true|WP_Error True if the request has read access, WP_Error object otherwise. */ public function get_item_permissions_check( $request ) { + $this->retrieve_widgets(); + + $sidebar = $this->get_sidebar( $request['id'] ); + if ( $sidebar && $this->check_read_permission( $sidebar ) ) { + return true; + } + return $this->do_permissions_check(); } + /** + * Checks if a sidebar can be read publicly. + * + * @since 5.9.0 + * + * @param array $sidebar The registered sidebar configuration. + * @return bool Whether the side can be read. + */ + protected function check_read_permission( $sidebar ) { + return ! empty( $sidebar['show_in_rest'] ); + } + /** * Retrieves one sidebar from the collection. * @@ -137,10 +183,9 @@ class WP_REST_Sidebars_Controller extends WP_REST_Controller { * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function get_item( $request ) { - retrieve_widgets(); + $this->retrieve_widgets(); $sidebar = $this->get_sidebar( $request['id'] ); - if ( ! $sidebar ) { return new WP_Error( 'rest_sidebar_not_found', __( 'No sidebar exists with that id.' ), array( 'status' => 404 ) ); } @@ -234,28 +279,25 @@ class WP_REST_Sidebars_Controller extends WP_REST_Controller { * * @since 5.8.0 * - * @global array $wp_registered_sidebars The registered sidebars. - * * @param string|int $id ID of the sidebar. * @return array|null The discovered sidebar, or null if it is not registered. */ protected function get_sidebar( $id ) { - global $wp_registered_sidebars; + return wp_get_sidebar( $id ); + } - foreach ( (array) $wp_registered_sidebars as $sidebar ) { - if ( $sidebar['id'] === $id ) { - return $sidebar; - } + /** + * Looks for "lost" widgets once per request. + * + * @since 5.9.0 + * + * @see retrieve_widgets() + */ + protected function retrieve_widgets() { + if ( ! $this->widgets_retrieved ) { + retrieve_widgets(); + $this->widgets_retrieved = true; } - - if ( 'wp_inactive_widgets' === $id ) { - return array( - 'id' => 'wp_inactive_widgets', - 'name' => __( 'Inactive widgets' ), - ); - } - - return null; } /** diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-widgets-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-widgets-controller.php index 4c54620f50..6385be8ef8 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-widgets-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-widgets-controller.php @@ -16,6 +16,14 @@ */ class WP_REST_Widgets_Controller extends WP_REST_Controller { + /** + * Tracks whether {@see retrieve_widgets()} has been called in the current request. + * + * @since 5.9.0 + * @var bool + */ + protected $widgets_retrieved = false; + /** * Widgets controller constructor. * @@ -97,6 +105,17 @@ class WP_REST_Widgets_Controller extends WP_REST_Controller { * @return true|WP_Error True if the request has read access, WP_Error object otherwise. */ public function get_items_permissions_check( $request ) { + $this->retrieve_widgets(); + if ( isset( $request['sidebar'] ) && $this->check_read_sidebar_permission( $request['sidebar'] ) ) { + return true; + } + + foreach ( wp_get_sidebars_widgets() as $sidebar_id => $widget_ids ) { + if ( $this->check_read_sidebar_permission( $sidebar_id ) ) { + return true; + } + } + return $this->permissions_check( $request ); } @@ -109,15 +128,20 @@ class WP_REST_Widgets_Controller extends WP_REST_Controller { * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function get_items( $request ) { - retrieve_widgets(); + $this->retrieve_widgets(); - $prepared = array(); + $prepared = array(); + $permissions_check = $this->permissions_check( $request ); foreach ( wp_get_sidebars_widgets() as $sidebar_id => $widget_ids ) { if ( isset( $request['sidebar'] ) && $sidebar_id !== $request['sidebar'] ) { continue; } + if ( is_wp_error( $permissions_check ) && ! $this->check_read_sidebar_permission( $sidebar_id ) ) { + continue; + } + foreach ( $widget_ids as $widget_id ) { $response = $this->prepare_item_for_response( compact( 'sidebar_id', 'widget_id' ), $request ); @@ -139,9 +163,32 @@ class WP_REST_Widgets_Controller extends WP_REST_Controller { * @return true|WP_Error True if the request has read access, WP_Error object otherwise. */ public function get_item_permissions_check( $request ) { + $this->retrieve_widgets(); + + $widget_id = $request['id']; + $sidebar_id = wp_find_widgets_sidebar( $widget_id ); + + if ( $sidebar_id && $this->check_read_sidebar_permission( $sidebar_id ) ) { + return true; + } + return $this->permissions_check( $request ); } + /** + * Checks if a sidebar can be read publicly. + * + * @since 5.9.0 + * + * @param string $sidebar_id The sidebar id. + * @return bool Whether the sidebar can be read. + */ + protected function check_read_sidebar_permission( $sidebar_id ) { + $sidebar = wp_get_sidebar( $sidebar_id ); + + return ! empty( $sidebar['show_in_rest'] ); + } + /** * Gets an individual widget. * @@ -151,7 +198,7 @@ class WP_REST_Widgets_Controller extends WP_REST_Controller { * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function get_item( $request ) { - retrieve_widgets(); + $this->retrieve_widgets(); $widget_id = $request['id']; $sidebar_id = wp_find_widgets_sidebar( $widget_id ); @@ -247,8 +294,7 @@ class WP_REST_Widgets_Controller extends WP_REST_Controller { * See https://core.trac.wordpress.org/ticket/53657. */ wp_get_sidebars_widgets(); - - retrieve_widgets(); + $this->retrieve_widgets(); $widget_id = $request['id']; $sidebar_id = wp_find_widgets_sidebar( $widget_id ); @@ -323,8 +369,7 @@ class WP_REST_Widgets_Controller extends WP_REST_Controller { * See https://core.trac.wordpress.org/ticket/53657. */ wp_get_sidebars_widgets(); - - retrieve_widgets(); + $this->retrieve_widgets(); $widget_id = $request['id']; $sidebar_id = wp_find_widgets_sidebar( $widget_id ); @@ -439,6 +484,20 @@ class WP_REST_Widgets_Controller extends WP_REST_Controller { return true; } + /** + * Looks for "lost" widgets once per request. + * + * @since 5.9.0 + * + * @see retrieve_widgets() + */ + protected function retrieve_widgets() { + if ( ! $this->widgets_retrieved ) { + retrieve_widgets(); + $this->widgets_retrieved = true; + } + } + /** * Saves the widget in the request object. * @@ -767,23 +826,23 @@ class WP_REST_Widgets_Controller extends WP_REST_Controller { 'instance' => array( 'description' => __( 'Instance settings of the widget, if supported.' ), 'type' => 'object', - 'context' => array( 'view', 'edit', 'embed' ), + 'context' => array( 'edit' ), 'default' => null, 'properties' => array( 'encoded' => array( 'description' => __( 'Base64 encoded representation of the instance settings.' ), 'type' => 'string', - 'context' => array( 'view', 'edit', 'embed' ), + 'context' => array( 'edit' ), ), 'hash' => array( 'description' => __( 'Cryptographic hash of the instance settings.' ), 'type' => 'string', - 'context' => array( 'view', 'edit', 'embed' ), + 'context' => array( 'edit' ), ), 'raw' => array( 'description' => __( 'Unencoded instance settings, if supported.' ), 'type' => 'object', - 'context' => array( 'view', 'edit', 'embed' ), + 'context' => array( 'edit' ), ), ), ), diff --git a/src/wp-includes/widgets.php b/src/wp-includes/widgets.php index de15317d1b..56087c32f3 100644 --- a/src/wp-includes/widgets.php +++ b/src/wp-includes/widgets.php @@ -220,6 +220,7 @@ function register_sidebars( $number = 1, $args = array() ) { * * @since 2.2.0 * @since 5.6.0 Added the `before_sidebar` and `after_sidebar` arguments. + * @since 5.9.0 Added the `show_in_rest` argument. * * @global array $wp_registered_sidebars Registered sidebars. * @@ -250,6 +251,8 @@ function register_sidebars( $number = 1, $args = array() ) { * @type string $after_sidebar HTML content to append to the sidebar when displayed. * Outputs before the {@see 'dynamic_sidebar_after'} action. * Default empty string. + * @type bool $show_in_rest Whether to show this sidebar publicly in the REST API. + * Defaults to only showing the sidebar to administrator users. * } * @return string Sidebar ID added to $wp_registered_sidebars global. */ @@ -272,6 +275,7 @@ function register_sidebar( $args = array() ) { 'after_title' => "\n", 'before_sidebar' => '', 'after_sidebar' => '', + 'show_in_rest' => false, ); /** @@ -1035,6 +1039,35 @@ function wp_get_sidebars_widgets( $deprecated = true ) { return apply_filters( 'sidebars_widgets', $sidebars_widgets ); } +/** + * Retrieves the registered sidebar with the given id. + * + * @since 5.9.0 + * + * @global array $wp_registered_sidebars The registered sidebars. + * + * @param string $id The sidebar id. + * @return array|null The discovered sidebar, or null if it is not registered. + */ +function wp_get_sidebar( $id ) { + global $wp_registered_sidebars; + + foreach ( (array) $wp_registered_sidebars as $sidebar ) { + if ( $sidebar['id'] === $id ) { + return $sidebar; + } + } + + if ( 'wp_inactive_widgets' === $id ) { + return array( + 'id' => 'wp_inactive_widgets', + 'name' => __( 'Inactive widgets' ), + ); + } + + return null; +} + /** * Set the sidebar widget option to update sidebars. * diff --git a/tests/phpunit/tests/rest-api/rest-sidebars-controller.php b/tests/phpunit/tests/rest-api/rest-sidebars-controller.php index 850210cc20..91bdeeb342 100644 --- a/tests/phpunit/tests/rest-api/rest-sidebars-controller.php +++ b/tests/phpunit/tests/rest-api/rest-sidebars-controller.php @@ -164,6 +164,102 @@ class WP_Test_REST_Sidebars_Controller extends WP_Test_REST_Controller_Testcase $this->assertErrorResponse( 'rest_cannot_manage_widgets', $response, 401 ); } + /** + * @ticket 53915 + */ + public function test_get_items_no_permission_show_in_rest() { + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Test sidebar', + 'show_in_rest' => true, + ) + ); + wp_set_current_user( 0 ); + $request = new WP_REST_Request( 'GET', '/wp/v2/sidebars' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $data = $this->remove_links( $data ); + $this->assertSame( + array( + array( + 'id' => 'sidebar-1', + 'name' => 'Test sidebar', + 'description' => '', + 'class' => '', + 'before_widget' => '', + 'after_widget' => '', + 'before_title' => '', + 'after_title' => '', + 'status' => 'active', + 'widgets' => array(), + ), + ), + $data + ); + } + + /** + * @ticket 53915 + */ + public function test_get_items_without_show_in_rest_are_removed_from_the_list() { + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Test sidebar 1', + 'show_in_rest' => true, + ) + ); + $this->setup_sidebar( + 'sidebar-2', + array( + 'name' => 'Test sidebar 2', + 'show_in_rest' => false, + ) + ); + $this->setup_sidebar( + 'sidebar-3', + array( + 'name' => 'Test sidebar 3', + 'show_in_rest' => true, + ) + ); + wp_set_current_user( self::$author_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/sidebars' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $data = $this->remove_links( $data ); + $this->assertSame( + array( + array( + 'id' => 'sidebar-1', + 'name' => 'Test sidebar 1', + 'description' => '', + 'class' => '', + 'before_widget' => '', + 'after_widget' => '', + 'before_title' => '', + 'after_title' => '', + 'status' => 'active', + 'widgets' => array(), + ), + array( + 'id' => 'sidebar-3', + 'name' => 'Test sidebar 3', + 'description' => '', + 'class' => '', + 'before_widget' => '', + 'after_widget' => '', + 'before_title' => '', + 'after_title' => '', + 'status' => 'active', + 'widgets' => array(), + ), + ), + $data + ); + } + /** * @ticket 41683 */ @@ -191,6 +287,18 @@ class WP_Test_REST_Sidebars_Controller extends WP_Test_REST_Controller_Testcase $data = $this->remove_links( $data ); $this->assertSame( array( + array( + 'id' => 'wp_inactive_widgets', + 'name' => 'Inactive widgets', + 'description' => '', + 'class' => '', + 'before_widget' => '', + 'after_widget' => '', + 'before_title' => '', + 'after_title' => '', + 'status' => 'inactive', + 'widgets' => array(), + ), array( 'id' => 'sidebar-1', 'name' => 'Test sidebar', @@ -206,6 +314,7 @@ class WP_Test_REST_Sidebars_Controller extends WP_Test_REST_Controller_Testcase ), $data ); + } /** @@ -412,6 +521,40 @@ class WP_Test_REST_Sidebars_Controller extends WP_Test_REST_Controller_Testcase $this->assertErrorResponse( 'rest_cannot_manage_widgets', $response, 401 ); } + /** + * @ticket 41683 + */ + public function test_get_item_no_permission_public() { + wp_set_current_user( 0 ); + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Test sidebar', + 'show_in_rest' => true, + ) + ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/sidebars/sidebar-1' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $data = $this->remove_links( $data ); + $this->assertSame( + array( + 'id' => 'sidebar-1', + 'name' => 'Test sidebar', + 'description' => '', + 'class' => '', + 'before_widget' => '', + 'after_widget' => '', + 'before_title' => '', + 'after_title' => '', + 'status' => 'active', + 'widgets' => array(), + ), + $data + ); + } + /** * @ticket 41683 */ diff --git a/tests/phpunit/tests/rest-api/rest-widgets-controller.php b/tests/phpunit/tests/rest-api/rest-widgets-controller.php index be0d84e430..03d4ea0931 100644 --- a/tests/phpunit/tests/rest-api/rest-widgets-controller.php +++ b/tests/phpunit/tests/rest-api/rest-widgets-controller.php @@ -240,6 +240,100 @@ class WP_Test_REST_Widgets_Controller extends WP_Test_REST_Controller_Testcase { $this->assertErrorResponse( 'rest_cannot_manage_widgets', $response, 401 ); } + /** + * @ticket 53915 + */ + public function test_get_items_no_permission_show_in_rest() { + $this->setup_widget( + 'text', + 1, + array( + 'text' => 'Custom text test', + ) + ); + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Test sidebar', + 'show_in_rest' => true, + ), + array( 'text-1', 'testwidget' ) + ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/widgets' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $data = $this->remove_links( $data ); + $this->assertSameIgnoreEOL( + array( + array( + 'id' => 'text-1', + 'id_base' => 'text', + 'sidebar' => 'sidebar-1', + 'rendered' => '
Custom text test
', + ), + array( + 'id' => 'testwidget', + 'id_base' => 'testwidget', + 'sidebar' => 'sidebar-1', + 'rendered' => '

Default id

Default text', + ), + ), + $data + ); + } + + /** + * @ticket 53915 + */ + public function test_get_items_without_show_in_rest_are_removed_from_the_list() { + wp_set_current_user( self::$author_id ); + $this->setup_widget( + 'text', + 1, + array( + 'text' => 'Custom text test', + ) + ); + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Test sidebar 1', + 'show_in_rest' => true, + ), + array( 'text-1', 'testwidget' ) + ); + $this->setup_sidebar( + 'sidebar-2', + array( + 'name' => 'Test sidebar 2', + 'show_in_rest' => false, + ), + array( 'text-1', 'testwidget' ) + ); + $request = new WP_REST_Request( 'GET', '/wp/v2/widgets' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $data = $this->remove_links( $data ); + $this->assertSameIgnoreEOL( + array( + array( + 'id' => 'text-1', + 'id_base' => 'text', + 'sidebar' => 'sidebar-1', + 'rendered' => '
Custom text test
', + ), + array( + 'id' => 'testwidget', + 'id_base' => 'testwidget', + 'sidebar' => 'sidebar-1', + 'rendered' => '

Default id

Default text', + ), + ), + $data + ); + } + /** * @ticket 41683 */ @@ -295,56 +389,18 @@ class WP_Test_REST_Widgets_Controller extends WP_Test_REST_Controller_Testcase { 'id_base' => 'block', 'sidebar' => 'sidebar-1', 'rendered' => '

Block test

', - 'instance' => array( - 'encoded' => base64_encode( - serialize( - array( - 'content' => $block_content, - ) - ) - ), - 'hash' => wp_hash( - serialize( - array( - 'content' => $block_content, - ) - ) - ), - 'raw' => array( - 'content' => $block_content, - ), - ), ), array( 'id' => 'rss-1', 'id_base' => 'rss', 'sidebar' => 'sidebar-1', 'rendered' => 'RSS RSS test', - 'instance' => array( - 'encoded' => base64_encode( - serialize( - array( - 'title' => 'RSS test', - 'url' => 'https://wordpress.org/news/feed', - ) - ) - ), - 'hash' => wp_hash( - serialize( - array( - 'title' => 'RSS test', - 'url' => 'https://wordpress.org/news/feed', - ) - ) - ), - ), ), array( 'id' => 'testwidget', 'id_base' => 'testwidget', 'sidebar' => 'sidebar-1', 'rendered' => '

Default id

Default text', - 'instance' => null, ), ), $data @@ -469,25 +525,6 @@ class WP_Test_REST_Widgets_Controller extends WP_Test_REST_Controller_Testcase { 'id_base' => 'text', 'sidebar' => 'sidebar-1', 'rendered' => '
Custom text test
', - 'instance' => array( - 'encoded' => base64_encode( - serialize( - array( - 'text' => 'Custom text test', - ) - ) - ), - 'hash' => wp_hash( - serialize( - array( - 'text' => 'Custom text test', - ) - ) - ), - 'raw' => array( - 'text' => 'Custom text test', - ), - ), ), $data ); @@ -543,6 +580,42 @@ class WP_Test_REST_Widgets_Controller extends WP_Test_REST_Controller_Testcase { $this->assertErrorResponse( 'rest_cannot_manage_widgets', $response, 403 ); } + /** + * @ticket 53915 + */ + public function test_get_item_no_permission_show_in_rest() { + wp_set_current_user( 0 ); + + $this->setup_widget( + 'text', + 1, + array( + 'text' => 'Custom text test', + ) + ); + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Test sidebar', + 'show_in_rest' => true, + ), + array( 'text-1' ) + ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/widgets/text-1' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertSameSets( + array( + 'id' => 'text-1', + 'id_base' => 'text', + 'sidebar' => 'sidebar-1', + 'rendered' => '
Custom text test
', + ), + $data + ); + } + /** * @ticket 41683 */ diff --git a/tests/qunit/fixtures/wp-api-generated.js b/tests/qunit/fixtures/wp-api-generated.js index 58089b1b69..b2ebfcd65a 100644 --- a/tests/qunit/fixtures/wp-api-generated.js +++ b/tests/qunit/fixtures/wp-api-generated.js @@ -7135,27 +7135,21 @@ mockedApiResponse.Schema = { "description": "Base64 encoded representation of the instance settings.", "type": "string", "context": [ - "view", - "edit", - "embed" + "edit" ] }, "hash": { "description": "Cryptographic hash of the instance settings.", "type": "string", "context": [ - "view", - "edit", - "embed" + "edit" ] }, "raw": { "description": "Unencoded instance settings, if supported.", "type": "object", "context": [ - "view", - "edit", - "embed" + "edit" ] } }, @@ -7235,27 +7229,21 @@ mockedApiResponse.Schema = { "description": "Base64 encoded representation of the instance settings.", "type": "string", "context": [ - "view", - "edit", - "embed" + "edit" ] }, "hash": { "description": "Cryptographic hash of the instance settings.", "type": "string", "context": [ - "view", - "edit", - "embed" + "edit" ] }, "raw": { "description": "Unencoded instance settings, if supported.", "type": "object", "context": [ - "view", - "edit", - "embed" + "edit" ] } },