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

Add ability to preview template in post editor for non administrators #58301

Closed
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php
/**
* REST API: Gutenberg_REST_Templates_Controller_6_6 class
*
* @package gutenberg
*/

/**
* Gutenberg_REST_Templates_Controller_6_6 class
*
* Templates and template parts currently only allow access to administrators with the
* `edit_theme_options` capability. In order to allow other roles to also view the templates,
* we need to override the permissions check for the REST API endpoints.
*/
class Gutenberg_REST_Templates_Controller_6_6 extends Gutenberg_REST_Templates_Controller_6_4 {

/**
* Checks if a given request has access to read templates.
*
* @since 6.6
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access, WP_Error object otherwise.
*/
public function get_items_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
/*
* Allow access to anyone who can edit posts.
*/
if ( ! current_user_can( 'edit_posts' ) ) {
return new WP_Error(
'rest_cannot_manage_templates',
__( 'Sorry, you are not allowed to access the templates on this site.', 'default' ),
array(
'status' => rest_authorization_required_code(),
)
);
}

return true;
}

/**
* Checks if a given request has access to read templates.
*
* @since 6.6
*
* @param WP_REST_Request $request Full details about the request.
* @return true|WP_Error True if the request has read access, WP_Error object otherwise.
*/
public function get_item_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
/*
* Allow access to anyone who can edit posts.
*/
if ( ! current_user_can( 'edit_posts' ) ) {
return new WP_Error(
'rest_cannot_manage_templates',
__( 'Sorry, you are not allowed to access the templates on this site.', 'default' ),
array(
'status' => rest_authorization_required_code(),
)
);
}

return true;
}
}
31 changes: 31 additions & 0 deletions lib/compat/wordpress-6.6/rest-api.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php
/**
* PHP and WordPress configuration compatibility functions for the Gutenberg
* editor plugin changes related to REST API.
*
* @package gutenberg
*/

if ( ! defined( 'ABSPATH' ) ) {
die( 'Silence is golden.' );
}

if ( ! function_exists( 'wp_api_template_access_controller' ) ) {
/**
* Hook in to the template and template part post types and modify the
* access control for the rest endpoint to allow lower user roles to access
* the templates and template parts.
*
* @param array $args Current registered post type args.
* @param string $post_type Name of post type.
*
* @return array
*/
function wp_api_template_access_controller( $args, $post_type ) {
if ( 'wp_template' === $post_type || 'wp_template_part' === $post_type ) {
$args['rest_controller_class'] = 'Gutenberg_REST_Templates_Controller_6_6';
}
return $args;
}
}
add_filter( 'register_post_type_args', 'wp_api_template_access_controller', 10, 2 );
4 changes: 4 additions & 0 deletions lib/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ function gutenberg_is_experiment_enabled( $name ) {
require_once __DIR__ . '/compat/wordpress-6.5/class-gutenberg-rest-global-styles-revisions-controller-6-5.php';
require_once __DIR__ . '/compat/wordpress-6.5/rest-api.php';

// WordPress 6.6 compat.
require_once __DIR__ . '/compat/wordpress-6.6/class-gutenberg-rest-templates-controller-6-6.php';
require_once __DIR__ . '/compat/wordpress-6.6/rest-api.php';

// Plugin specific code.
require_once __DIR__ . '/class-wp-rest-global-styles-controller-gutenberg.php';
require_once __DIR__ . '/rest-api.php';
Expand Down
62 changes: 61 additions & 1 deletion packages/block-library/src/template-part/edit/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
useBlockProps,
Warning,
store as blockEditorStore,
useInnerBlocksProps,
useSettings,
RecursionProvider,
useHasRecursion,
InspectorControls,
Expand All @@ -15,7 +17,7 @@ import {
import { PanelBody, Spinner, Modal, MenuItem } from '@wordpress/components';
import { useAsyncList } from '@wordpress/compose';
import { __, sprintf } from '@wordpress/i18n';
import { store as coreStore } from '@wordpress/core-data';
import { store as coreStore, useEntityBlockEditor } from '@wordpress/core-data';
import { useState } from '@wordpress/element';
import { store as noticesStore } from '@wordpress/notices';

Expand Down Expand Up @@ -88,12 +90,53 @@ function TemplatesList( { availableTemplates, onSelect } ) {
);
}

function NonEditableTemplatePartPreview( {
Copy link
Member Author

Choose a reason for hiding this comment

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

This preview component does not handle the case where a template part is missing

postId: id,
layout,
tagName: TagName,
blockProps,
} ) {
const themeSupportsLayout = useSelect( ( select ) => {
const { getSettings } = select( blockEditorStore );
return getSettings()?.supportsLayout;
}, [] );
const [ defaultLayout ] = useSettings( 'layout' );
const usedLayout = layout?.inherit ? defaultLayout || {} : layout;

const [ blocks ] = useEntityBlockEditor( 'postType', 'wp_template_part', {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can't we just retrieve the "blocks" property from the entity without using this hook.

id,
context: 'view',
} );

const innerBlocksProps = useInnerBlocksProps( blockProps, {
value: blocks,
onChange: () => {},
onInput: () => {},
renderAppender: undefined,
layout: themeSupportsLayout ? usedLayout : undefined,
} );

return <TagName { ...innerBlocksProps } />;
}

export default function TemplatePartEdit( {
attributes,
setAttributes,
clientId,
} ) {
const { createSuccessNotice } = useDispatch( noticesStore );
const { canEditTemplatePart, canViewTemplatePart } = useSelect(
( select ) => ( {
canEditTemplatePart:
select( coreStore ).canUser( 'create', 'template-parts' ) ??
false,
canViewTemplatePart:
select( coreStore ).canUser( 'read', 'template-parts' ) ??
false,
} ),
[]
);

const currentTheme = useSelect(
( select ) => select( coreStore ).getCurrentTheme()?.stylesheet,
[]
Expand Down Expand Up @@ -161,6 +204,23 @@ export default function TemplatePartEdit( {
setAttributes
);

if ( ! canEditTemplatePart && canViewTemplatePart ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

What happens if you can't view or edit, is there even a world where you can't view a template part? Aren't these public?

Copy link
Member Author

@fabiankaegy fabiankaegy Feb 14, 2024

Choose a reason for hiding this comment

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

Curretly Template parts have the same rest restrictions as templates. They are both locked down so only admins can view them.

With my update template parts and templates can now be viewed by any user that has the capability to edit_posts. Which is every user that also can open the editor in the first place. So there will never be a cicrumstance where someone cannot view the template

return (
<RecursionProvider uniqueId={ templatePartId }>
<NonEditableTemplatePartPreview
attributes={ attributes }
setAttributes={ setAttributes }
clientId={ clientId }
tagName={ TagName }
blockProps={ blockProps }
postId={ templatePartId }
hasInnerBlocks={ hasInnerBlocks }
layout={ layout }
/>
</RecursionProvider>
);
}

// We don't want to render a missing state if we have any inner blocks.
// A new template part is automatically created if we have any inner blocks but no entity.
if (
Expand Down
17 changes: 12 additions & 5 deletions packages/core-data/src/entity-provider.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,14 +145,19 @@ const parsedBlocksCache = new WeakMap();
* `BlockEditorProvider` and are intended to be used with it,
* or similar components or hooks.
*
* @param {string} kind The entity kind.
* @param {string} name The entity name.
* @param {string} kind The entity kind.
* @param {string} name The entity name.
* @param {Object} options
* @param {string} [options.id] An entity ID to use instead of the context-provided one.
* @param {string} [options.id] An entity ID to use instead of the context-provided one.
* @param {string} [options.context] The context param to be passed to the REST API.
*
* @return {[WPBlock[], Function, Function]} The block array and setters.
*/
export function useEntityBlockEditor( kind, name, { id: _id } = {} ) {
export function useEntityBlockEditor(
kind,
name,
{ id: _id, context = 'edit' } = {}
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't like the context prop too much here. It's also weird to pass context: view to getEditedEntityRecord.

Copy link
Contributor

Choose a reason for hiding this comment

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

I actually think this hook is probably not for "previewing" but should only be used for "editing".

) {
const providerId = useEntityId( kind, name );
const id = _id ?? providerId;
const { getEntityRecord, getEntityRecordEdits } = useSelect( STORE_NAME );
Expand All @@ -162,7 +167,9 @@ export function useEntityBlockEditor( kind, name, { id: _id } = {} ) {
return {};
}
const { getEditedEntityRecord } = select( STORE_NAME );
const editedRecord = getEditedEntityRecord( kind, name, id );
const editedRecord = getEditedEntityRecord( kind, name, id, {
context,
} );
return {
editedBlocks: editedRecord.blocks,
content: editedRecord.content,
Expand Down
4 changes: 2 additions & 2 deletions packages/edit-post/src/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,12 @@ function Editor( {
getEditorSettings().supportsTemplateMode;
const isViewable =
getPostType( currentPost.postType )?.viewable ?? false;
const canEditTemplate = canUser( 'create', 'templates' );
const canViewTemplate = canUser( 'read', 'templates' );
return {
template:
supportsTemplateMode &&
isViewable &&
canEditTemplate &&
canViewTemplate &&
currentPost.postType !== 'wp_template'
? getEditedPostTemplate()
: null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* WordPress dependencies
*/
import { useSelect, useDispatch } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';
import { useEffect, useState, useRef } from '@wordpress/element';
import { store as noticesStore } from '@wordpress/notices';
import { __ } from '@wordpress/i18n';
Expand Down Expand Up @@ -42,12 +43,20 @@ export default function EditTemplateBlocksNotification( { contentRef } ) {

const { createInfoNotice, removeNotice } = useDispatch( noticesStore );

const canEditTemplate = useSelect(
( select ) =>
select( coreStore ).canUser( 'create', 'templates' ) ?? false
);

const [ isDialogOpen, setIsDialogOpen ] = useState( false );

const lastNoticeId = useRef( 0 );

useEffect( () => {
const handleClick = async ( event ) => {
if ( ! canEditTemplate ) {
return;
}
if ( ! event.target.classList.contains( 'is-root-container' ) ) {
return;
}
Expand Down Expand Up @@ -104,8 +113,13 @@ export default function EditTemplateBlocksNotification( { contentRef } ) {
onNavigateToEntityRecord,
templateId,
removeNotice,
canEditTemplate,
] );

if ( ! canEditTemplate ) {
return null;
}

return (
<ConfirmDialog
isOpen={ isDialogOpen }
Expand Down
55 changes: 32 additions & 23 deletions packages/editor/src/components/post-template/block-theme.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useSelect, useDispatch } from '@wordpress/data';
import { decodeEntities } from '@wordpress/html-entities';
import { DropdownMenu, MenuGroup, MenuItem } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { useEntityRecord } from '@wordpress/core-data';
import { useEntityRecord, store as coreStore } from '@wordpress/core-data';
import { check } from '@wordpress/icons';
import { store as noticesStore } from '@wordpress/notices';

Expand Down Expand Up @@ -48,6 +48,12 @@ export default function BlockThemeControl( { id } ) {
'wp_template',
id
);

const canCreateTemplate = useSelect(
( select ) =>
select( coreStore ).canUser( 'create', 'templates' ) ?? false
);

const { createSuccessNotice } = useDispatch( noticesStore );
const { setRenderingMode } = useDispatch( editorStore );

Expand Down Expand Up @@ -81,30 +87,33 @@ export default function BlockThemeControl( { id } ) {
{ ( { onClose } ) => (
<>
<MenuGroup>
<MenuItem
onClick={ () => {
onNavigateToEntityRecord( {
postId: template.id,
postType: 'wp_template',
} );
onClose();
createSuccessNotice(
__(
'Editing template. Changes made here affect all posts and pages that use the template.'
),
{
type: 'snackbar',
actions: notificationAction,
}
);
} }
>
{ __( 'Edit template' ) }
</MenuItem>

{ canCreateTemplate && (
<MenuItem
onClick={ () => {
onNavigateToEntityRecord( {
postId: template.id,
postType: 'wp_template',
} );
onClose();
createSuccessNotice(
__(
'Editing template. Changes made here affect all posts and pages that use the template.'
),
{
type: 'snackbar',
actions: notificationAction,
}
);
} }
>
{ __( 'Edit template' ) }
</MenuItem>
) }
<SwapTemplateButton onClick={ onClose } />
<ResetDefaultTemplate onClick={ onClose } />
<CreateNewTemplate onClick={ onClose } />
{ canCreateTemplate && (
<CreateNewTemplate onClick={ onClose } />
) }
</MenuGroup>
<MenuGroup>
<MenuItem
Expand Down
Loading