Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Nav Block bug as Contributor user via fix to *loadPostTypeEntities #18669

Merged

Conversation

getdave
Copy link
Contributor

@getdave getdave commented Nov 21, 2019

Closes #18659

The Nav Block placeholder has a button for Create from all Top Pages. To work the Block must have Page data available. If it isn't available then the button is disabled.

For higher Role users with greater permissions, this works absolutely fine. However, for the Contributor user the Pages data cannot be accessed. This appears to be due to the way we are accessing the Page data in our selectors/resolvers. Primarily the call to getEntityRecords sets context to edit for the Page post type REST API query. Lower user types don't have permission to access the endpoint under the edit context and so the request fails.

The (new) direction for this PR is to use apiFetch directly within the component to avoid the selectors entirely and make a request to the API. This ensures the Pages data is local to the component and allows us to set the context to view to ensure Pages are returned for all user Roles.

Screenshots

This shows me as a User with the Contributor Role inserting the Block:

Screen Capture on 2019-11-21 at 16-26-28

More Detail

Ultimately this traces back to loadPostTypeEntities.

On master the loadPostTypeEntities resolver is returning a WP_Error REST API response.

The problem here is this line:

yield apiFetch( { path: '/wp/v2/types?context=edit' } );

The usage of the edit context on the request URL is what is causing this. Try toggling the following request between edit and view in the URL param:

http://localhost:8889/wp-json/wp/v2/types?context=edit

You will see a different response. You can do the same by

  1. Create a Contributor User.
  2. Switch to (login as) that User.
  3. Create a New Post
  4. Open your Console and type:
fetch('http://localhost:8889/wp-json/wp/v2/types?context=edit')
  1. Switch to the Network tab and inspect the resulting http request. You'll see an error status code.
  2. Now do the same but change the context to view. You'll see all the Post Types returned.
  3. Switch back to your Admin user and repeat from #2 above. It works fine for this permission level.

This is caused by the permissions check in the WP_REST_Post_Types_Controller REST API endpoint. Specifically

current_user_can( $post_type->cap->edit_posts )

It appears that Contributor Role is not allowed edit_posts access to the Pages post type. As a result the error is returned on this REST permission check. This is in turn because we are passing the ?context=edit param in the apiFetch call.

Originally this PR tried to circumvent this by using the view context on the API request to retrieve the custom post type entities thereby avoiding the permissions check which seems unnecessary. But this was not a good option.


How has this been tested?

Manually testing that the Nav Block now has access to Pages data.

Types of changes

Breaking change (fix or feature that would cause existing functionality to not work as expected).

Checklist:

  • My code is tested.
  • My code follows the WordPress code style.
  • My code follows the accessibility standards.
  • My code has proper inline documentation.
  • I've included developer documentation if appropriate.
  • I've updated all React Native files affected by any refactorings/renamings in this PR. .

@getdave getdave added [Type] Bug An existing feature does not function as intended [Block] Navigation Affects the Navigation Block labels Nov 21, 2019
@getdave getdave requested a review from nerrad as a code owner November 21, 2019 14:02
@getdave getdave self-assigned this Nov 21, 2019
@getdave getdave added this to 👀 PRs to review in Navigation editor via automation Nov 21, 2019
@@ -199,6 +199,7 @@ export default compose( [
parent: 0,
order: 'asc',
orderby: 'id',
context: 'view',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't just switch to using the "view" context because it means we're having inconsistent entities format in the state depending on the caller. I think right now, we just can't use getEntityRecords. Instead, we should use apiFetch in these components directly. One way to make this easier and avoid repeating the same lifecycle/state handling for all apiFetch usage is to consider a useApiFetch hook.

const { data, status, error } = useApiFetch( { path: 'url' } )

Copy link
Contributor Author

@getdave getdave Nov 21, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@youknowriad So using apiFetch directly and not updating the "Redux" state at all, only local component state?

Also even if we do use a more direct method to make the API request, the data will still be wrong unless we switch the context to view. Is this what you are recommending?

Are there any good examples (or documentation) for using apiFetch directly that I could reference?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, local state + view context.

And API fetch is just a simple wrapper around wp.fetch to make it work in WP context https://developer.wordpress.org/block-editor/packages/packages-api-fetch/

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like Latest Posts uses it

this.fetchRequest = apiFetch( {

return () => {
isMounted = false;
};
}, [] );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume any component using apiFetch will hhave the same state, the same effects (status, data, error), should we extract this into a reusable useApiFetch hook like suggested?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes we could should but I wanted to check the basics were on track first before commiting additional effort.

Ill take this as thumbs up to proceed with the hook 😁

@getdave
Copy link
Contributor Author

getdave commented Nov 22, 2019

@youknowriad Looks like I'll need to be focused elsewhere for the next week so unlikely I'll get to this soon. Apologies.

@getdave getdave moved this from 👀 PRs to review to 💻 Issues in progress in Navigation editor Jan 7, 2020
@mtias
Copy link
Member

mtias commented Mar 10, 2020

@getdave what's the status of this one? I think we should hide this block for contributors (possibly also for authors).

@noisysocks noisysocks moved this from Issues in progress to PRs in progress in Navigation editor Mar 11, 2020
@tellthemachines tellthemachines force-pushed the fix/nav-block-broken-contrib-role-getentity-records branch from ad5b2da to 1c47636 Compare March 24, 2020 23:10
@github-actions
Copy link

github-actions bot commented Mar 25, 2020

Size Change: +4.5 kB (0%)

Total Size: 870 kB

Filename Size Change
build/a11y/index.js 1.02 kB +18 B (1%)
build/annotations/index.js 3.45 kB +6 B (0%)
build/api-fetch/index.js 3.8 kB +410 B (10%) ⚠️
build/autop/index.js 2.59 kB +3 B (0%)
build/block-directory/index.js 6.02 kB -4 B (0%)
build/block-editor/index.js 102 kB -10 B (0%)
build/block-library/index.js 110 kB +15 B (0%)
build/blocks/index.js 57.5 kB +1 B
build/components/index.js 195 kB +3.91 kB (2%)
build/compose/index.js 6.21 kB +5 B (0%)
build/core-data/index.js 10.7 kB +37 B (0%)
build/data/index.js 8.23 kB -26 B (0%)
build/date/index.js 5.37 kB +1 B
build/dom-ready/index.js 569 B +1 B
build/dom/index.js 3.05 kB -1 B
build/edit-post/index.js 92.3 kB -77 B (0%)
build/edit-site/index.js 9.03 kB +381 B (4%)
build/edit-site/style-rtl.css 3.41 kB -47 B (1%)
build/edit-site/style.css 3.41 kB -47 B (1%)
build/edit-widgets/index.js 4.43 kB +2 B (0%)
build/editor/index.js 42.8 kB -41 B (0%)
build/element/index.js 4.44 kB -1 B
build/format-library/index.js 6.94 kB -4 B (0%)
build/hooks/index.js 1.93 kB +2 B (0%)
build/i18n/index.js 3.57 kB -1 B
build/is-shallow-equal/index.js 710 B -2 B (0%)
build/keyboard-shortcuts/index.js 2.3 kB -5 B (0%)
build/list-reusable-blocks/index.js 2.99 kB +2 B (0%)
build/media-utils/index.js 4.84 kB -1 B
build/notices/index.js 1.57 kB -1 B
build/nux/index.js 3.01 kB -6 B (0%)
build/plugins/index.js 2.54 kB -5 B (0%)
build/priority-queue/index.js 780 B -1 B
build/rich-text/index.js 14.5 kB -1 B
build/server-side-render/index.js 2.54 kB -5 B (0%)
build/shortcode/index.js 1.7 kB -7 B (0%)
build/token-list/index.js 1.28 kB +1 B
build/viewport/index.js 1.6 kB -3 B (0%)
build/warning/index.js 1.14 kB +1 B
build/wordcount/index.js 1.17 kB -1 B
ℹ️ View Unchanged
Filename Size Change
build/blob/index.js 620 B 0 B
build/block-directory/style-rtl.css 760 B 0 B
build/block-directory/style.css 760 B 0 B
build/block-editor/style-rtl.css 11 kB 0 B
build/block-editor/style.css 11 kB 0 B
build/block-library/editor-rtl.css 7.21 kB 0 B
build/block-library/editor.css 7.21 kB 0 B
build/block-library/style-rtl.css 7.5 kB 0 B
build/block-library/style.css 7.51 kB 0 B
build/block-library/theme-rtl.css 669 B 0 B
build/block-library/theme.css 671 B 0 B
build/block-serialization-default-parser/index.js 1.65 kB 0 B
build/block-serialization-spec-parser/index.js 3.1 kB 0 B
build/components/style-rtl.css 15.8 kB 0 B
build/components/style.css 15.7 kB 0 B
build/data-controls/index.js 1.03 kB 0 B
build/deprecated/index.js 772 B 0 B
build/edit-navigation/index.js 2.4 kB 0 B
build/edit-navigation/style-rtl.css 95 B 0 B
build/edit-navigation/style.css 95 B 0 B
build/edit-post/style-rtl.css 8.35 kB 0 B
build/edit-post/style.css 8.34 kB 0 B
build/edit-widgets/style-rtl.css 2.57 kB 0 B
build/edit-widgets/style.css 2.57 kB 0 B
build/editor/editor-styles-rtl.css 423 B 0 B
build/editor/editor-styles.css 426 B 0 B
build/editor/style-rtl.css 3.38 kB 0 B
build/editor/style.css 3.38 kB 0 B
build/escape-html/index.js 733 B 0 B
build/format-library/style-rtl.css 502 B 0 B
build/format-library/style.css 502 B 0 B
build/html-entities/index.js 622 B 0 B
build/keycodes/index.js 1.7 kB 0 B
build/list-reusable-blocks/style-rtl.css 226 B 0 B
build/list-reusable-blocks/style.css 226 B 0 B
build/nux/style-rtl.css 616 B 0 B
build/nux/style.css 613 B 0 B
build/primitives/index.js 1.5 kB 0 B
build/redux-routine/index.js 2.84 kB 0 B
build/url/index.js 4.01 kB 0 B

compressed-size-action

@@ -0,0 +1,31 @@
/**
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added the reusable apiFetch hook in the navigation folder for now as I'm not sure what the best place for it would be, or where it might be reused. Feedback appreciated!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I suppose if it went in the api-fetch package, react would have to be added as a dependency to that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think, @youknowriad? Adding @wordpress/element as a dependency of @wordpress/api-fetch isn't totally out of the question since e.g. @wordpress/data depends on @wordpress/element.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, maybe it's fine. It does increase bundle size if loaded on the frontend without react need but I don't have a better alternative here.

@@ -134,7 +155,7 @@ function Navigation( {
selectBlock( clientId );
}

const hasPages = hasResolvedPages && pages && pages.length;
const hasPages = pages && pages.length;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think best to cast this to a boolean so that there's no surprises down the road when someone uses hasPages expecting a boolean but gets e.g. a number.

Suggested change
const hasPages = pages && pages.length;
const hasPages = !! pages && !! pages.length;

// We've stopped fetching
setIsRequesting( false );
} );
}, [] );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now this won't re-fetch if path changes. Is that right? It's probably a use case worth supporting for e.g. components where path is computed dynamically based on user input.

@noisysocks
Copy link
Member

Pulling master so that this PR includes #21118 should fix the PHP unit test failures that Travis CI is reporting.

@noisysocks
Copy link
Member

This works well when I test it locally! 👍

At first I thought switching to ?context=view would have implications in that we would no longer receive title.raw in the API response, but it looks like we're already using title.rendered and that HTML in the title isn't stripped out or rendered when setting title.rendered . (From memory, HTML isn't actually supported in page titles, it's just that most themes let you do it.)

Screen Shot 2020-03-30 at 16 43 59

I'd like #18669 (comment) and #18669 (comment) to be addressed and then this is a ✅ for me.

* @param {Function} setData Function to set data being returned, or empty array on error.
* @param {string} path Query path.
*/
export default function useApiFetch( setIsRequesting, setData, path ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of setIsRequesting and setData why not an API like this:

const { isLoading, data, error } = useApiFetch( queryObject )

similar to https://github.com/marcin-piela/react-fetching-library
I've used this on a side project and it was very nice.

Copy link
Contributor Author

@getdave getdave Apr 1, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a question. What if I need to make my fetch call cancelable? As far as I know the only way to do that is via something like this

const makeCancelable = ( promise ) => {

How could I mirror that behaviour with this hook?

Perhaps something like

https://github.com/marcin-piela/react-fetching-library/blob/master/src/hooks/useQuery/useQuery.ts#L23

Although AbortController doesn't have IE11 support...

@tellthemachines tellthemachines force-pushed the fix/nav-block-broken-contrib-role-getentity-records branch from 33b8b9c to 90f92fa Compare March 31, 2020 01:39
@getdave getdave requested a review from mmtr as a code owner March 31, 2020 03:42
@tellthemachines
Copy link
Contributor

Hmm, I moved the hook to @wordpress/api-fetch but now it's not getting imported correctly in the navigation block, not sure why. I added it as a named export to the index file in api-fetch, so import { useApiFetch } from '@wordpress/api-fetch' should work? It works fine if I change it to the direct path 😕

@noisysocks
Copy link
Member

Might need to set it as a property of the default export.

apiFetch.useApiFetch = useApiFetch;
...
export default apiFetch;

const [ error, setError ] = useState( null );

useEffect( () => {
apiFetch( { path } )
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might be wrong, but I'm wondering if the state should be reset prior to calling apiFetch by calling setIsLoading, setData and setError again with the initial values, as I think if the path changes, useState won't override any existing values being stored.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oooh good point, it probably won't because only the effect will re-run.

export function useApiFetch( path ) {
// Indicate the fetching status
const [ isLoading, setIsLoading ] = useState( true );
const [ data, setData ] = useState( [] );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed the data is referred to as dataList later on as well. The data returned from an API might also be an object, like when fetching an individual post. I think this could just be kept as an undefined initial value useState().

Copy link
Member

@noisysocks noisysocks left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! 👍

const ref = useRef();
const { selectBlock } = useDispatch( 'core/block-editor' );

/* eslint-disable @wordpress/no-unused-vars-before-return */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we find ourselves having to disable this often in components? If it's not a useful rule then we should just remove it...

(Not relevant to this PR!)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The value of the rule is explained here:

https://github.com/WordPress/gutenberg/blob/master/packages/eslint-plugin/docs/rules/no-unused-vars-before-return.md

Based on specific instances, we can choose to improve it as well. For example, it should probably be exempting hooks as a special circumstances, due to constraints imposed on us externally via Rules of Hooks.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The value of the rule is explained here:

I should have also mentioned there was previously quite a few offending cases that were cleaned up as part of the introduction of the rule, in case that's a more convincing method for interpreting the value.

See: #12827

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might not have been necessary to disable the rule here anyways? It was removed in #20884. If I recall correctly, the rule makes exceptions for object destructuring.

To the general problem:

we can choose to improve it as well. For example, it should probably be exempting hooks as a special circumstances, due to constraints imposed on us externally via Rules of Hooks.

This already exists as of #16737:

'@wordpress/no-unused-vars-before-return': [
'error',
{
excludePattern: '^use',
},
],

The problem here is that the hook is named as experimental, __experimentalUseColors. There are two ways we could accommodate for this:

  1. We can adopt a convention to reference the slot by its non-experimental name in the implementation of the component, renaming via the import (example)
  2. We can expand the pattern above to include __experimental and __unstable naming conventions

@noisysocks noisysocks merged commit f2b6778 into master Apr 1, 2020
Navigation editor automation moved this from PRs in progress to Done Apr 1, 2020
@noisysocks noisysocks deleted the fix/nav-block-broken-contrib-role-getentity-records branch April 1, 2020 06:24
@github-actions github-actions bot added this to the Gutenberg 7.9 milestone Apr 1, 2020
@noisysocks noisysocks removed this from Done in Navigation editor Apr 1, 2020
@noisysocks noisysocks added this to PRs in progress in Navigation editor via automation Apr 1, 2020
@noisysocks noisysocks moved this from PRs in progress to Done in Navigation editor Apr 1, 2020
*
* @param {string} path Query path.
*/
function useApiFetch( path ) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@aduth
Copy link
Member

aduth commented Apr 1, 2020

My initial reaction here is that we should be encouraging to use core data entities and the data mechanisms consistently, and not to make arbitrary API requests from components. If there's insufficiencies in these patterns, we should aim to resolve those.

I see there was a conversation early on at #18669 (comment) about how this is a particularly troublesome example of where we're unable to get data due to constraints of the REST API and its context parameter. Even if it's something we're okay to relent on, I think it would be important to be very conscious that it's an exception to the rule. We may want to use a hook like useAPIFetch, but it doesn't seem to me we should want to be making this publicly available. Making it part of the public API would tacitly condone this as a valid/encouraged means of making data available within a component, and I don't know that we're prepared to make that decision, especially considering that:

  • This was prepared for a case that we consciously considered as an exception, and only after relenting on a point where we grant that we'd prefer to use data and not a direct API request.
  • There's no clear guidelines on when and why it would be appropriate to use one or the other of useAPIFetch vs. core data, and we now have multiple and inconsistent methods to retrieve data.

Comment on lines +177 to +186
.then( ( fetchedData ) => {
setData( fetchedData );
// We've stopped fetching
setIsLoading( false );
} )
.catch( ( err ) => {
setError( err );
// We've stopped fetching
setIsLoading( false );
} );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no guarantee that the component is still mounted at the point we're calling setState here. If it was removed in the time between the request starting and its completion, the state setters will fail, and React will log a warning to the console.

See also: https://reactjs.org/docs/hooks-reference.html#cleaning-up-an-effect

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can I also reinforce that this anti pattern is super easy to introduce. I wonder whether we could make the hook handle this edge cases automatically by:

  1. Wrapping the fetch Promise to allow to be cancelled.
  2. Automatically calling the .cancel() on unmount.
  3. Checking for isCancelled state in .then / .catch handlers and exiting early ( to avoid state updates).

For an example see Automattic/jetpack#15228

Copy link
Contributor

@nerrad nerrad Apr 2, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This point makes me think of the behaviour of resolvers in wp.data which often include making fetch requests. This same issue can surface there via usage of useSelect with selectors that resolve (and I think already is). We probably should be looking into that too.

Copy link
Member

@noisysocks noisysocks Apr 3, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could promote good behaviour1 here by having apiFetch return a function that cancels the request.

useEffect( () => {
	setIsLoading( true );
	setData( null );
	setError( null );

	const cancelFetch = apiFetch( { path } )
		.then( ( fetchedData ) => {
			setData( fetchedData );
			// We've stopped fetching
			setIsLoading( false );
		} )
		.catch( ( err ) => {
			setError( err );
			// We've stopped fetching
			setIsLoading( false );
		} );

	return cancelFetch;
}, [ path ] );

1: If it's easy to do the right thing, it's more likely that one will do the right thing 🙂

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm motivated toward anything which keeps apiFetch cumbersome to use in a component, if it means that the preferred core data entities are the more obvious solution by virtue of their ergonomics advantage. Which isn't to say there's not issues there as well (#18669 (comment)), but I think we're in a better position to solve those internal to those implementations, even if it's something like this.

@noisysocks
Copy link
Member

@aduth: What do you suggest we do as a next step?

@aduth
Copy link
Member

aduth commented Apr 2, 2020

@noisysocks In my view, I think the most immediate next step could be to simply move useAPIFetch to be inlined within navigation/edit.js, in order to remove it from the public API. This is the most pressing for me.

There are other points mentioned about async state updates where we should consider making improvements. I believe the specific implementation may be blocking @getdave in some way, though to me it's not entirely clear if his original implementation had migrated from core entities to apiFetch if there was some plans for how to reconcile this.

@getdave
Copy link
Contributor Author

getdave commented Apr 17, 2020

@noisysocks @aduth @draganescu I'm thinking that we sholuld:

  1. Revert this PR via Revert introduction of useApiFetch hook and associated changes to Core Navigation block data fetching. #21674.
  2. Raise followup PR to incorporate the context fix in this PR, but using the Core entities methods.

This will:

  1. Remove useApiFetch - this will be a deprecation of a public API so what's the procedure here?
  2. Avoid relying on apiFetch which causes errors on unmount if not given special treatment.
  3. Ensure we continue utilising Core data and entities as the preferred data fetch method.

Any objections?

getdave added a commit that referenced this pull request Apr 17, 2020
…peEntities` (#18669)"

This reverts commit f2b6778.

# Conflicts:
#	package-lock.json
#	packages/api-fetch/package.json
#	packages/block-library/src/navigation/edit.js
getdave added a commit that referenced this pull request Apr 20, 2020
…loadPostTypeEntities` (#18669)"

This partially reverts commit f2b6778.

Only reverts the changes to utilise useApiFetch Hook.
getdave added a commit that referenced this pull request Apr 21, 2020
* Partially Revert "Fix Nav Block bug as Contributor user via fix to `*loadPostTypeEntities` (#18669)"

This partially reverts commit f2b6778.

Only reverts the changes to utilise useApiFetch Hook.

* Remove unneeded eslint disable rule
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Block] Navigation Affects the Navigation Block [Type] Bug An existing feature does not function as intended
Projects
No open projects
Development

Successfully merging this pull request may close these issues.

Navigation Block: not fully functional for contributor role due to getEntityRecords records issue
8 participants