From 33ec3857e2453e4b938997da45418434fbfcf874 Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Mon, 20 Dec 2021 04:06:44 +0000 Subject: [PATCH] Alternative: restrict Navigation permissions and show UI warning if cannot create (#37454) * Test for publish abtility and display warning * Update creation of Nav posts to require admin perms * Only show menu creation option if user has permission to create * Move permission selectors to hook * Show warning if unable to create Navigation Menus * Copy changes from Core patch See https://github.com/WordPress/wordpress-develop/pull/2056/files * Only show error if create is not allowed * Revert "Copy changes from Core patch" This reverts commit 1872f62a454dc41ce787103e3d1830f7ff00d63c. * Use streamlined permissions Kudos to @spacedmonkey for https://github.com/WordPress/gutenberg/pull/37454#discussion_r771026149 * Remove inline warning and reenable ability to select existing * Refactor Notices to reusable hook * Add notices for creating Menus * Rename dep for clarity * Hide other creation options * Add e2e test * Hide classic Menus from dropdown See https://github.com/WordPress/gutenberg/pull/37454#issuecomment-996569572 * Fix up e2e tests following changes from rebase * Update to make component props more agnostic * Make component props less tighly coupled to permissions * Remove unneeded undefined fallback * Refactor hooks * Try switch user to admin * Try switching back to admin after each permissions test * Try targetting test that relies on avoiding a 404 from URL details endpoint --- lib/navigation.php | 12 +++ .../src/navigation/edit/index.js | 93 ++++++++++--------- .../edit/navigation-menu-selector.js | 32 ++++--- .../src/navigation/edit/placeholder/index.js | 64 ++++++++----- .../navigation/edit/use-navigation-notice.js | 37 ++++++++ .../block-library/src/navigation/editor.scss | 1 + .../src/navigation/use-navigation-menu.js | 5 + .../specs/editor/blocks/navigation.test.js | 28 ++++++ 8 files changed, 189 insertions(+), 83 deletions(-) create mode 100644 packages/block-library/src/navigation/edit/use-navigation-notice.js diff --git a/lib/navigation.php b/lib/navigation.php index 1053463bce240d..7bfc3bd063c175 100644 --- a/lib/navigation.php +++ b/lib/navigation.php @@ -50,6 +50,18 @@ function gutenberg_register_navigation_post_type() { 'editor', 'revisions', ), + 'capabilities' => array( + 'edit_others_posts' => 'edit_theme_options', + 'delete_posts' => 'edit_theme_options', + 'publish_posts' => 'edit_theme_options', + 'create_posts' => 'edit_theme_options', + 'read_private_posts' => 'edit_theme_options', + 'delete_private_posts' => 'edit_theme_options', + 'delete_published_posts' => 'edit_theme_options', + 'delete_others_posts' => 'edit_theme_options', + 'edit_private_posts' => 'edit_theme_options', + 'edit_published_posts' => 'edit_theme_options', + ), ); register_post_type( 'wp_navigation', $args ); diff --git a/packages/block-library/src/navigation/edit/index.js b/packages/block-library/src/navigation/edit/index.js index 82355fa98972d8..441fee4dbff23e 100644 --- a/packages/block-library/src/navigation/edit/index.js +++ b/packages/block-library/src/navigation/edit/index.js @@ -39,7 +39,6 @@ import { Button, } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; -import { store as noticeStore } from '@wordpress/notices'; /** * Internal dependencies @@ -54,6 +53,7 @@ import NavigationMenuSelector from './navigation-menu-selector'; import NavigationMenuNameControl from './navigation-menu-name-control'; import UnsavedInnerBlocks from './unsaved-inner-blocks'; import NavigationMenuDeleteControl from './navigation-menu-delete-control'; +import useNavigationNotice from './use-navigation-notice'; const EMPTY_ARRAY = []; @@ -107,8 +107,6 @@ function Navigation( { customPlaceholder: CustomPlaceholder = null, customAppender: CustomAppender = null, } ) { - const noticeRef = useRef(); - const { openSubmenusOnClick, overlayMenu, @@ -192,8 +190,6 @@ function Navigation( { __unstableMarkNextChangeAsNotPersistent, } = useDispatch( blockEditorStore ); - const { createWarningNotice, removeNotice } = useDispatch( noticeStore ); - const [ hasSavedUnsavedInnerBlocks, setHasSavedUnsavedInnerBlocks, @@ -220,6 +216,8 @@ function Navigation( { hasResolvedCanUserUpdateNavigationEntity, canUserDeleteNavigationEntity, hasResolvedCanUserDeleteNavigationEntity, + canUserCreateNavigation, + hasResolvedCanUserCreateNavigation, } = useNavigationMenu( ref ); const navRef = useRef(); @@ -307,7 +305,7 @@ function Navigation( { setDetectedColor, setDetectedBackgroundColor ); - const subMenuElement = navRef.current.querySelector( + const subMenuElement = navRef.current?.querySelector( '[data-type="core/navigation-link"] [data-type="core/navigation-link"]' ); if ( subMenuElement ) { @@ -336,52 +334,52 @@ function Navigation( { } }, [ clientId, ref, hasUncontrolledInnerBlocks, controlledInnerBlocks ] ); - useEffect( () => { - const setPermissionsNotice = () => { - if ( noticeRef.current ) { - return; - } - - noticeRef.current = - 'block-library/core/navigation/permissions/update'; - - createWarningNotice( - __( - 'You do not have permission to edit this Menu. Any changes made will not be saved.' - ), - { - id: noticeRef.current, - type: 'snackbar', - } - ); - }; + const [ showCantEditNotice, hideCantEditNotice ] = useNavigationNotice( { + name: 'block-library/core/navigation/permissions/update', + message: __( + 'You do not have permission to edit this Menu. Any changes made will not be saved.' + ), + } ); - const removePermissionsNotice = () => { - if ( ! noticeRef.current ) { - return; - } - removeNotice( noticeRef.current ); - noticeRef.current = null; - }; + const [ showCantCreateNotice, hideCantCreateNotice ] = useNavigationNotice( + { + name: 'block-library/core/navigation/permissions/create', + message: __( + 'You do not have permission to create Navigation Menus.' + ), + } + ); + useEffect( () => { if ( ! isSelected && ! isInnerBlockSelected ) { - removePermissionsNotice(); + hideCantEditNotice(); + hideCantCreateNotice(); } - if ( - ( isSelected || isInnerBlockSelected ) && - hasResolvedCanUserUpdateNavigationEntity && - ! canUserUpdateNavigationEntity - ) { - setPermissionsNotice(); + if ( isSelected || isInnerBlockSelected ) { + if ( + hasResolvedCanUserUpdateNavigationEntity && + ! canUserUpdateNavigationEntity + ) { + showCantEditNotice(); + } + + if ( + ! ref && + hasResolvedCanUserCreateNavigation && + ! canUserCreateNavigation + ) { + showCantCreateNotice(); + } } }, [ - ref, - isEntityAvailable, - hasResolvedCanUserUpdateNavigationEntity, - canUserUpdateNavigationEntity, isSelected, isInnerBlockSelected, + canUserUpdateNavigationEntity, + hasResolvedCanUserUpdateNavigationEntity, + canUserCreateNavigation, + hasResolvedCanUserCreateNavigation, + ref, ] ); const startWithEmptyMenu = useCallback( () => { @@ -488,6 +486,7 @@ function Navigation( { onClose(); } } onCreateNew={ startWithEmptyMenu } + showCreate={ canUserCreateNavigation } /> ) } @@ -642,11 +641,13 @@ function Navigation( { hasResolvedNavigationMenus } clientId={ clientId } + canUserCreateNavigation={ canUserCreateNavigation } /> ) } - { ! isEntityAvailable && ! isPlaceholderShown && ( - - ) } + { ! hasResolvedCanUserCreateNavigation || + ( ! isEntityAvailable && ! isPlaceholderShown && ( + + ) ) } { ! isPlaceholderShown && ( - - - { __( 'Create new menu' ) } - - - { __( 'Manage menus' ) } - - + { showCreate && ( + + + { __( 'Create new menu' ) } + + + { __( 'Manage menus' ) } + + + ) } ); } diff --git a/packages/block-library/src/navigation/edit/placeholder/index.js b/packages/block-library/src/navigation/edit/placeholder/index.js index a5ef385dfaa6df..bf2453488b8b6b 100644 --- a/packages/block-library/src/navigation/edit/placeholder/index.js +++ b/packages/block-library/src/navigation/edit/placeholder/index.js @@ -31,6 +31,7 @@ const ExistingMenusDropdown = ( { onFinish, menus, onCreateFromMenu, + showClassicMenus = false, } ) => { const toggleProps = { variant: 'tertiary', @@ -65,22 +66,24 @@ const ExistingMenusDropdown = ( { ); } ) } - - { menus?.map( ( menu ) => { - return ( - { - setSelectedMenu( menu.id ); - onCreateFromMenu( menu.name ); - } } - onClose={ onClose } - key={ menu.id } - > - { decodeEntities( menu.name ) } - - ); - } ) } - + { showClassicMenus && ( + + { menus?.map( ( menu ) => { + return ( + { + setSelectedMenu( menu.id ); + onCreateFromMenu( menu.name ); + } } + onClose={ onClose } + key={ menu.id } + > + { decodeEntities( menu.name ) } + + ); + } ) } + + ) } ) } @@ -92,6 +95,7 @@ export default function NavigationPlaceholder( { onFinish, canSwitchNavigationMenu, hasResolvedNavigationMenus, + canUserCreateNavigation = false, } ) { const [ selectedMenu, setSelectedMenu ] = useState(); const [ isCreatingFromMenu, setIsCreatingFromMenu ] = useState( false ); @@ -102,6 +106,10 @@ export default function NavigationPlaceholder( { blocks, navigationMenuTitle = null ) => { + if ( ! canUserCreateNavigation ) { + return; + } + const navigationMenu = await createNavigationMenu( navigationMenuTitle, blocks @@ -176,8 +184,10 @@ export default function NavigationPlaceholder( { { ' ' } { __( 'Navigation' ) } +
- { hasMenus || navigationMenus.length ? ( + + { hasMenus || navigationMenus?.length ? ( <>
) : undefined } - { hasPages ? ( + { canUserCreateNavigation && hasPages ? ( <> + + { canUserCreateNavigation && ( + + ) } diff --git a/packages/block-library/src/navigation/edit/use-navigation-notice.js b/packages/block-library/src/navigation/edit/use-navigation-notice.js new file mode 100644 index 00000000000000..2466c79c2e7d51 --- /dev/null +++ b/packages/block-library/src/navigation/edit/use-navigation-notice.js @@ -0,0 +1,37 @@ +/** + * WordPress dependencies + */ +import { useRef } from '@wordpress/element'; +import { useDispatch } from '@wordpress/data'; +import { store as noticeStore } from '@wordpress/notices'; + +function useNavigationNotice( { name, message } = {} ) { + const noticeRef = useRef(); + + const { createWarningNotice, removeNotice } = useDispatch( noticeStore ); + + const showNotice = () => { + if ( noticeRef.current ) { + return; + } + + noticeRef.current = name; + + createWarningNotice( message, { + id: noticeRef.current, + type: 'snackbar', + } ); + }; + + const hideNotice = () => { + if ( ! noticeRef.current ) { + return; + } + removeNotice( noticeRef.current ); + noticeRef.current = null; + }; + + return [ showNotice, hideNotice ]; +} + +export default useNavigationNotice; diff --git a/packages/block-library/src/navigation/editor.scss b/packages/block-library/src/navigation/editor.scss index 545075e1a7af65..542bafc747c300 100644 --- a/packages/block-library/src/navigation/editor.scss +++ b/packages/block-library/src/navigation/editor.scss @@ -372,6 +372,7 @@ $color-control-label-height: 20px; font-size: $default-font-size; font-family: $default-font; gap: $grid-unit-15 * 0.5; + align-items: center; // Margins. .components-dropdown, diff --git a/packages/block-library/src/navigation/use-navigation-menu.js b/packages/block-library/src/navigation/use-navigation-menu.js index 0ce579c778e0dc..f647f814339f65 100644 --- a/packages/block-library/src/navigation/use-navigation-menu.js +++ b/packages/block-library/src/navigation/use-navigation-menu.js @@ -79,6 +79,11 @@ export default function useNavigationMenu( ref ) { 'canUser', [ 'delete', 'navigation', ref ] ), + canUserCreateNavigation: canUser( 'create', 'navigation' ), + hasResolvedCanUserCreateNavigation: hasFinishedResolution( + 'canUser', + [ 'create', 'navigation' ] + ), }; }, [ ref ] diff --git a/packages/e2e-tests/specs/editor/blocks/navigation.test.js b/packages/e2e-tests/specs/editor/blocks/navigation.test.js index e8fc8a228a10fc..140ee38170520e 100644 --- a/packages/e2e-tests/specs/editor/blocks/navigation.test.js +++ b/packages/e2e-tests/specs/editor/blocks/navigation.test.js @@ -20,6 +20,7 @@ import { createUser, loginUser, deleteUser, + switchUserToAdmin, } from '@wordpress/e2e-test-utils'; /** @@ -761,6 +762,10 @@ describe( 'Navigation', () => { } ); } ); + afterEach( async () => { + await switchUserToAdmin(); + } ); + afterAll( async () => { await deleteUser( contributorUsername ); } ); @@ -810,5 +815,28 @@ describe( 'Navigation', () => { // Todo: removed once Nav Areas are removed from the Gutenberg Plugin. expect( console ).toHaveErrored(); } ); + + it( 'shows a warning if user does not have permission to create navigation menus', async () => { + const noticeText = + 'You do not have permission to create Navigation Menus.'; + // Switch to a Contributor role user - they should not have + // permission to update Navigations. + await loginUser( contributorUsername, contributorPassword ); + + await createNewPost(); + await insertBlock( 'Navigation' ); + + // Make sure the snackbar error shows up + await page.waitForXPath( + `//*[contains(@class, 'components-snackbar__content')][ text()="${ noticeText }" ]` + ); + + // Expect a console 403 for request to Navigation Areas for lower permission users. + // This is because reading requires the `edit_theme_options` capability + // which the Contributor level user does not have. + // See: https://github.com/WordPress/gutenberg/blob/4cedaf0c4abb0aeac4bfd4289d63e9889efe9733/lib/class-wp-rest-block-navigation-areas-controller.php#L81-L91. + // Todo: removed once Nav Areas are removed from the Gutenberg Plugin. + expect( console ).toHaveErrored(); + } ); } ); } );