diff --git a/editor/components/block-settings-menu/html-converter.js b/editor/components/block-settings-menu/html-converter.js new file mode 100644 index 00000000000000..826c5d647a687e --- /dev/null +++ b/editor/components/block-settings-menu/html-converter.js @@ -0,0 +1,50 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { IconButton } from '@wordpress/components'; +import { rawHandler, getBlockContent } from '@wordpress/blocks'; +import { compose } from '@wordpress/element'; +import { withSelect, withDispatch } from '@wordpress/data'; + +export function HTMLConverter( { block, onReplace, small, canUserUseUnfilteredHTML, role } ) { + if ( ! block || block.name !== 'core/html' ) { + return null; + } + + const label = __( 'Convert to blocks' ); + + const convertToBlocks = () => { + onReplace( block.uid, rawHandler( { + HTML: getBlockContent( block ), + mode: 'BLOCKS', + canUserUseUnfilteredHTML, + } ) ); + }; + + return ( + + { ! small && label } + + ); +} + +export default compose( + withSelect( ( select, { uid } ) => { + const { getBlock, getCurrentPostType, canUserUseUnfilteredHTML } = select( 'core/editor' ); + return { + block: getBlock( uid ), + postType: getCurrentPostType(), + canUserUseUnfilteredHTML: canUserUseUnfilteredHTML(), + }; + } ), + withDispatch( ( dispatch ) => ( { + onReplace: dispatch( 'core/editor' ).replaceBlocks, + } ) ), +)( HTMLConverter ); diff --git a/editor/components/block-settings-menu/index.js b/editor/components/block-settings-menu/index.js index e8fd0439e6b76b..c69ecdf0e7f698 100644 --- a/editor/components/block-settings-menu/index.js +++ b/editor/components/block-settings-menu/index.js @@ -22,6 +22,7 @@ import BlockRemoveButton from './block-remove-button'; import SharedBlockConvertButton from './shared-block-convert-button'; import SharedBlockDeleteButton from './shared-block-delete-button'; import UnknownConverter from './unknown-converter'; +import HTMLConverter from './html-converter'; import _BlockSettingsMenuFirstItem from './block-settings-menu-first-item'; export class BlockSettingsMenu extends Component { @@ -95,6 +96,7 @@ export class BlockSettingsMenu extends Component { <_BlockSettingsMenuFirstItem.Slot fillProps={ { onClose } } /> { count === 1 && } { count === 1 && } + { count === 1 && } { count === 1 && }
diff --git a/editor/store/selectors.js b/editor/store/selectors.js index c1fa0e87600136..6911186833790d 100644 --- a/editor/store/selectors.js +++ b/editor/store/selectors.js @@ -1891,3 +1891,14 @@ export function getTokenSettings( state, name ) { return state.tokens[ name ]; } + +/** + * Returns whether or not the user has the unfiltered_html capability. + * + * @param {Object} state Editor state. + * + * @return {boolean} Whether the user can or can't post unfiltered HTML. + */ +export function canUserUseUnfilteredHTML( state ) { + return has( getCurrentPost( state ), [ '_links', 'wp:action-unfiltered_html' ] ); +} diff --git a/editor/store/test/selectors.js b/editor/store/test/selectors.js index 8bbdb1f9a2c9c3..d8cf70809c4bbf 100644 --- a/editor/store/test/selectors.js +++ b/editor/store/test/selectors.js @@ -16,6 +16,7 @@ import { moment } from '@wordpress/date'; import * as selectors from '../selectors'; const { + canUserUseUnfilteredHTML, hasEditorUndo, hasEditorRedo, isEditedPostNew, @@ -3809,4 +3810,25 @@ describe( 'selectors', () => { expect( getBlockListSettings( state, 'chicken' ) ).toBe( undefined ); } ); } ); + + describe( 'canUserUseUnfilteredHTML', () => { + it( 'should return true if the _links object contains the property wp:action-unfiltered_html', () => { + const state = { + currentPost: { + _links: { + 'wp:action-unfiltered_html': [], + }, + }, + }; + expect( canUserUseUnfilteredHTML( state ) ).toBe( true ); + } ); + it( 'should return false if the _links object doesnt contain the property wp:action-unfiltered_html', () => { + const state = { + currentPost: { + _links: {}, + }, + }; + expect( canUserUseUnfilteredHTML( state ) ).toBe( false ); + } ); + } ); } ); diff --git a/lib/rest-api.php b/lib/rest-api.php index b05e2e46abb95d..d2d08cbd824d7e 100644 --- a/lib/rest-api.php +++ b/lib/rest-api.php @@ -247,6 +247,22 @@ function gutenberg_add_target_schema_to_links( $response, $post, $request ) { ); } } + if ( 'edit' === $request['context'] && current_user_can( 'unfiltered_html' ) ) { + $new_links['https://api.w.org/action-unfiltered_html'] = array( + array( + 'title' => __( 'The current user can post HTML markup and JavaScript.', 'gutenberg' ), + 'href' => $orig_href, + 'targetSchema' => array( + 'type' => 'object', + 'properties' => array( + 'unfiltered_html' => array( + 'type' => 'boolean', + ), + ), + ), + ), + ); + } if ( 'edit' === $request['context'] ) { if ( current_user_can( $post_type->cap->publish_posts ) ) { $new_links['https://api.w.org/action-publish'] = array( diff --git a/phpunit/class-gutenberg-rest-api-test.php b/phpunit/class-gutenberg-rest-api-test.php index ad51a7ee1845f6..9b65d34d4f9489 100644 --- a/phpunit/class-gutenberg-rest-api-test.php +++ b/phpunit/class-gutenberg-rest-api-test.php @@ -130,6 +130,51 @@ function test_viewable_field_without_context() { $this->assertFalse( isset( $result['viewable'] ) ); } + /** + * Only returns wp:action-unfiltered_html when current user can use unfiltered HTML. + * See https://codex.wordpress.org/Roles_and_Capabilities#Capability_vs._Role_Table + */ + function test_link_unfiltered_html() { + $post_id = $this->factory->post->create(); + $check_key = 'https://api.w.org/action-unfiltered_html'; + // admins can in a single site, but can't in a multisite. + wp_set_current_user( $this->administrator ); + $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . $post_id ); + $request->set_param( 'context', 'edit' ); + $response = rest_do_request( $request ); + $links = $response->get_links(); + if ( is_multisite() ) { + $this->assertFalse( isset( $links[ $check_key ] ) ); + } else { + $this->assertTrue( isset( $links[ $check_key ] ) ); + } + // authors can't. + wp_set_current_user( $this->author ); + $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . $post_id ); + $request->set_param( 'context', 'edit' ); + $response = rest_do_request( $request ); + $links = $response->get_links(); + $this->assertFalse( isset( $links[ $check_key ] ) ); + // editors can in a single site, but can't in a multisite. + wp_set_current_user( $this->editor ); + $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . $post_id ); + $request->set_param( 'context', 'edit' ); + $response = rest_do_request( $request ); + $links = $response->get_links(); + if ( is_multisite() ) { + $this->assertFalse( isset( $links[ $check_key ] ) ); + } else { + $this->assertTrue( isset( $links[ $check_key ] ) ); + } + // contributors can't. + wp_set_current_user( $this->contributor ); + $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . $post_id ); + $request->set_param( 'context', 'edit' ); + $response = rest_do_request( $request ); + $links = $response->get_links(); + $this->assertFalse( isset( $links[ $check_key ] ) ); + } + /** * Only returns wp:action-assign-author when current user can assign author. */