diff --git a/gutenberg.php b/gutenberg.php
index 6253c9a3bf0e48..cbb0375d2a7605 100644
--- a/gutenberg.php
+++ b/gutenberg.php
@@ -46,6 +46,21 @@ function the_gutenberg_project() {
+
namespace = 'wp/v2';
+ $this->rest_base = 'widgets';
+ }
+
+ /**
+ * Registers the necessary REST API route.
+ *
+ * @access public
+ */
+ public function register_routes() {
+ register_rest_route(
+ $this->namespace,
+ // Regex representing a PHP class extracted from http://php.net/manual/en/language.oop5.basic.php.
+ '/' . $this->rest_base . '/(?P[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)/',
+ array(
+ 'args' => array(
+ 'identifier' => array(
+ 'description' => __( 'Class name of the widget.', 'gutenberg' ),
+ 'type' => 'string',
+ ),
+ ),
+ array(
+ 'methods' => WP_REST_Server::EDITABLE,
+ 'permission_callback' => array( $this, 'compute_new_widget_permissions_check' ),
+ 'callback' => array( $this, 'compute_new_widget' ),
+ ),
+ )
+ );
+ }
+
+ /**
+ * Checks if the user has permissions to make the request.
+ *
+ * @since 5.2.0
+ * @access public
+ *
+ * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
+ */
+ public function compute_new_widget_permissions_check() {
+ // Verify if the current user has edit_theme_options capability.
+ // This capability is required to access the widgets screen.
+ if ( ! current_user_can( 'edit_theme_options' ) ) {
+ return new WP_Error(
+ 'widgets_cannot_access',
+ __( 'Sorry, you are not allowed to access widgets on this site.', 'gutenberg' ),
+ array(
+ 'status' => rest_authorization_required_code(),
+ )
+ );
+ }
+ return true;
+ }
+
+ /**
+ * Returns the new widget instance and the form that represents it.
+ *
+ * @since 5.2.0
+ * @access public
+ *
+ * @param WP_REST_Request $request Full details about the request.
+ * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
+ */
+ public function compute_new_widget( $request ) {
+ $url_params = $request->get_url_params();
+
+ $widget = $request->get_param( 'identifier' );
+
+ global $wp_widget_factory;
+
+ if (
+ null === $widget ||
+ ! isset( $wp_widget_factory->widgets[ $widget ] ) ||
+ ! ( $wp_widget_factory->widgets[ $widget ] instanceof WP_Widget )
+ ) {
+ return new WP_Error(
+ 'widget_invalid',
+ __( 'Invalid widget.', 'gutenberg' ),
+ array(
+ 'status' => 404,
+ )
+ );
+ }
+
+ $widget_obj = $wp_widget_factory->widgets[ $widget ];
+
+ $instance = $request->get_param( 'instance' );
+ if ( null === $instance ) {
+ $instance = array();
+ }
+ $id_to_use = $request->get_param( 'id_to_use' );
+ if ( null === $id_to_use ) {
+ $id_to_use = -1;
+ }
+
+ $widget_obj->_set( $id_to_use );
+ ob_start();
+
+ $instance_changes = $request->get_param( 'instance_changes' );
+ if ( null !== $instance_changes ) {
+ $old_instance = $instance;
+ $instance = $widget_obj->update( $instance_changes, $old_instance );
+ /**
+ * Filters a widget's settings before saving.
+ *
+ * Returning false will effectively short-circuit the widget's ability
+ * to update settings. The old setting will be returned.
+ *
+ * @since 5.2.0
+ *
+ * @param array $instance The current widget instance's settings.
+ * @param array $instance_changes Array of new widget settings.
+ * @param array $old_instance Array of old widget settings.
+ * @param WP_Widget $widget_ob The widget instance.
+ */
+ $instance = apply_filters( 'widget_update_callback', $instance, $instance_changes, $old_instance, $widget_obj );
+ if ( false === $instance ) {
+ $instance = $old_instance;
+ }
+ }
+
+ $instance = apply_filters( 'widget_form_callback', $instance, $widget_obj );
+
+ $return = null;
+ if ( false !== $instance ) {
+ $return = $widget_obj->form( $instance );
+
+ /**
+ * Fires at the end of the widget control form.
+ *
+ * Use this hook to add extra fields to the widget form. The hook
+ * is only fired if the value passed to the 'widget_form_callback'
+ * hook is not false.
+ *
+ * Note: If the widget has no form, the text echoed from the default
+ * form method can be hidden using CSS.
+ *
+ * @since 5.2.0
+ *
+ * @param WP_Widget $widget_obj The widget instance (passed by reference).
+ * @param null $return Return null if new fields are added.
+ * @param array $instance An array of the widget's settings.
+ */
+ do_action_ref_array( 'in_widget_form', array( &$widget_obj, &$return, $instance ) );
+ }
+
+ $id_base = $widget_obj->id_base;
+ $id = $widget_obj->id;
+ $form = ob_get_clean();
+
+ return rest_ensure_response(
+ array(
+ 'instance' => $instance,
+ 'form' => $form,
+ 'id_base' => $id_base,
+ 'id' => $id,
+ )
+ );
+ }
+}
+/**
+ * End: Include for phase 2
+ */
diff --git a/lib/client-assets.php b/lib/client-assets.php
index f721c3855bc0d5..6f000548e0175e 100644
--- a/lib/client-assets.php
+++ b/lib/client-assets.php
@@ -872,26 +872,75 @@ function gutenberg_editor_scripts_and_styles( $hook ) {
);
}
+ /**
+ * Start: Include for phase 2
+ */
+
+ /**
+ * Todo: The hardcoded array should be replaced with a mechanisms that allows core blocks
+ * and third party blocks to specify they already have equivalent blocks, and maybe even allow them
+ * to have a migration function.
+ */
+ $core_widgets = array( 'WP_Widget_Pages', 'WP_Widget_Calendar', 'WP_Widget_Archives', 'WP_Widget_Media_Audio', 'WP_Widget_Media_Image', 'WP_Widget_Media_Gallery', 'WP_Widget_Media_Video', 'WP_Widget_Meta', 'WP_Widget_Search', 'WP_Widget_Text', 'WP_Widget_Categories', 'WP_Widget_Recent_Posts', 'WP_Widget_Recent_Comments', 'WP_Widget_RSS', 'WP_Widget_Tag_Cloud', 'WP_Nav_Menu_Widget', 'WP_Widget_Custom_HTML' );
+
+ $has_permissions_to_manage_widgets = current_user_can( 'edit_theme_options' );
+ $available_legacy_widgets = array();
+ global $wp_widget_factory, $wp_registered_widgets;
+ foreach ( $wp_widget_factory->widgets as $class => $widget_obj ) {
+ if ( ! in_array( $class, $core_widgets ) ) {
+ $available_legacy_widgets[ $class ] = array(
+ 'name' => html_entity_decode( $widget_obj->name ),
+ 'description' => html_entity_decode( $widget_obj->widget_options['description'] ),
+ 'isCallbackWidget' => false,
+ );
+ }
+ }
+ foreach ( $wp_registered_widgets as $widget_id => $widget_obj ) {
+ if (
+ is_array( $widget_obj['callback'] ) &&
+ isset( $widget_obj['callback'][0] ) &&
+ ( $widget_obj['callback'][0] instanceof WP_Widget )
+ ) {
+ continue;
+ }
+ $available_legacy_widgets[ $widget_id ] = array(
+ 'name' => html_entity_decode( $widget_obj['name'] ),
+ 'description' => null,
+ 'isCallbackWidget' => true,
+ );
+ }
+ /**
+ * End: Include for phase 2
+ */
+
$editor_settings = array(
- 'alignWide' => $align_wide,
- 'availableTemplates' => $available_templates,
- 'allowedBlockTypes' => $allowed_block_types,
- 'disableCustomColors' => get_theme_support( 'disable-custom-colors' ),
- 'disableCustomFontSizes' => get_theme_support( 'disable-custom-font-sizes' ),
- 'disablePostFormats' => ! current_theme_supports( 'post-formats' ),
- 'titlePlaceholder' => apply_filters( 'enter_title_here', __( 'Add title', 'gutenberg' ), $post ),
- 'bodyPlaceholder' => apply_filters( 'write_your_story', __( 'Start writing or type / to choose a block', 'gutenberg' ), $post ),
- 'isRTL' => is_rtl(),
- 'autosaveInterval' => 10,
- 'maxUploadFileSize' => $max_upload_size,
- 'allowedMimeTypes' => get_allowed_mime_types(),
- 'styles' => $styles,
- 'imageSizes' => gutenberg_get_available_image_sizes(),
- 'richEditingEnabled' => user_can_richedit(),
+ 'alignWide' => $align_wide,
+ 'availableTemplates' => $available_templates,
+ /**
+ * Start: Include for phase 2
+ */
+ 'hasPermissionsToManageWidgets' => $has_permissions_to_manage_widgets,
+ 'availableLegacyWidgets' => $available_legacy_widgets,
+ /**
+ * End: Include for phase 2
+ */
+ 'allowedBlockTypes' => $allowed_block_types,
+ 'disableCustomColors' => get_theme_support( 'disable-custom-colors' ),
+ 'disableCustomFontSizes' => get_theme_support( 'disable-custom-font-sizes' ),
+ 'disablePostFormats' => ! current_theme_supports( 'post-formats' ),
+ 'titlePlaceholder' => apply_filters( 'enter_title_here', __( 'Add title', 'gutenberg' ), $post ),
+ 'bodyPlaceholder' => apply_filters( 'write_your_story', __( 'Start writing or type / to choose a block', 'gutenberg' ), $post ),
+ 'isRTL' => is_rtl(),
+ 'autosaveInterval' => 10,
+ 'maxUploadFileSize' => $max_upload_size,
+ 'allowedMimeTypes' => get_allowed_mime_types(),
+ 'styles' => $styles,
+ 'imageSizes' => gutenberg_get_available_image_sizes(),
+ 'richEditingEnabled' => user_can_richedit(),
// Ideally, we'd remove this and rely on a REST API endpoint.
- 'postLock' => $lock_details,
- 'postLockUtils' => array(
+ 'postLock' => $lock_details,
+ 'postLockUtils' => array(
'nonce' => wp_create_nonce( 'lock-post_' . $post->ID ),
'unlockNonce' => wp_create_nonce( 'update-post_' . $post->ID ),
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
@@ -899,7 +948,7 @@ function gutenberg_editor_scripts_and_styles( $hook ) {
// Whether or not to load the 'postcustom' meta box is stored as a user meta
// field so that we're not always loading its assets.
- 'enableCustomFields' => (bool) get_user_meta( get_current_user_id(), 'enable_custom_fields', true ),
+ 'enableCustomFields' => (bool) get_user_meta( get_current_user_id(), 'enable_custom_fields', true ),
);
$post_autosave = gutenberg_get_autosave_newer_than_post_save( $post );
diff --git a/lib/load.php b/lib/load.php
index 999523ed52ff68..cb6f3c5cee300f 100644
--- a/lib/load.php
+++ b/lib/load.php
@@ -12,6 +12,15 @@
// These files only need to be loaded if within a rest server instance
// which this class will exist if that is the case.
if ( class_exists( 'WP_REST_Controller' ) ) {
+ /**
+ * Start: Include for phase 2
+ */
+ if ( ! class_exists( 'WP_REST_Widget_Updater_Controller' ) ) {
+ require dirname( __FILE__ ) . '/class-wp-rest-widget-updater-controller.php';
+ }
+ /**
+ * End: Include for phase 2
+ */
require dirname( __FILE__ ) . '/rest-api.php';
}
@@ -43,6 +52,18 @@
if ( ! function_exists( 'render_block_core_latest_posts' ) ) {
require dirname( __FILE__ ) . '/../packages/block-library/src/latest-posts/index.php';
}
+
+
+/**
+ * Start: Include for phase 2
+ */
+if ( ! function_exists( 'render_block_legacy_widget' ) ) {
+ require dirname( __FILE__ ) . '/../packages/block-library/src/legacy-widget/index.php';
+}
+/**
+ * End: Include for phase 2
+ */
+
if ( ! function_exists( 'render_block_core_rss' ) ) {
require dirname( __FILE__ ) . '/../packages/block-library/src/rss/index.php';
}
diff --git a/lib/rest-api.php b/lib/rest-api.php
index 1b36ba7df9b38b..eaccc09c61978d 100644
--- a/lib/rest-api.php
+++ b/lib/rest-api.php
@@ -51,3 +51,22 @@ function gutenberg_filter_oembed_result( $response, $handler, $request ) {
);
}
add_filter( 'rest_request_after_callbacks', 'gutenberg_filter_oembed_result', 10, 3 );
+
+
+
+/**
+ * Start: Include for phase 2
+ */
+/**
+ * Registers the REST API routes needed by the legacy widget block.
+ *
+ * @since 5.0.0
+ */
+function gutenberg_register_rest_widget_updater_routes() {
+ $widgets_controller = new WP_REST_Widget_Updater_Controller();
+ $widgets_controller->register_routes();
+}
+add_action( 'rest_api_init', 'gutenberg_register_rest_widget_updater_routes' );
+/**
+ * End: Include for phase 2
+ */
diff --git a/packages/block-editor/README.md b/packages/block-editor/README.md
index 3172cb260f2a83..87f2bfb49d5dda 100644
--- a/packages/block-editor/README.md
+++ b/packages/block-editor/README.md
@@ -28,20 +28,22 @@ Undocumented declaration.
The default editor settings
- alignWide boolean Enable/Disable Wide/Full Alignments
- colors Array Palette colors
- disableCustomColors boolean Whether or not the custom colors are disabled
- fontSizes Array Available font sizes
- disableCustomFontSizes boolean Whether or not the custom font sizes are disabled
- imageSizes Array Available image sizes
- maxWidth number Max width to constraint resizing
- allowedBlockTypes boolean|Array Allowed block types
- hasFixedToolbar boolean Whether or not the editor toolbar is fixed
- focusMode boolean Whether the focus mode is enabled or not
- styles Array Editor Styles
- isRTL boolean Whether the editor is in RTL mode
- bodyPlaceholder string Empty post placeholder
- titlePlaceholder string Empty title placeholder
+ alignWide boolean Enable/Disable Wide/Full Alignments
+ availableLegacyWidgets Array Array of objects representing the legacy widgets available.
+ colors Array Palette colors
+ disableCustomColors boolean Whether or not the custom colors are disabled
+ fontSizes Array Available font sizes
+ disableCustomFontSizes boolean Whether or not the custom font sizes are disabled
+ imageSizes Array Available image sizes
+ maxWidth number Max width to constraint resizing
+ allowedBlockTypes boolean|Array Allowed block types
+ hasFixedToolbar boolean Whether or not the editor toolbar is fixed
+ hasPermissionsToManageWidgets boolean Whether or not the user is able to manage widgets.
+ focusMode boolean Whether the focus mode is enabled or not
+ styles Array Editor Styles
+ isRTL boolean Whether the editor is in RTL mode
+ bodyPlaceholder string Empty post placeholder
+ titlePlaceholder string Empty title placeholder
diff --git a/packages/block-editor/src/store/defaults.js b/packages/block-editor/src/store/defaults.js
index 728b500a51d593..14c114216f12a9 100644
--- a/packages/block-editor/src/store/defaults.js
+++ b/packages/block-editor/src/store/defaults.js
@@ -10,20 +10,22 @@ export const PREFERENCES_DEFAULTS = {
/**
* The default editor settings
*
- * alignWide boolean Enable/Disable Wide/Full Alignments
- * colors Array Palette colors
- * disableCustomColors boolean Whether or not the custom colors are disabled
- * fontSizes Array Available font sizes
- * disableCustomFontSizes boolean Whether or not the custom font sizes are disabled
- * imageSizes Array Available image sizes
- * maxWidth number Max width to constraint resizing
- * allowedBlockTypes boolean|Array Allowed block types
- * hasFixedToolbar boolean Whether or not the editor toolbar is fixed
- * focusMode boolean Whether the focus mode is enabled or not
- * styles Array Editor Styles
- * isRTL boolean Whether the editor is in RTL mode
- * bodyPlaceholder string Empty post placeholder
- * titlePlaceholder string Empty title placeholder
+ * alignWide boolean Enable/Disable Wide/Full Alignments
+ * availableLegacyWidgets Array Array of objects representing the legacy widgets available.
+ * colors Array Palette colors
+ * disableCustomColors boolean Whether or not the custom colors are disabled
+ * fontSizes Array Available font sizes
+ * disableCustomFontSizes boolean Whether or not the custom font sizes are disabled
+ * imageSizes Array Available image sizes
+ * maxWidth number Max width to constraint resizing
+ * allowedBlockTypes boolean|Array Allowed block types
+ * hasFixedToolbar boolean Whether or not the editor toolbar is fixed
+ * hasPermissionsToManageWidgets boolean Whether or not the user is able to manage widgets.
+ * focusMode boolean Whether the focus mode is enabled or not
+ * styles Array Editor Styles
+ * isRTL boolean Whether the editor is in RTL mode
+ * bodyPlaceholder string Empty post placeholder
+ * titlePlaceholder string Empty title placeholder
*/
export const SETTINGS_DEFAULTS = {
alignWide: false,
@@ -131,5 +133,8 @@ export const SETTINGS_DEFAULTS = {
// List of allowed mime types and file extensions.
allowedMimeTypes: null,
+
+ availableLegacyWidgets: {},
+ hasPermissionsToManageWidgets: false,
};
diff --git a/packages/block-library/README.md b/packages/block-library/README.md
index f21ffbea41303d..78573bbf753e7d 100644
--- a/packages/block-library/README.md
+++ b/packages/block-library/README.md
@@ -18,7 +18,7 @@ _This package assumes that your code will run in an **ES2015+** environment. If
### registerCoreBlocks
-[src/index.js#L69-L130](src/index.js#L69-L130)
+[src/index.js#L70-L132](src/index.js#L70-L132)
Function to register core blocks provided by the block editor.
diff --git a/packages/block-library/src/editor.scss b/packages/block-library/src/editor.scss
index 517218be5e7154..8422d2c82571de 100644
--- a/packages/block-library/src/editor.scss
+++ b/packages/block-library/src/editor.scss
@@ -14,6 +14,7 @@
@import "./image/editor.scss";
@import "./latest-comments/editor.scss";
@import "./latest-posts/editor.scss";
+@import "./legacy-widget/editor.scss";
@import "./media-text/editor.scss";
@import "./list/editor.scss";
@import "./more/editor.scss";
diff --git a/packages/block-library/src/index.js b/packages/block-library/src/index.js
index 1fc00265bc8759..81a0e9bec632b4 100644
--- a/packages/block-library/src/index.js
+++ b/packages/block-library/src/index.js
@@ -34,6 +34,7 @@ import * as html from './html';
import * as mediaText from './media-text';
import * as latestComments from './latest-comments';
import * as latestPosts from './latest-posts';
+import * as legacyWidget from './legacy-widget';
import * as list from './list';
import * as missing from './missing';
import * as more from './more';
@@ -97,6 +98,7 @@ export const registerCoreBlocks = () => {
mediaText,
latestComments,
latestPosts,
+ process.env.GUTENBERG_PHASE === 2 ? legacyWidget : null,
missing,
more,
nextpage,
diff --git a/packages/block-library/src/legacy-widget/WidgetEditDomManager.js b/packages/block-library/src/legacy-widget/WidgetEditDomManager.js
new file mode 100644
index 00000000000000..30ded7a7a7a6f8
--- /dev/null
+++ b/packages/block-library/src/legacy-widget/WidgetEditDomManager.js
@@ -0,0 +1,143 @@
+/**
+ * External dependencies
+ */
+import { includes } from 'lodash';
+
+/**
+ * WordPress dependencies
+ */
+import { Component, createRef } from '@wordpress/element';
+import isShallowEqual from '@wordpress/is-shallow-equal';
+
+class WidgetEditDomManager extends Component {
+ constructor() {
+ super( ...arguments );
+
+ this.containerRef = createRef();
+ this.formRef = createRef();
+ this.widgetContentRef = createRef();
+ this.triggerWidgetEvent = this.triggerWidgetEvent.bind( this );
+ }
+
+ componentDidMount() {
+ this.triggerWidgetEvent( 'widget-added' );
+ this.previousFormData = new window.FormData(
+ this.formRef.current
+ );
+ }
+
+ shouldComponentUpdate( nextProps ) {
+ // We can not leverage react render otherwise we would destroy dom changes applied by the plugins.
+ // We manually update the required dom node replicating what the widget screen and the customizer do.
+ if ( nextProps.form !== this.props.form && this.widgetContentRef.current ) {
+ const widgetContent = this.widgetContentRef.current;
+ widgetContent.innerHTML = nextProps.form;
+ this.triggerWidgetEvent( 'widget-updated' );
+ this.previousFormData = new window.FormData(
+ this.formRef.current
+ );
+ }
+ return false;
+ }
+
+ render() {
+ const { id, idBase, widgetNumber, form } = this.props;
+ return (
+
+ );
+ }
+
+ shouldTriggerInstanceUpdate() {
+ if ( ! this.formRef.current ) {
+ return false;
+ }
+ if ( ! this.previousFormData ) {
+ return true;
+ }
+ const currentFormData = new window.FormData(
+ this.formRef.current
+ );
+ const currentFormDataKeys = Array.from( currentFormData.keys() );
+ const previousFormDataKeys = Array.from( this.previousFormData.keys() );
+ if (
+ currentFormDataKeys.length !== previousFormDataKeys.length
+ ) {
+ return true;
+ }
+ for ( const rawKey of currentFormDataKeys ) {
+ if ( ! isShallowEqual(
+ currentFormData.getAll( rawKey ),
+ this.previousFormData.getAll( rawKey )
+ ) ) {
+ this.previousFormData = currentFormData;
+ return true;
+ }
+ }
+ return false;
+ }
+
+ triggerWidgetEvent( event ) {
+ window.$( window.document ).trigger(
+ event,
+ [ window.$( this.containerRef.current ) ]
+ );
+ }
+
+ retrieveUpdatedInstance() {
+ if ( this.formRef.current ) {
+ const { idBase, widgetNumber } = this.props;
+ const form = this.formRef.current;
+ const formData = new window.FormData( form );
+ const updatedInstance = {};
+ const keyPrefixLength = `widget-${ idBase }[${ widgetNumber }][`.length;
+ const keySuffixLength = `]`.length;
+ for ( const rawKey of formData.keys() ) {
+ // This fields are added to the form because the widget JavaScript code may use this values.
+ // They are not relevant for the update mechanism.
+ if ( includes(
+ [ 'widget-id', 'id_base', 'widget_number', 'multi_number', 'add_new' ],
+ rawKey,
+ ) ) {
+ continue;
+ }
+ const keyParsed = rawKey.substring( keyPrefixLength, rawKey.length - keySuffixLength );
+
+ const value = formData.getAll( rawKey );
+ if ( value.length > 1 ) {
+ updatedInstance[ keyParsed ] = value;
+ } else {
+ updatedInstance[ keyParsed ] = value[ 0 ];
+ }
+ }
+ return updatedInstance;
+ }
+ }
+}
+
+export default WidgetEditDomManager;
+
diff --git a/packages/block-library/src/legacy-widget/WidgetEditHandler.js b/packages/block-library/src/legacy-widget/WidgetEditHandler.js
new file mode 100644
index 00000000000000..527e864c90037a
--- /dev/null
+++ b/packages/block-library/src/legacy-widget/WidgetEditHandler.js
@@ -0,0 +1,122 @@
+/**
+ * WordPress dependencies
+ */
+import { Component } from '@wordpress/element';
+import { __ } from '@wordpress/i18n';
+import apiFetch from '@wordpress/api-fetch';
+import { withInstanceId } from '@wordpress/compose';
+
+/**
+ * Internal dependencies
+ */
+import WidgetEditDomManager from './WidgetEditDomManager';
+
+class WidgetEditHandler extends Component {
+ constructor() {
+ super( ...arguments );
+ this.state = {
+ form: null,
+ idBase: null,
+ };
+ this.instanceUpdating = null;
+ this.onInstanceChange = this.onInstanceChange.bind( this );
+ this.requestWidgetUpdater = this.requestWidgetUpdater.bind( this );
+ }
+
+ componentDidMount() {
+ this.isStillMounted = true;
+ this.requestWidgetUpdater();
+ }
+
+ componentDidUpdate( prevProps ) {
+ if (
+ prevProps.instance !== this.props.instance &&
+ this.instanceUpdating !== this.props.instance
+ ) {
+ this.requestWidgetUpdater();
+ }
+ if ( this.instanceUpdating === this.props.instance ) {
+ this.instanceUpdating = null;
+ }
+ }
+
+ componentWillUnmount() {
+ this.isStillMounted = false;
+ }
+
+ render() {
+ const { instanceId, identifier } = this.props;
+ const { id, idBase, form } = this.state;
+ if ( ! identifier ) {
+ return __( 'Not a valid widget.' );
+ }
+ if ( ! form ) {
+ return null;
+ }
+ return (
+
+ {
+ this.widgetEditDomManagerRef = ref;
+ } }
+ onInstanceChange={ this.onInstanceChange }
+ widgetNumber={ instanceId * -1 }
+ id={ id }
+ idBase={ idBase }
+ form={ form }
+ />
+
+ );
+ }
+
+ onInstanceChange( instanceChanges ) {
+ this.requestWidgetUpdater( instanceChanges, ( response ) => {
+ this.instanceUpdating = response.instance;
+ this.props.onInstanceChange( response.instance );
+ } );
+ }
+
+ requestWidgetUpdater( instanceChanges, callback ) {
+ const { identifier, instanceId, instance } = this.props;
+ if ( ! identifier ) {
+ return;
+ }
+
+ apiFetch( {
+ path: `/wp/v2/widgets/${ identifier }/`,
+ data: {
+ identifier,
+ instance,
+ // use negative ids to make sure the id does not exist on the database.
+ id_to_use: instanceId * -1,
+ instance_changes: instanceChanges,
+ },
+ method: 'POST',
+ } ).then(
+ ( response ) => {
+ if ( this.isStillMounted ) {
+ this.setState( {
+ form: response.form,
+ idBase: response.id_base,
+ id: response.id,
+ } );
+ if ( callback ) {
+ callback( response );
+ }
+ }
+ }
+ );
+ }
+}
+
+export default withInstanceId( WidgetEditHandler );
+
diff --git a/packages/block-library/src/legacy-widget/edit.js b/packages/block-library/src/legacy-widget/edit.js
new file mode 100644
index 00000000000000..e2f2a4c4cf2405
--- /dev/null
+++ b/packages/block-library/src/legacy-widget/edit.js
@@ -0,0 +1,195 @@
+/**
+ * External dependencies
+ */
+import { map } from 'lodash';
+
+/**
+ * WordPress dependencies
+ */
+import { Component, Fragment } from '@wordpress/element';
+import {
+ Button,
+ IconButton,
+ PanelBody,
+ Placeholder,
+ SelectControl,
+ Toolbar,
+} from '@wordpress/components';
+import { __ } from '@wordpress/i18n';
+import { withSelect } from '@wordpress/data';
+
+/**
+ * Internal dependencies
+ */
+import {
+ BlockControls,
+ BlockIcon,
+ InspectorControls,
+ ServerSideRender,
+} from '@wordpress/editor';
+
+import WidgetEditHandler from './WidgetEditHandler';
+
+class LegacyWidgetEdit extends Component {
+ constructor() {
+ super( ...arguments );
+ this.state = {
+ isPreview: false,
+ };
+ this.switchToEdit = this.switchToEdit.bind( this );
+ this.switchToPreview = this.switchToPreview.bind( this );
+ this.changeWidget = this.changeWidget.bind( this );
+ }
+
+ render() {
+ const {
+ attributes,
+ availableLegacyWidgets,
+ hasPermissionsToManageWidgets,
+ setAttributes,
+ } = this.props;
+ const { isPreview } = this.state;
+ const { identifier, isCallbackWidget } = attributes;
+ const widgetObject = identifier && availableLegacyWidgets[ identifier ];
+ if ( ! widgetObject ) {
+ let placeholderContent;
+
+ if ( ! hasPermissionsToManageWidgets ) {
+ placeholderContent = __( 'You don\'t have permissions to use widgets on this site.' );
+ } else if ( availableLegacyWidgets.length === 0 ) {
+ placeholderContent = __( 'There are no widgets available.' );
+ } else {
+ placeholderContent = (
+ setAttributes( {
+ instance: {},
+ identifier: value,
+ isCallbackWidget: availableLegacyWidgets[ value ].isCallbackWidget,
+ } ) }
+ options={ [ { value: 'none', label: 'Select widget' } ].concat(
+ map( availableLegacyWidgets, ( widget, key ) => {
+ return {
+ value: key,
+ label: widget.name,
+ };
+ } )
+ ) }
+ />
+ );
+ }
+
+ return (
+ }
+ label={ __( 'Legacy Widget' ) }
+ >
+ { placeholderContent }
+
+ );
+ }
+
+ const inspectorControls = (
+
+
+ { widgetObject.description }
+
+
+ );
+ if ( ! hasPermissionsToManageWidgets ) {
+ return (
+
+ { inspectorControls }
+ { this.renderWidgetPreview() }
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ { ! isCallbackWidget && (
+
+
+
+
+ ) }
+
+
+ { inspectorControls }
+ { ! isCallbackWidget && (
+ {
+ this.props.setAttributes( {
+ instance: newInstance,
+ } );
+ }
+ }
+ />
+ ) }
+ { ( isPreview || isCallbackWidget ) && this.renderWidgetPreview() }
+
+ );
+ }
+
+ changeWidget() {
+ this.switchToEdit();
+ this.props.setAttributes( {
+ instance: {},
+ identifier: undefined,
+ } );
+ }
+
+ switchToEdit() {
+ this.setState( { isPreview: false } );
+ }
+
+ switchToPreview() {
+ this.setState( { isPreview: true } );
+ }
+
+ renderWidgetPreview() {
+ const { attributes } = this.props;
+ return (
+
+ );
+ }
+}
+
+export default withSelect( ( select ) => {
+ const editorSettings = select( 'core/block-editor' ).getSettings();
+ const {
+ availableLegacyWidgets,
+ hasPermissionsToManageWidgets,
+ } = editorSettings;
+ return {
+ hasPermissionsToManageWidgets,
+ availableLegacyWidgets,
+ };
+} )( LegacyWidgetEdit );
diff --git a/packages/block-library/src/legacy-widget/editor.scss b/packages/block-library/src/legacy-widget/editor.scss
new file mode 100644
index 00000000000000..f3b8b13b1e1f96
--- /dev/null
+++ b/packages/block-library/src/legacy-widget/editor.scss
@@ -0,0 +1,25 @@
+.wp-block-legacy-widget__edit-container,
+.wp-block-legacy-widget__preview {
+ padding-left: 2.5em;
+ padding-right: 2.5em;
+}
+
+.wp-block-legacy-widget__edit-container {
+ .widget-inside {
+ border: none;
+ display: block;
+ }
+}
+
+.wp-block-legacy-widget__update-button {
+ margin-left: auto;
+ display: block;
+}
+
+.wp-block-legacy-widget__edit-container .widget-inside {
+ box-shadow: none;
+}
+
+.wp-block-legacy-widget__preview {
+ overflow: auto;
+}
diff --git a/packages/block-library/src/legacy-widget/index.js b/packages/block-library/src/legacy-widget/index.js
new file mode 100644
index 00000000000000..43142ec5ca1306
--- /dev/null
+++ b/packages/block-library/src/legacy-widget/index.js
@@ -0,0 +1,33 @@
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { G, Path, SVG } from '@wordpress/components';
+
+/**
+ * Internal dependencies
+ */
+import edit from './edit';
+
+export const name = 'core/legacy-widget';
+
+export const settings = {
+ title: __( 'Legacy Widget (Experimental)' ),
+
+ description: __( 'Display a legacy widget.' ),
+
+ icon: ,
+
+ category: 'widgets',
+
+ supports: {
+ html: false,
+ },
+
+ edit,
+
+ save() {
+ // Handled by PHP.
+ return null;
+ },
+};
diff --git a/packages/block-library/src/legacy-widget/index.php b/packages/block-library/src/legacy-widget/index.php
new file mode 100644
index 00000000000000..b812f8e3c710ff
--- /dev/null
+++ b/packages/block-library/src/legacy-widget/index.php
@@ -0,0 +1,78 @@
+ $identifier,
+ 'widget_name' => $widget['name'],
+ ),
+ (array) $wp_registered_widgets[ $identifier ]['params']
+ );
+ $params = apply_filters( 'dynamic_sidebar_params', $params );
+
+ $callback = $widget['callback'];
+
+ if ( is_callable( $callback ) ) {
+ ob_start();
+ call_user_func_array( $callback, $params );
+ return ob_get_clean();
+ }
+ return '';
+ }
+ ob_start();
+ the_widget( $identifier, $attributes['instance'] );
+ return ob_get_clean();
+
+}
+
+/**
+ * Register legacy widget block.
+ */
+function register_block_core_legacy_widget() {
+ register_block_type(
+ 'core/legacy-widget',
+ array(
+ 'attributes' => array(
+ 'identifier' => array(
+ 'type' => 'string',
+ ),
+ 'instance' => array(
+ 'type' => 'object',
+ ),
+ 'isCallbackWidget' => array(
+ 'type' => 'boolean',
+ ),
+ ),
+ 'render_callback' => 'render_block_legacy_widget',
+ )
+ );
+}
+
+add_action( 'init', 'register_block_core_legacy_widget' );
diff --git a/packages/e2e-tests/fixtures/blocks/core__legacy-widget.html b/packages/e2e-tests/fixtures/blocks/core__legacy-widget.html
new file mode 100644
index 00000000000000..a94242f7e4e6ea
--- /dev/null
+++ b/packages/e2e-tests/fixtures/blocks/core__legacy-widget.html
@@ -0,0 +1 @@
+
diff --git a/packages/e2e-tests/fixtures/blocks/core__legacy-widget.json b/packages/e2e-tests/fixtures/blocks/core__legacy-widget.json
new file mode 100644
index 00000000000000..c9e750dbaa0663
--- /dev/null
+++ b/packages/e2e-tests/fixtures/blocks/core__legacy-widget.json
@@ -0,0 +1,10 @@
+[
+ {
+ "clientId": "_clientId_0",
+ "name": "core/legacy-widget",
+ "isValid": true,
+ "attributes": {},
+ "innerBlocks": [],
+ "originalContent": ""
+ }
+]
diff --git a/packages/e2e-tests/fixtures/blocks/core__legacy-widget.parsed.json b/packages/e2e-tests/fixtures/blocks/core__legacy-widget.parsed.json
new file mode 100644
index 00000000000000..93ea4bba8f175d
--- /dev/null
+++ b/packages/e2e-tests/fixtures/blocks/core__legacy-widget.parsed.json
@@ -0,0 +1,18 @@
+[
+ {
+ "blockName": "core/legacy-widget",
+ "attrs": {},
+ "innerBlocks": [],
+ "innerHTML": "",
+ "innerContent": []
+ },
+ {
+ "blockName": null,
+ "attrs": {},
+ "innerBlocks": [],
+ "innerHTML": "\n",
+ "innerContent": [
+ "\n"
+ ]
+ }
+]
diff --git a/packages/e2e-tests/fixtures/blocks/core__legacy-widget.serialized.html b/packages/e2e-tests/fixtures/blocks/core__legacy-widget.serialized.html
new file mode 100644
index 00000000000000..a94242f7e4e6ea
--- /dev/null
+++ b/packages/e2e-tests/fixtures/blocks/core__legacy-widget.serialized.html
@@ -0,0 +1 @@
+
diff --git a/packages/editor/src/components/provider/index.js b/packages/editor/src/components/provider/index.js
index 8817eb5d4be3b3..14ead7919501f8 100644
--- a/packages/editor/src/components/provider/index.js
+++ b/packages/editor/src/components/provider/index.js
@@ -55,6 +55,7 @@ class EditorProvider extends Component {
return {
...pick( settings, [
'alignWide',
+ 'availableLegacyWidgets',
'colors',
'disableCustomColors',
'fontSizes',
@@ -63,6 +64,7 @@ class EditorProvider extends Component {
'maxWidth',
'allowedBlockTypes',
'hasFixedToolbar',
+ 'hasPermissionsToManageWidgets',
'focusMode',
'styles',
'isRTL',