From f77a46674dcf775f1f9b4f234679bb12c9a888a4 Mon Sep 17 00:00:00 2001 From: Ramon Date: Wed, 18 Sep 2024 10:41:47 +1000 Subject: [PATCH] Global Styles: allow read access to users with `edit_posts` capabilities (#65071) Allows any role that can edit a post, including custom post types, or edit theme options to read global styles from the API and in the editor. This enables read-only access to global styles in the post editor. Adds test coverage. --------- Co-authored-by: aaronrobertshaw Co-authored-by: ramonjd Co-authored-by: Mamaduka Co-authored-by: spacedmonkey Co-authored-by: talldan Co-authored-by: peterwilsoncc Co-authored-by: kraftner --- backport-changelog/6.7/7336.md | 3 + ...est-global-styles-controller-gutenberg.php | 51 ++++++----- lib/compat/wordpress-6.7/rest-api.php | 23 +++++ packages/core-data/src/resolvers.js | 4 +- .../global-styles-provider/index.js | 86 +++++++++++-------- ...lobal-styles-controller-gutenberg-test.php | 69 ++++++++++++++- 6 files changed, 168 insertions(+), 68 deletions(-) create mode 100644 backport-changelog/6.7/7336.md diff --git a/backport-changelog/6.7/7336.md b/backport-changelog/6.7/7336.md new file mode 100644 index 0000000000000..7cb2e26d7eeb9 --- /dev/null +++ b/backport-changelog/6.7/7336.md @@ -0,0 +1,3 @@ +https://github.com/WordPress/wordpress-develop/pull/7336 + +* https://github.com/WordPress/gutenberg/pull/65071 diff --git a/lib/class-wp-rest-global-styles-controller-gutenberg.php b/lib/class-wp-rest-global-styles-controller-gutenberg.php index e33304e596e12..1f6543fa18428 100644 --- a/lib/class-wp-rest-global-styles-controller-gutenberg.php +++ b/lib/class-wp-rest-global-styles-controller-gutenberg.php @@ -532,27 +532,39 @@ public function get_item_schema() { * Checks if a given request has access to read a single theme global styles config. * * @since 5.9.0 + * @since 6.7.0 Allow users with edit post capabilities to view theme global styles. * * @param WP_REST_Request $request Full details about the request. * @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise. */ public function get_theme_item_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + /* + * Verify if the current user has edit_posts capability. + */ + if ( current_user_can( 'edit_posts' ) ) { + return true; + } + + foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) { + if ( current_user_can( $post_type->cap->edit_posts ) ) { + return true; + } + } /* * Verify if the current user has edit_theme_options capability. - * This capability is required to edit/view/delete global styles. */ - if ( ! current_user_can( 'edit_theme_options' ) ) { - return new WP_Error( - 'rest_cannot_manage_global_styles', - __( 'Sorry, you are not allowed to access the global styles on this site.', 'gutenberg' ), - array( - 'status' => rest_authorization_required_code(), - ) - ); + if ( current_user_can( 'edit_theme_options' ) ) { + return true; } - return true; + return new WP_Error( + 'rest_cannot_read_global_styles', + __( 'Sorry, you are not allowed to access the global styles on this site.', 'gutenberg' ), + array( + 'status' => rest_authorization_required_code(), + ) + ); } /** @@ -616,23 +628,8 @@ public function get_theme_item( $request ) { * @param WP_REST_Request $request Full details about the request. * @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise. */ - public function get_theme_items_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable - - /* - * Verify if the current user has edit_theme_options capability. - * This capability is required to edit/view/delete global styles. - */ - if ( ! current_user_can( 'edit_theme_options' ) ) { - return new WP_Error( - 'rest_cannot_manage_global_styles', - __( 'Sorry, you are not allowed to access the global styles on this site.', 'gutenberg' ), - array( - 'status' => rest_authorization_required_code(), - ) - ); - } - - return true; + public function get_theme_items_permissions_check( $request ) { + return $this->get_theme_item_permissions_check( $request ); } /** diff --git a/lib/compat/wordpress-6.7/rest-api.php b/lib/compat/wordpress-6.7/rest-api.php index c5e2927198da0..313367594caae 100644 --- a/lib/compat/wordpress-6.7/rest-api.php +++ b/lib/compat/wordpress-6.7/rest-api.php @@ -114,3 +114,26 @@ function gutenberg_override_default_rest_server() { return 'Gutenberg_REST_Server'; } add_filter( 'wp_rest_server_class', 'gutenberg_override_default_rest_server', 1 ); + + +/** + * Filters the arguments for registering a wp_global_styles post type. + * Note when syncing to Core: the capabilities should be updates for `wp_global_styles` in the wp-includes/post.php. + * + * @since 6.7.0 + * + * @param array $args Array of arguments for registering a post type. + * See the register_post_type() function for accepted arguments. + * @param string $post_type Post type key. + * + * @return array Array of arguments for registering a post type. + */ +function gutenberg_register_post_type_args_for_wp_global_styles( $args, $post_type ) { + if ( 'wp_global_styles' === $post_type ) { + $args['capabilities']['read'] = 'edit_posts'; + } + + return $args; +} + +add_filter( 'register_post_type_args', 'gutenberg_register_post_type_args_for_wp_global_styles', 10, 2 ); diff --git a/packages/core-data/src/resolvers.js b/packages/core-data/src/resolvers.js index ce8c2db7a53b4..6cc7df27f48b1 100644 --- a/packages/core-data/src/resolvers.js +++ b/packages/core-data/src/resolvers.js @@ -645,7 +645,7 @@ export const __experimentalGetCurrentThemeBaseGlobalStyles = async ( { resolveSelect, dispatch } ) => { const currentTheme = await resolveSelect.getCurrentTheme(); const themeGlobalStyles = await apiFetch( { - path: `/wp/v2/global-styles/themes/${ currentTheme.stylesheet }`, + path: `/wp/v2/global-styles/themes/${ currentTheme.stylesheet }?context=view`, } ); dispatch.__experimentalReceiveThemeBaseGlobalStyles( currentTheme.stylesheet, @@ -658,7 +658,7 @@ export const __experimentalGetCurrentThemeGlobalStylesVariations = async ( { resolveSelect, dispatch } ) => { const currentTheme = await resolveSelect.getCurrentTheme(); const variations = await apiFetch( { - path: `/wp/v2/global-styles/themes/${ currentTheme.stylesheet }/variations`, + path: `/wp/v2/global-styles/themes/${ currentTheme.stylesheet }/variations?context=view`, } ); dispatch.__experimentalReceiveThemeGlobalStyleVariations( currentTheme.stylesheet, diff --git a/packages/editor/src/components/global-styles-provider/index.js b/packages/editor/src/components/global-styles-provider/index.js index 8268997d1f1de..6f2d8177056cb 100644 --- a/packages/editor/src/components/global-styles-provider/index.js +++ b/packages/editor/src/components/global-styles-provider/index.js @@ -46,24 +46,38 @@ export function mergeBaseAndUserConfigs( base, user ) { function useGlobalStylesUserConfig() { const { globalStylesId, isReady, settings, styles, _links } = useSelect( ( select ) => { - const { getEditedEntityRecord, hasFinishedResolution, canUser } = - select( coreStore ); + const { + getEntityRecord, + getEditedEntityRecord, + hasFinishedResolution, + canUser, + } = select( coreStore ); const _globalStylesId = select( coreStore ).__experimentalGetCurrentGlobalStylesId(); - const record = - _globalStylesId && - canUser( 'read', { - kind: 'root', - name: 'globalStyles', - id: _globalStylesId, - } ) - ? getEditedEntityRecord( - 'root', - 'globalStyles', - _globalStylesId - ) - : undefined; + let record; + const userCanEditGlobalStyles = canUser( 'update', { + kind: 'root', + name: 'globalStyles', + id: _globalStylesId, + } ); + + if ( _globalStylesId ) { + if ( userCanEditGlobalStyles ) { + record = getEditedEntityRecord( + 'root', + 'globalStyles', + _globalStylesId + ); + } else { + record = getEntityRecord( + 'root', + 'globalStyles', + _globalStylesId, + { context: 'view' } + ); + } + } let hasResolved = false; if ( @@ -71,13 +85,22 @@ function useGlobalStylesUserConfig() { '__experimentalGetCurrentGlobalStylesId' ) ) { - hasResolved = _globalStylesId - ? hasFinishedResolution( 'getEditedEntityRecord', [ - 'root', - 'globalStyles', - _globalStylesId, - ] ) - : true; + if ( _globalStylesId ) { + hasResolved = userCanEditGlobalStyles + ? hasFinishedResolution( 'getEditedEntityRecord', [ + 'root', + 'globalStyles', + _globalStylesId, + ] ) + : hasFinishedResolution( 'getEntityRecord', [ + 'root', + 'globalStyles', + _globalStylesId, + { context: 'view' }, + ] ); + } else { + hasResolved = true; + } } return { @@ -145,20 +168,11 @@ function useGlobalStylesUserConfig() { } function useGlobalStylesBaseConfig() { - const baseConfig = useSelect( ( select ) => { - const { - __experimentalGetCurrentThemeBaseGlobalStyles, - getCurrentTheme, - canUser, - } = select( coreStore ); - const currentTheme = getCurrentTheme(); - - return currentTheme && - canUser( 'read', 'global-styles/themes', currentTheme.stylesheet ) - ? __experimentalGetCurrentThemeBaseGlobalStyles() - : undefined; - }, [] ); - + const baseConfig = useSelect( + ( select ) => + select( coreStore ).__experimentalGetCurrentThemeBaseGlobalStyles(), + [] + ); return [ !! baseConfig, baseConfig ]; } diff --git a/phpunit/class-wp-rest-global-styles-controller-gutenberg-test.php b/phpunit/class-wp-rest-global-styles-controller-gutenberg-test.php index 85731a7dff520..c9453f4bd8e5c 100644 --- a/phpunit/class-wp-rest-global-styles-controller-gutenberg-test.php +++ b/phpunit/class-wp-rest-global-styles-controller-gutenberg-test.php @@ -12,11 +12,21 @@ class WP_REST_Global_Styles_Controller_Gutenberg_Test extends WP_Test_REST_Contr */ protected static $admin_id; + /** + * @var int + */ + protected static $editor_id; + /** * @var int */ protected static $subscriber_id; + /** + * @var int + */ + protected static $theme_manager_id; + /** * @var int */ @@ -44,12 +54,30 @@ public static function wpSetupBeforeClass( $factory ) { ) ); + self::$editor_id = $factory->user->create( + array( + 'role' => 'editor', + ) + ); + self::$subscriber_id = $factory->user->create( array( 'role' => 'subscriber', ) ); + self::$theme_manager_id = $factory->user->create( + array( + 'role' => 'subscriber', + ) + ); + + // Add the 'edit_theme_options' capability to the theme manager (subscriber). + $theme_manager_id = get_user_by( 'id', self::$theme_manager_id ); + if ( $theme_manager_id instanceof WP_User ) { + $theme_manager_id->add_cap( 'edit_theme_options' ); + } + // This creates the global styles for the current theme. self::$global_styles_id = $factory->post->create( array( @@ -72,7 +100,9 @@ public static function wpSetupBeforeClass( $factory ) { */ public static function wpTearDownAfterClass() { self::delete_user( self::$admin_id ); + self::delete_user( self::$editor_id ); self::delete_user( self::$subscriber_id ); + self::delete_user( self::$theme_manager_id ); } /** @@ -195,17 +225,50 @@ public function test_get_theme_item_no_user() { wp_set_current_user( 0 ); $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/themes/emptytheme' ); $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_cannot_manage_global_styles', $response, 401 ); + $this->assertErrorResponse( 'rest_cannot_read_global_styles', $response, 401 ); } /** * @covers WP_REST_Global_Styles_Controller_Gutenberg::get_theme_item */ - public function test_get_theme_item_permission_check() { + public function test_get_theme_item_subscriber_permission_check() { wp_set_current_user( self::$subscriber_id ); + switch_theme( 'emptytheme' ); + $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/themes/emptytheme' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_read_global_styles', $response, 403 ); + } + + /** + * @covers WP_REST_Global_Styles_Controller_Gutenberg::get_theme_item + */ + public function test_get_theme_item_editor_permission_check() { + wp_set_current_user( self::$editor_id ); + switch_theme( 'emptytheme' ); $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/themes/emptytheme' ); $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_cannot_manage_global_styles', $response, 403 ); + // Checks that the response has the expected keys. + $data = $response->get_data(); + $links = $response->get_links(); + $this->assertArrayHasKey( 'settings', $data, 'Data does not have "settings" key' ); + $this->assertArrayHasKey( 'styles', $data, 'Data does not have "styles" key' ); + $this->assertArrayHasKey( 'self', $links, 'Links do not have a "self" key' ); + } + + /** + * @covers WP_REST_Global_Styles_Controller_Gutenberg::get_theme_item + */ + public function test_get_theme_item_theme_options_manager_permission_check() { + wp_set_current_user( self::$theme_manager_id ); + switch_theme( 'emptytheme' ); + $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/themes/emptytheme' ); + $response = rest_get_server()->dispatch( $request ); + // Checks that the response has the expected keys. + $data = $response->get_data(); + $links = $response->get_links(); + $this->assertArrayHasKey( 'settings', $data, 'Data does not have "settings" key' ); + $this->assertArrayHasKey( 'styles', $data, 'Data does not have "styles" key' ); + $this->assertArrayHasKey( 'self', $links, 'Links do not have a "self" key' ); } /**