Skip to content

Commit

Permalink
Patterns: fix capabilities settings for pattern categories (#55379)
Browse files Browse the repository at this point in the history
Co-authored-by: Daniel Richards <daniel.richards@automattic.com>
  • Loading branch information
glendaviesnz and talldan authored Oct 20, 2023
1 parent 6a2c92b commit 5b10d27
Show file tree
Hide file tree
Showing 14 changed files with 483 additions and 96 deletions.
23 changes: 12 additions & 11 deletions lib/compat/wordpress-6.4/block-patterns.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,21 @@
*/
function gutenberg_register_taxonomy_patterns() {
$args = array(
'public' => true,
'publicly_queryable' => false,
'hierarchical' => false,
'labels' => array(
'public' => true,
'publicly_queryable' => false,
'hierarchical' => false,
'labels' => array(
'name' => _x( 'Pattern Categories', 'taxonomy general name' ),
'singular_name' => _x( 'Pattern Category', 'taxonomy singular name' ),
),
'query_var' => false,
'rewrite' => false,
'show_ui' => true,
'_builtin' => true,
'show_in_nav_menus' => false,
'show_in_rest' => true,
'show_admin_column' => true,
'query_var' => false,
'rewrite' => false,
'show_ui' => true,
'_builtin' => true,
'show_in_nav_menus' => false,
'show_in_rest' => true,
'show_admin_column' => true,
'rest_controller_class' => 'Gutenberg_REST_Pattern_Categories_Controller',
);
register_taxonomy( 'wp_pattern_category', array( 'wp_block' ), $args );
}
Expand Down
24 changes: 24 additions & 0 deletions lib/compat/wordpress-6.4/blocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,27 @@ function gutenberg_add_custom_capabilities_to_wp_block( $args ) {
return $args;
}
add_filter( 'register_wp_block_post_type_args', 'gutenberg_add_custom_capabilities_to_wp_block', 10, 1 );

/**
* Updates the wp_block REST enpoint in order to modify the wp_pattern_category action
* links that are returned because as although the taxonomy is flat Author level users
* are only allowed to assign categories.
*
* Note: This should be removed when the minimum required WP version is >= 6.4.
*
* @see https://github.com/WordPress/gutenberg/pull/55379
*
* @param array $args Register post type args.
* @param string $post_type The post type string.
*
* @return array Register post type args.
*/
function gutenberg_update_patterns_block_rest_controller_class( $args, $post_type ) {
if ( 'wp_block' === $post_type ) {
$args['rest_controller_class'] = 'Gutenberg_REST_Blocks_Controller_6_4';
}

return $args;
}

add_filter( 'register_post_type_args', 'gutenberg_update_patterns_block_rest_controller_class', 11, 2 );
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php
/**
* Reusable blocks REST API: WP_REST_Blocks_Controller class
*
* @package WordPress
* @subpackage REST_API
* @since 5.0.0
*/

/**
* Controller which provides a REST endpoint for the editor to read, create,
* edit and delete reusable blocks. Blocks are stored as posts with the wp_block
* post type.
*
* @since 5.0.0
*
* @see WP_REST_Posts_Controller
* @see WP_REST_Controller
*/
class Gutenberg_REST_Blocks_Controller_6_4 extends Gutenberg_REST_Blocks_Controller {
/**
* Gets the link relations available for the post and current user.
*
* @since 6.4.0 Ensures that only users with `edit_terms` capability can add taxonomy terms.
*
* @param WP_Post $post Post object.
* @param WP_REST_Request $request Request object.
* @return array List of link relations.
*/
protected function get_available_actions( $post, $request ) {
if ( 'edit' !== $request['context'] ) {
return array();
}

$rels = array();

$post_type = get_post_type_object( $post->post_type );

if ( 'attachment' !== $this->post_type && current_user_can( $post_type->cap->publish_posts ) ) {
$rels[] = 'https://api.w.org/action-publish';
}

if ( current_user_can( 'unfiltered_html' ) ) {
$rels[] = 'https://api.w.org/action-unfiltered-html';
}

if ( 'post' === $post_type->name ) {
if ( current_user_can( $post_type->cap->edit_others_posts ) && current_user_can( $post_type->cap->publish_posts ) ) {
$rels[] = 'https://api.w.org/action-sticky';
}
}

if ( post_type_supports( $post_type->name, 'author' ) ) {
if ( current_user_can( $post_type->cap->edit_others_posts ) ) {
$rels[] = 'https://api.w.org/action-assign-author';
}
}

$taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );

foreach ( $taxonomies as $tax ) {
$tax_base = ! empty( $tax->rest_base ) ? $tax->rest_base : $tax->name;

if ( current_user_can( $tax->cap->edit_terms ) ) {
$rels[] = 'https://api.w.org/action-create-' . $tax_base;
}

if ( current_user_can( $tax->cap->assign_terms ) ) {
$rels[] = 'https://api.w.org/action-assign-' . $tax_base;
}
}

return $rels;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php
/**
* Gutenberg_REST_Pattern_Categories_Controller class
*
* Extends the WP_REST_Terms_Controller to handle permissions of pattern categories.
*
* @package Gutenberg
* @subpackage REST_API
* @since 6.4.0
*/

/**
* Extends the WP_REST_Terms_Controller to handle permissions of pattern categories.
*
* @access public
*/
class Gutenberg_REST_Pattern_Categories_Controller extends WP_REST_Terms_Controller {
/**
* Make pattern categories behave more like a hierarchical taxonomy in terms of permissions.
* Check the edit_terms cap to see whether term creation is possible.
*
* @since 6.4.0
*
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
public function create_item_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
if ( ! $this->check_is_taxonomy_allowed( $this->taxonomy ) ) {
return false;
}

$taxonomy_obj = get_taxonomy( $this->taxonomy );

// Patterns categories are a flat hierarchy (like tags), but work more like post categories in terms of permissions.
if ( ! current_user_can( $taxonomy_obj->cap->edit_terms ) ) {
return new WP_Error(
'rest_cannot_create',
__( 'Sorry, you are not allowed to create terms in this taxonomy.' ),
array( 'status' => rest_authorization_required_code() )
);
}

return true;
}
}
2 changes: 2 additions & 0 deletions lib/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ function gutenberg_is_experiment_enabled( $name ) {
// WordPress 6.4 compat.
require_once __DIR__ . '/compat/wordpress-6.4/class-gutenberg-rest-global-styles-revisions-controller-6-4.php';
require_once __DIR__ . '/compat/wordpress-6.4/class-gutenberg-rest-block-patterns-controller.php';
require_once __DIR__ . '/compat/wordpress-6.4/class-gutenberg-rest-blocks-controller-6-4.php';
require_once __DIR__ . '/compat/wordpress-6.4/class-gutenberg-rest-pattern-categories-controller.php';
require_once __DIR__ . '/compat/wordpress-6.4/rest-api.php';
require_once __DIR__ . '/compat/wordpress-6.4/theme-previews.php';

Expand Down
1 change: 1 addition & 0 deletions packages/core-data/src/resolvers.js
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,7 @@ export const getUserPatternCategories =
{
per_page: -1,
_fields: 'id,name,description,slug',
context: 'view',
}
);

Expand Down
1 change: 1 addition & 0 deletions packages/editor/src/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export {
export { default as PostTaxonomies } from './post-taxonomies';
export { FlatTermSelector as PostTaxonomiesFlatTermSelector } from './post-taxonomies/flat-term-selector';
export { HierarchicalTermSelector as PostTaxonomiesHierarchicalTermSelector } from './post-taxonomies/hierarchical-term-selector';
export { PatternCategoriesSelector as PostPatternCategoriesSelector } from './post-taxonomies/pattern-categories-selector';
export { default as PostTaxonomiesCheck } from './post-taxonomies/check';
export { default as PostTextEditor } from './post-text-editor';
export { default as PostTitle } from './post-title';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/**
* WordPress dependencies
*/
import { useSelect, useDispatch } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';
import { addFilter } from '@wordpress/hooks';
import { privateApis as patternsPrivateApis } from '@wordpress/patterns';

/**
* Internal dependencies
*/
import { unlock } from '../../lock-unlock';
import { store as editorStore } from '../../store';

const { CategorySelector } = unlock( patternsPrivateApis );

const EMPTY_ARRAY = [];

const DEFAULT_QUERY = {
per_page: -1,
orderby: 'name',
order: 'asc',
_fields: 'id,name,parent',
context: 'view',
};

/*
* Pattern categories are a flat taxonomy but do not allow Author users and below to create
* new categories, so this selector overrides the default flat taxonomy selector for
* wp_block post types and users without 'create' capability for wp_pattern_category.
*/
export function PatternCategoriesSelector( { slug } ) {
const { hasAssignAction, terms, availableTerms, taxonomy, loading } =
useSelect(
( select ) => {
const { getCurrentPost, getEditedPostAttribute } =
select( editorStore );
const { getTaxonomy, getEntityRecords, isResolving } =
select( coreStore );
const _taxonomy = getTaxonomy( slug );
const post = getCurrentPost();

return {
hasAssignAction: _taxonomy
? post._links?.[
'wp:action-assign-' + _taxonomy.rest_base
] ?? false
: false,
terms: _taxonomy
? getEditedPostAttribute( _taxonomy.rest_base )
: EMPTY_ARRAY,
loading: isResolving( 'getEntityRecords', [
'taxonomy',
slug,
DEFAULT_QUERY,
] ),
availableTerms:
getEntityRecords( 'taxonomy', slug, DEFAULT_QUERY ) ||
EMPTY_ARRAY,
taxonomy: _taxonomy,
};
},
[ slug ]
);

const { editPost } = useDispatch( editorStore );

if ( ! hasAssignAction || loading || availableTerms.length === 0 ) {
return null;
}

const onUpdateTerms = ( termIds ) => {
editPost( { [ taxonomy.rest_base ]: termIds } );
};

const onChange = ( term ) => {
const hasTerm = terms.includes( term.id );
const newTerms = hasTerm
? terms.filter( ( id ) => id !== term.id )
: [ ...terms, term.id ];
onUpdateTerms( newTerms );
};

const isCategorySelected = ( term ) => terms.includes( term.id );

const categoryOptions = availableTerms.map( ( term ) => ( {
...term,
label: term.name,
} ) );

return (
<CategorySelector
onChange={ onChange }
categoryOptions={ categoryOptions }
isCategorySelected={ isCategorySelected }
showLabel={ false }
/>
);
}

export default function patternCategorySelector( OriginalComponent ) {
return function ( props ) {
const canAddCategories = useSelect( ( select ) => {
const { canUser } = select( coreStore );
return canUser( 'create', 'wp_pattern_category' );
} );
if ( props.slug === 'wp_pattern_category' && ! canAddCategories ) {
return <PatternCategoriesSelector { ...props } />;
}

return <OriginalComponent { ...props } />;
};
}

addFilter(
'editor.PostTaxonomyType',
'core/pattern-category-selector',
patternCategorySelector
);
Loading

1 comment on commit 5b10d27

@github-actions
Copy link

Choose a reason for hiding this comment

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

Flaky tests detected in 5b10d27.
Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/6585743306
📝 Reported issues:

Please sign in to comment.