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

Core Data: Add support for fetching permissions of custom actions #18956

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 0 additions & 12 deletions docs/designers-developers/developers/data/data-core-editor.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,6 @@ _Related_

- canInsertBlockType in core/block-editor store.

<a name="canUserUseUnfilteredHTML" href="#canUserUseUnfilteredHTML">#</a> **canUserUseUnfilteredHTML**

Returns whether or not the user has the unfiltered_html capability.

_Parameters_

- _state_ `Object`: Editor state.

_Returns_

- `boolean`: Whether the user can or can't post unfiltered HTML.

<a name="didPostSaveRequestFail" href="#didPostSaveRequestFail">#</a> **didPostSaveRequestFail**

Returns true if a previous post save was attempted but failed, or false
Expand Down
60 changes: 39 additions & 21 deletions packages/core-data/src/resolvers.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,8 @@ export function* hasUploadPermissions() {
* Checks whether the current user can perform the given action on the given
* REST resource.
*
* @param {string} action Action to check. One of: 'create', 'read', 'update',
* 'delete'.
* @param {string} action Action to check. One of 'create', 'read', 'update',
* 'delete', or a JSON Hyper Schema targetSchema key.
* @param {string} resource REST resource to check, e.g. 'media' or 'posts'.
* @param {?string} id ID of the rest resource to check.
*/
Expand All @@ -140,43 +140,61 @@ export function* canUser( action, resource, id ) {
delete: 'DELETE',
};

const method = methods[ action ];
if ( ! method ) {
throw new Error( `'${ action }' is not a valid action.` );
const isCustomAction = ! methods.hasOwnProperty( action );

let path = '/wp/v2/' + resource;

if ( id ) {
path += '/' + id;
Comment on lines +145 to +148
Copy link
Contributor

Choose a reason for hiding this comment

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

We don't want to add "undefined" when resource and/or id are undefined.

Copy link
Member Author

Choose a reason for hiding this comment

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

resource should be considered a required parameter. id is optional†, but this concatenation occurs in a condition to consider it as being set.

† The JSDoc should be updated from the nullable {?string} type to the correct [id] optional parameter notation.

Copy link
Contributor

Choose a reason for hiding this comment

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

What if I want to request the root/index resource?

Copy link
Member Author

Choose a reason for hiding this comment

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

You would omit the id parameter.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think I might not be understanding what you're asking. There will always be a resource.

When I say it doesn't necessarily need to be a singular resource, I consider it the same as how it might map to a use of current_user_can in PHP:

current_user_can( 'publish_posts' );
current_user_can( 'publish_post', $post->ID );

https://developer.wordpress.org/reference/functions/current_user_can/

I see this mapping to canUser as something like:

canUser( 'publish', 'posts' );
canUser( 'publish', 'posts', postId );

The caveat mentioned in the original post is that, currently, the REST API only supports the latter of these two, since it only includes action _links on the response of an item:

https://github.com/WordPress/wordpress-develop/blob/47e38ea/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php#L1675-L1683

My thinking is that, to represent the first of these (without an ID), it could be possible to include the action _links properties on the collection response as well:

https://github.com/WordPress/wordpress-develop/blob/47e38ea/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php#L155

Copy link
Contributor

Choose a reason for hiding this comment

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

Right, but we also have a base resource:

f6ce79c#diff-c5eaec560b039f2dc76f93fa76ba89da

Aren't there capabilities for /wp/v2/ or /?

I still think that canUser should have the same API as getEntityRecord with kind and name and we can just grab the resource from the entity config as I outlined here: #18956 (review).

Copy link
Member Author

Choose a reason for hiding this comment

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

Aren't there capabilities for /wp/v2/ or /?

Doesn't seem to me like there would be.

I still think that canUser should have the same API as getEntityRecord with kind and name and we can just grab the resource from the entity config as I outlined here: #18956 (review).

I agree this should be a goal for reuse. Not entirely sure yet how I feel about the overloaded signature, or if it's something to reevaluate whether "resource" is ever a sufficient parameter (i.e. might be more inclined to deprecate this parameter in favor of kind, name, if it came to it, or perhaps vice versa).

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh, I never meant to have an overloaded signature. I was suggesting to deprecate "resource".

}

const path = id ? `/wp/v2/${ resource }/${ id }` : `/wp/v2/${ resource }`;
if ( isCustomAction ) {
path = addQueryArgs( path, { context: 'edit' } );
}

let response;
try {
response = yield apiFetch( {
path,
// Ideally this would always be an OPTIONS request, but unfortunately there's
// a bug in the REST API which causes the Allow header to not be sent on
// OPTIONS requests to /posts/:id routes.
// https://core.trac.wordpress.org/ticket/45753
method: id ? 'GET' : 'OPTIONS',
parse: false,
// Ideally, this should only be a GET request if requesting details
// of a custom action. Until required WordPress support reaches 5.3
// or newer, the `id` condition must be included, due to a previous
// bug in the REST API where the Allow header was not on OPTIONS
// requests to /posts/:id routes.
//
// See: https://core.trac.wordpress.org/ticket/45753
method: id || isCustomAction ? 'GET' : 'OPTIONS',

// Only parse response as JSON if requesting permissions of a custom
// actions. Non-custom actions derive permissions from the response
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// actions. Non-custom actions derive permissions from the response
// action. Non-custom actions derive permissions from the response

// headers, which aren't available on the parsed result.
parse: isCustomAction,
} );
} catch ( error ) {
// Do nothing if our OPTIONS request comes back with an API error (4xx or
// 5xx). The previously determined isAllowed value will remain in the store.
return;
}

let allowHeader;
if ( hasIn( response, [ 'headers', 'get' ] ) ) {
// If the request is fetched using the fetch api, the header can be
// retrieved using the 'get' method.
allowHeader = response.headers.get( 'allow' );
let isAllowed;
if ( isCustomAction ) {
isAllowed = hasIn( response, [ '_links', 'wp:action-' + action ] );
} else {
// If the request was preloaded server-side and is returned by the
// preloading middleware, the header will be a simple property.
allowHeader = get( response, [ 'headers', 'Allow' ], '' );
let allowHeader;
if ( hasIn( response, [ 'headers', 'get' ] ) ) {
// If the request is fetched using the fetch api, the header can be
// retrieved using the 'get' method.
allowHeader = response.headers.get( 'allow' );
} else {
// If the request was preloaded server-side and is returned by the
// preloading middleware, the header will be a simple property.
allowHeader = get( response, [ 'headers', 'Allow' ], '' );
}

isAllowed = includes( allowHeader, methods[ action ] );
}

const key = compact( [ action, resource, id ] ).join( '/' );
const isAllowed = includes( allowHeader, method );
yield receiveUserPermission( key, isAllowed );
}

Expand Down
26 changes: 26 additions & 0 deletions packages/core-data/src/test/resolvers.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,32 @@ describe( 'canUser', () => {
expect( received.done ).toBe( true );
expect( received.value ).toBeUndefined();
} );

it( 'receives custom actions', () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice!

const generator = canUser( 'publish', 'posts' );

let received = generator.next();
expect( received.done ).toBe( false );
expect( received.value ).toEqual( apiFetch( {
path: '/wp/v2/posts?context=edit',
method: 'GET',
parse: true,
} ) );

received = generator.next( {
_links: {
'wp:action-publish': [
{ href: 'http://localhost:8888/wp-json/wp/v2/posts' },
],
},
} );
expect( received.done ).toBe( false );
expect( received.value ).toEqual( receiveUserPermission( 'publish/posts', true ) );

received = generator.next();
expect( received.done ).toBe( true );
expect( received.value ).toBeUndefined();
} );
} );

describe( 'getAutosaves', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
/**
* External dependencies
*/
import { get } from 'lodash';

/**
* WordPress dependencies
*/
Expand Down Expand Up @@ -76,11 +71,7 @@ export function PostPublishButtonOrToggle( {

export default compose(
withSelect( ( select ) => ( {
hasPublishAction: get(
select( 'core/editor' ).getCurrentPost(),
[ '_links', 'wp:action-publish' ],
false
),
hasPublishAction: select( 'core' ).canUser( 'publish', 'posts', select( 'core/editor' ).getCurrentPostId() ),
isBeingScheduled: select( 'core/editor' ).isEditedPostBeingScheduled(),
isPending: select( 'core/editor' ).isCurrentPostPending(),
isPublished: select( 'core/editor' ).isCurrentPostPublished(),
Expand Down
9 changes: 2 additions & 7 deletions packages/editor/src/components/post-author/check.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
/**
* External dependencies
*/
import { get } from 'lodash';

/**
* WordPress dependencies
*/
Expand All @@ -24,9 +19,9 @@ export function PostAuthorCheck( { hasAssignAuthorAction, authors, children } )

export default compose( [
withSelect( ( select ) => {
const post = select( 'core/editor' ).getCurrentPost();
const postId = select( 'core/editor' ).getCurrentPostId();
return {
hasAssignAuthorAction: get( post, [ '_links', 'wp:action-assign-author' ], false ),
hasAssignAuthorAction: select( 'core' ).canUser( 'assign-author', 'posts', postId ),
postType: select( 'core/editor' ).getCurrentPostType(),
authors: select( 'core' ).getAuthors(),
};
Expand Down
9 changes: 2 additions & 7 deletions packages/editor/src/components/post-pending-status/check.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
/**
* External dependencies
*/
import { get } from 'lodash';

/**
* WordPress dependencies
*/
Expand All @@ -19,9 +14,9 @@ export function PostPendingStatusCheck( { hasPublishAction, isPublished, childre

export default compose(
withSelect( ( select ) => {
const { isCurrentPostPublished, getCurrentPostType, getCurrentPost } = select( 'core/editor' );
const { isCurrentPostPublished, getCurrentPostType, getCurrentPostId } = select( 'core/editor' );
return {
hasPublishAction: get( getCurrentPost(), [ '_links', 'wp:action-publish' ], false ),
hasPublishAction: select( 'core' ).canUser( 'publish', 'posts', getCurrentPostId() ),
isPublished: isCurrentPostPublished(),
postType: getCurrentPostType(),
};
Expand Down
6 changes: 3 additions & 3 deletions packages/editor/src/components/post-publish-button/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
import { noop, get } from 'lodash';
import { noop } from 'lodash';
import classnames from 'classnames';
import memoize from 'memize';
import EquivalentKeyMap from 'equivalent-key-map';
Expand Down Expand Up @@ -201,11 +201,11 @@ export default compose( [
isEditedPostSaveable,
isEditedPostPublishable,
isPostSavingLocked,
getCurrentPost,
getCurrentPostType,
getCurrentPostId,
hasNonPostEntityChanges,
} = select( 'core/editor' );
const { canUser } = select( 'core' );
return {
isSaving: isSavingPost(),
isBeingScheduled: isEditedPostBeingScheduled(),
Expand All @@ -214,7 +214,7 @@ export default compose( [
isPostSavingLocked: isPostSavingLocked(),
isPublishable: isEditedPostPublishable(),
isPublished: isCurrentPostPublished(),
hasPublishAction: get( getCurrentPost(), [ '_links', 'wp:action-publish' ], false ),
hasPublishAction: canUser( 'publish', 'posts', getCurrentPostId() ),
postType: getCurrentPostType(),
postId: getCurrentPostId(),
hasNonPostEntityChanges: hasNonPostEntityChanges(),
Expand Down
10 changes: 3 additions & 7 deletions packages/editor/src/components/post-publish-button/label.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
/**
* External dependencies
*/
import { get } from 'lodash';

/**
* WordPress dependencies
*/
Expand Down Expand Up @@ -47,16 +42,17 @@ export default compose( [
isEditedPostBeingScheduled,
isSavingPost,
isPublishingPost,
getCurrentPost,
getCurrentPostId,
getCurrentPostType,
isAutosavingPost,
} = select( 'core/editor' );
const { canUser } = select( 'core' );
return {
isPublished: isCurrentPostPublished(),
isBeingScheduled: isEditedPostBeingScheduled(),
isSaving: forceIsSaving || isSavingPost(),
isPublishing: isPublishingPost(),
hasPublishAction: get( getCurrentPost(), [ '_links', 'wp:action-publish' ], false ),
hasPublishAction: canUser( 'publish', 'posts', getCurrentPostId() ),
postType: getCurrentPostType(),
isAutosaving: isAutosavingPost(),
};
Expand Down
6 changes: 3 additions & 3 deletions packages/editor/src/components/post-publish-panel/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,9 @@ export class PostPublishPanel extends Component {

export default compose( [
withSelect( ( select ) => {
const { getPostType } = select( 'core' );
const { canUser, getPostType } = select( 'core' );
const {
getCurrentPost,
getCurrentPostId,
getEditedPostAttribute,
isCurrentPostPublished,
isCurrentPostScheduled,
Expand All @@ -125,7 +125,7 @@ export default compose( [
const postType = getPostType( getEditedPostAttribute( 'type' ) );

return {
hasPublishAction: get( getCurrentPost(), [ '_links', 'wp:action-publish' ], false ),
hasPublishAction: canUser( 'publish', 'posts', getCurrentPostId() ),
isPostTypeViewable: get( postType, [ 'viewable' ], false ),
isBeingScheduled: isEditedPostBeingScheduled(),
isDirty: isEditedPostDirty(),
Expand Down
10 changes: 3 additions & 7 deletions packages/editor/src/components/post-publish-panel/prepublish.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
/**
* External dependencies
*/
import { get } from 'lodash';

/**
* WordPress dependencies
*/
Expand Down Expand Up @@ -68,11 +63,12 @@ function PostPublishPanelPrepublish( {
export default withSelect(
( select ) => {
const {
getCurrentPost,
getCurrentPostId,
isEditedPostBeingScheduled,
} = select( 'core/editor' );
const { canUser } = select( 'core' );
return {
hasPublishAction: get( getCurrentPost(), [ '_links', 'wp:action-publish' ], false ),
hasPublishAction: canUser( 'publish', 'posts', getCurrentPostId() ),
isBeingScheduled: isEditedPostBeingScheduled(),
};
}
Expand Down
9 changes: 4 additions & 5 deletions packages/editor/src/components/post-saved-state/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
* External dependencies
*/
import classnames from 'classnames';
import { get } from 'lodash';

/**
* WordPress dependencies
Expand Down Expand Up @@ -44,7 +43,7 @@ export class PostSavedState extends Component {

render() {
const {
post,
hasPublishAction,
isNew,
isScheduled,
isPublished,
Expand Down Expand Up @@ -96,7 +95,6 @@ export class PostSavedState extends Component {

// Once the post has been submitted for review this button
// is not needed for the contributor role.
const hasPublishAction = get( post, [ '_links', 'wp:action-publish' ], false );
if ( ! hasPublishAction && isPending ) {
return null;
}
Expand Down Expand Up @@ -136,12 +134,13 @@ export default compose( [
isEditedPostDirty,
isSavingPost,
isEditedPostSaveable,
getCurrentPost,
getCurrentPostId,
isAutosavingPost,
getEditedPostAttribute,
} = select( 'core/editor' );
const { canUser } = select( 'core' );
return {
post: getCurrentPost(),
hasPublishAction: canUser( 'publish', 'posts', getCurrentPostId() ),
isNew: isEditedPostNew(),
isPublished: isCurrentPostPublished(),
isScheduled: isCurrentPostScheduled(),
Expand Down
10 changes: 3 additions & 7 deletions packages/editor/src/components/post-schedule/check.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
/**
* External dependencies
*/
import { get } from 'lodash';

/**
* WordPress dependencies
*/
Expand All @@ -19,9 +14,10 @@ export function PostScheduleCheck( { hasPublishAction, children } ) {

export default compose( [
withSelect( ( select ) => {
const { getCurrentPost, getCurrentPostType } = select( 'core/editor' );
const { getCurrentPostId, getCurrentPostType } = select( 'core/editor' );
const { canUser } = select( 'core' );
return {
hasPublishAction: get( getCurrentPost(), [ '_links', 'wp:action-publish' ], false ),
hasPublishAction: canUser( 'publish', 'posts', getCurrentPostId() ),
postType: getCurrentPostType(),
};
} ),
Expand Down
12 changes: 4 additions & 8 deletions packages/editor/src/components/post-sticky/check.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
/**
* External dependencies
*/
import { get } from 'lodash';

/**
* WordPress dependencies
*/
Expand All @@ -22,10 +17,11 @@ export function PostStickyCheck( { hasStickyAction, postType, children } ) {

export default compose( [
withSelect( ( select ) => {
const post = select( 'core/editor' ).getCurrentPost();
const { canUser } = select( 'core' );
const { getCurrentPostId, getCurrentPostType } = select( 'core/editor' );
return {
hasStickyAction: get( post, [ '_links', 'wp:action-sticky' ], false ),
postType: select( 'core/editor' ).getCurrentPostType(),
hasStickyAction: canUser( 'sticky', 'posts', getCurrentPostId() ),
postType: getCurrentPostType(),
};
} ),
] )( PostStickyCheck );
Loading