Skip to content

Commit

Permalink
Core Data: Resolve entity collection user permissions (#64504)
Browse files Browse the repository at this point in the history
Co-authored-by: Mamaduka <mamaduka@git.wordpress.org>
Co-authored-by: TimothyBJacobs <timothyblynjacobs@git.wordpress.org>
Co-authored-by: swissspidy <swissspidy@git.wordpress.org>
Co-authored-by: youknowriad <youknowriad@git.wordpress.org>
Co-authored-by: tyxla <tyxla@git.wordpress.org>
Co-authored-by: spacedmonkey <spacedmonkey@git.wordpress.org>
  • Loading branch information
7 people committed Aug 27, 2024
1 parent 7c73e6e commit 6be14bb
Show file tree
Hide file tree
Showing 9 changed files with 322 additions and 12 deletions.
3 changes: 3 additions & 0 deletions backport-changelog/6.7/7139.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
https://github.com/WordPress/wordpress-develop/pull/7139

* https://github.com/WordPress/gutenberg/pull/64504
169 changes: 169 additions & 0 deletions lib/compat/wordpress-6.7/class-gutenberg-rest-server.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
<?php
/**
* A custom REST server for Gutenberg.
*
* @package gutenberg
* @since 6.7.0
*/

class Gutenberg_REST_Server extends WP_REST_Server {
/**
* Converts a response to data to send.
*
* @since 4.4.0
* @since 5.4.0 The `$embed` parameter can now contain a list of link relations to include.
*
* @param WP_REST_Response $response Response object.
* @param bool|string[] $embed Whether to embed all links, a filtered list of link relations, or no links.
* @return array {
* Data with sub-requests embedded.
*
* @type array $_links Links.
* @type array $_embedded Embedded objects.
* }
*/
// @core-merge: Do not merge. The method is copied here to fix the inheritance issue.
public function response_to_data( $response, $embed ) {
$data = $response->get_data();
$links = static::get_compact_response_links( $response );

if ( ! empty( $links ) ) {
// Convert links to part of the data.
$data['_links'] = $links;
}

if ( $embed ) {
$this->embed_cache = array();
// Determine if this is a numeric array.
if ( wp_is_numeric_array( $data ) ) {
foreach ( $data as $key => $item ) {
$data[ $key ] = $this->embed_links( $item, $embed );
}
} else {
$data = $this->embed_links( $data, $embed );
}
$this->embed_cache = array();
}

return $data;
}

/**
* Retrieves links from a response.
*
* Extracts the links from a response into a structured hash, suitable for
* direct output.
*
* @since 4.4.0
* @since 6.7.0 The `targetHints` property to the `self` link object was added.
*
* @param WP_REST_Response $response Response to extract links from.
* @return array Map of link relation to list of link hashes.
*/
public static function get_response_links( $response ) {
$links = $response->get_links();

if ( empty( $links ) ) {
return array();
}

$server = rest_get_server();

// Convert links to part of the data.
$data = array();
foreach ( $links as $rel => $items ) {
$data[ $rel ] = array();

foreach ( $items as $item ) {
$attributes = $item['attributes'];
$attributes['href'] = $item['href'];

if ( 'self' !== $rel ) {
$data[ $rel ][] = $attributes;
continue;
}

// Prefer targetHints that were specifically designated by the developer.
if ( isset( $attributes['targetHints']['allow'] ) ) {
$data[ $rel ][] = $attributes;
continue;
}

$request = WP_REST_Request::from_url( $item['href'] );
if ( ! $request ) {
$data[ $rel ][] = $attributes;
continue;
}

$match = $server->match_request_to_handler( $request );
if ( ! is_wp_error( $match ) ) {
$response = new WP_REST_Response();
$response->set_matched_route( $match[0] );
$response->set_matched_handler( $match[1] );
$headers = rest_send_allow_header( $response, $server, $request )->get_headers();

foreach ( $headers as $name => $value ) {
$name = WP_REST_Request::canonicalize_header_name( $name );
$attributes['targetHints'][ $name ] = array_map( 'trim', explode( ',', $value ) );
}
}

$data[ $rel ][] = $attributes;
}
}

return $data;
}

/**
* Retrieves the CURIEs (compact URIs) used for relations.
*
* Extracts the links from a response into a structured hash, suitable for
* direct output.
*
* @since 4.5.0
*
* @param WP_REST_Response $response Response to extract links from.
* @return array Map of link relation to list of link hashes.
*/
// @core-merge: Do not merge. The method is copied here to fix the inheritance issue.
public static function get_compact_response_links( $response ) {
$links = static::get_response_links( $response );

if ( empty( $links ) ) {
return array();
}

$curies = $response->get_curies();
$used_curies = array();

foreach ( $links as $rel => $items ) {

// Convert $rel URIs to their compact versions if they exist.
foreach ( $curies as $curie ) {
$href_prefix = substr( $curie['href'], 0, strpos( $curie['href'], '{rel}' ) );
if ( ! str_starts_with( $rel, $href_prefix ) ) {
continue;
}

// Relation now changes from '$uri' to '$curie:$relation'.
$rel_regex = str_replace( '\{rel\}', '(.+)', preg_quote( $curie['href'], '!' ) );
preg_match( '!' . $rel_regex . '!', $rel, $matches );
if ( $matches ) {
$new_rel = $curie['name'] . ':' . $matches[1];
$used_curies[ $curie['name'] ] = $curie;
$links[ $new_rel ] = $items;
unset( $links[ $rel ] );
break;
}
}
}

// Push the curies onto the start of the links array.
if ( $used_curies ) {
$links['curies'] = array_values( $used_curies );
}

return $links;
}
}
10 changes: 10 additions & 0 deletions lib/compat/wordpress-6.7/rest-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,13 @@ function gutenberg_register_wp_rest_templates_controller_plugin_field() {
);
}
add_action( 'rest_api_init', 'gutenberg_register_wp_rest_templates_controller_plugin_field' );

/**
* Overrides the default 'WP_REST_Server' class.
*
* @return string The name of the custom server class.
*/
function gutenberg_override_default_rest_server() {
return 'Gutenberg_REST_Server';
}
add_filter( 'wp_rest_server_class', 'gutenberg_override_default_rest_server', 1 );
1 change: 1 addition & 0 deletions lib/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ function gutenberg_is_experiment_enabled( $name ) {

// WordPress 6.7 compat.
require __DIR__ . '/compat/wordpress-6.7/class-gutenberg-rest-templates-controller-6-7.php';
require __DIR__ . '/compat/wordpress-6.7/class-gutenberg-rest-server.php';
require __DIR__ . '/compat/wordpress-6.7/rest-api.php';

// Plugin specific code.
Expand Down
50 changes: 45 additions & 5 deletions packages/core-data/src/resolvers.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
forwardResolver,
getNormalizedCommaSeparable,
getUserPermissionCacheKey,
getUserPermissionsFromResponse,
getUserPermissionsFromAllowHeader,
ALLOWED_RESOURCE_ACTIONS,
} from './utils';
import { getSyncProvider } from './sync';
Expand Down Expand Up @@ -173,7 +173,9 @@ export const getEntityRecord =

const response = await apiFetch( { path, parse: false } );
const record = await response.json();
const permissions = getUserPermissionsFromResponse( response );
const permissions = getUserPermissionsFromAllowHeader(
response.headers?.get( 'allow' )
);

registry.batch( () => {
dispatch.receiveEntityRecords( kind, name, record, query );
Expand Down Expand Up @@ -299,19 +301,52 @@ export const getEntityRecords =
meta
);

// When requesting all fields, the list of results can be used to
// resolve the `getEntityRecord` selector in addition to `getEntityRecords`.
// When requesting all fields, the list of results can be used to resolve
// the `getEntityRecord` and `canUser` selectors in addition to `getEntityRecords`.
// See https://github.com/WordPress/gutenberg/pull/26575
// See https://github.com/WordPress/gutenberg/pull/64504
if ( ! query?._fields && ! query.context ) {
const key = entityConfig.key || DEFAULT_ENTITY_KEY;
const resolutionsArgs = records
.filter( ( record ) => record?.[ key ] )
.map( ( record ) => [ kind, name, record[ key ] ] );

const targetHints = records
.filter( ( record ) => record?.[ key ] )
.map( ( record ) => ( {
id: record[ key ],
permissions: getUserPermissionsFromAllowHeader(
record?._links?.self?.[ 0 ].targetHints.allow
),
} ) );

const canUserResolutionsArgs = [];
for ( const targetHint of targetHints ) {
for ( const action of ALLOWED_RESOURCE_ACTIONS ) {
canUserResolutionsArgs.push( [
action,
{ kind, name, id: targetHint.id },
] );

dispatch.receiveUserPermission(
getUserPermissionCacheKey( action, {
kind,
name,
id: targetHint.id,
} ),
targetHint.permissions[ action ]
);
}
}

dispatch.finishResolutions(
'getEntityRecord',
resolutionsArgs
);
dispatch.finishResolutions(
'canUser',
canUserResolutionsArgs
);
}

dispatch.__unstableReleaseStoreLock( lock );
Expand Down Expand Up @@ -440,7 +475,12 @@ export const canUser =
return;
}

const permissions = getUserPermissionsFromResponse( response );
// Optional chaining operator is used here because the API requests don't
// return the expected result in the React native version. Instead, API requests
// only return the result, without including response properties like the headers.
const permissions = getUserPermissionsFromAllowHeader(
response.headers?.get( 'allow' )
);
registry.batch( () => {
for ( const action of ALLOWED_RESOURCE_ACTIONS ) {
const key = getUserPermissionCacheKey( action, resource, id );
Expand Down
1 change: 1 addition & 0 deletions packages/core-data/src/test/resolvers.js
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ describe( 'getEntityRecords', () => {
const finishResolutions = jest.fn();
const dispatch = Object.assign( jest.fn(), {
receiveEntityRecords: jest.fn(),
receiveUserPermission: jest.fn(),
__unstableAcquireStoreLock: jest.fn(),
__unstableReleaseStoreLock: jest.fn(),
finishResolutions,
Expand Down
2 changes: 1 addition & 1 deletion packages/core-data/src/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ export { default as getNestedValue } from './get-nested-value';
export { default as isNumericID } from './is-numeric-id';
export {
getUserPermissionCacheKey,
getUserPermissionsFromResponse,
getUserPermissionsFromAllowHeader,
ALLOWED_RESOURCE_ACTIONS,
} from './user-permissions';
10 changes: 4 additions & 6 deletions packages/core-data/src/utils/user-permissions.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,11 @@ export const ALLOWED_RESOURCE_ACTIONS = [
'delete',
];

export function getUserPermissionsFromResponse( response ) {
export function getUserPermissionsFromAllowHeader( allowedMethods ) {
const permissions = {};

// Optional chaining operator is used here because the API requests don't
// return the expected result in the React native version. Instead, API requests
// only return the result, without including response properties like the headers.
const allowedMethods = response.headers?.get( 'allow' ) || '';
if ( ! allowedMethods ) {
return permissions;
}

const methods = {
create: 'POST',
Expand Down
Loading

0 comments on commit 6be14bb

Please sign in to comment.