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.
*/