diff --git a/docs/reference-guides/data/data-core.md b/docs/reference-guides/data/data-core.md index ba77f065584cfe..bd35fb87beb9ce 100644 --- a/docs/reference-guides/data/data-core.md +++ b/docs/reference-guides/data/data-core.md @@ -550,6 +550,26 @@ _Returns_ - `boolean`: True if the REST request was completed. False otherwise. +### hasPermission + +Returns whether the current user can perform the given action on the entity record. + +Calling this may trigger an OPTIONS request to the REST API via the `hasPermission()` resolver. + + + +_Parameters_ + +- _state_ `State`: Data state. +- _action_ `string`: Action to check. One of: 'create', 'read', 'update', 'delete'. +- _kind_ `string`: Entity kind. +- _name_ `string`: Entity name +- _key_ `EntityRecordKey`: Optional record's key. + +_Returns_ + +- `boolean | undefined`: Whether or not the user can perform the action, or `undefined` if the OPTIONS request is still being made. + ### hasRedo Returns true if there is a next edit from the current undo offset for the entity records edits history, and false otherwise. diff --git a/packages/core-data/README.md b/packages/core-data/README.md index 694f780dafb99d..50ce5957eb2633 100644 --- a/packages/core-data/README.md +++ b/packages/core-data/README.md @@ -871,6 +871,26 @@ _Returns_ - `boolean`: True if the REST request was completed. False otherwise. +### hasPermission + +Returns whether the current user can perform the given action on the entity record. + +Calling this may trigger an OPTIONS request to the REST API via the `hasPermission()` resolver. + + + +_Parameters_ + +- _state_ `State`: Data state. +- _action_ `string`: Action to check. One of: 'create', 'read', 'update', 'delete'. +- _kind_ `string`: Entity kind. +- _name_ `string`: Entity name +- _key_ `EntityRecordKey`: Optional record's key. + +_Returns_ + +- `boolean | undefined`: Whether or not the user can perform the action, or `undefined` if the OPTIONS request is still being made. + ### hasRedo Returns true if there is a next edit from the current undo offset for the entity records edits history, and false otherwise. diff --git a/packages/core-data/src/resolvers.js b/packages/core-data/src/resolvers.js index 5eb6fd8642c75e..d5b4a5db764ee2 100644 --- a/packages/core-data/src/resolvers.js +++ b/packages/core-data/src/resolvers.js @@ -424,6 +424,90 @@ export const canUser = } ); }; +/** + * Checks whether the current user can perform the given action on the entity record. + * + * @param {string} action Action to check. One of: 'create', 'read', 'update', 'delete'. + * @param {string} kind Entity kind. + * @param {string} name Entity name + * @param {number|string} key Optional record's key. + */ +export const hasPermission = + ( action, kind, name, key ) => + async ( { dispatch, registry } ) => { + const configs = await dispatch( getOrLoadEntitiesConfig( kind, name ) ); + const entityConfig = configs.find( + ( config ) => config.name === name && config.kind === kind + ); + if ( ! entityConfig ) { + return; + } + + const { hasStartedResolution } = registry.select( STORE_NAME ); + const supportedActions = [ 'create', 'read', 'update', 'delete' ]; + + if ( ! supportedActions.includes( action ) ) { + throw new Error( `'${ action }' is not a valid action.` ); + } + + // Prevent resolving the same resource twice. + for ( const relatedAction of supportedActions ) { + if ( relatedAction === action ) { + continue; + } + const isAlreadyResolving = hasStartedResolution( 'hasPermission', [ + relatedAction, + kind, + name, + key, + ] ); + if ( isAlreadyResolving ) { + return; + } + } + + let response; + try { + response = await apiFetch( { + path: entityConfig.baseURL + ( key ? '/' + key : '' ), + method: 'OPTIONS', + parse: false, + } ); + } 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; + } + + // Optional chaining operator is used here because the API requests don't + // return the expected result in the native version. Instead, API requests + // only return the result, without including response properties like the headers. + const allowHeader = response.headers?.get( 'allow' ); + const allowedMethods = allowHeader?.allow || allowHeader || ''; + + const permissions = {}; + const methods = { + create: 'POST', + read: 'GET', + update: 'PUT', + delete: 'DELETE', + }; + for ( const [ actionName, methodName ] of Object.entries( methods ) ) { + permissions[ actionName ] = allowedMethods.includes( methodName ); + } + + registry.batch( () => { + for ( const supportedAction of supportedActions ) { + dispatch.receiveUserPermission( + [ supportedAction, kind, name, key ] + .filter( Boolean ) + .join( '/' ), + permissions[ supportedAction ] + ); + } + } ); + }; + /** * Checks whether the current user can perform the given action on the given * REST resource. diff --git a/packages/core-data/src/selectors.ts b/packages/core-data/src/selectors.ts index 425a537cad7363..e110ed4e63bcd2 100644 --- a/packages/core-data/src/selectors.ts +++ b/packages/core-data/src/selectors.ts @@ -1182,6 +1182,34 @@ export function canUserEditEntityRecord( return canUser( state, 'update', resource, recordId ); } +/** + * Returns whether the current user can perform the given action on the entity record. + * + * Calling this may trigger an OPTIONS request to the REST API via the + * `hasPermission()` resolver. + * + * https://developer.wordpress.org/rest-api/reference/ + * + * @param state Data state. + * @param action Action to check. One of: 'create', 'read', 'update', 'delete'. + * @param kind Entity kind. + * @param name Entity name + * @param key Optional record's key. + * + * @return Whether or not the user can perform the action, + * or `undefined` if the OPTIONS request is still being made. + */ +export function hasPermission( + state: State, + action: string, + kind: string, + name: string, + key?: EntityRecordKey +): boolean | undefined { + const cacheKey = [ action, kind, name, key ].filter( Boolean ).join( '/' ); + return state.userPermissions[ cacheKey ]; +} + /** * Returns the latest autosaves for the post. *