From a0b805360808246cd3dfc7281efec0f0308f6863 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Wed, 25 May 2022 10:16:11 +0100 Subject: [PATCH 01/13] Update the minimum WP version to 5.9 --- gutenberg.php | 2 +- lib/compat/wordpress-5.9/admin-menu.php | 112 - .../wordpress-5.9/block-editor-settings.php | 29 - lib/compat/wordpress-5.9/block-gallery.php | 57 - lib/compat/wordpress-5.9/block-patterns.php | 45 - .../wordpress-5.9/block-template-utils.php | 630 ------ lib/compat/wordpress-5.9/block-template.php | 310 --- lib/compat/wordpress-5.9/blocks.php | 247 --- .../class-gutenberg-block-template.php | 131 -- ...class-wp-rest-global-styles-controller.php | 590 ----- .../class-wp-rest-menu-items-controller.php | 1169 ---------- ...lass-wp-rest-menu-locations-controller.php | 276 --- .../class-wp-rest-menus-controller.php | 693 ------ .../class-wp-rest-url-details-controller.php | 591 ----- .../wordpress-5.9/class-wp-theme-json-5-9.php | 1944 ----------------- .../class-wp-theme-json-resolver-5-9.php | 416 ---- .../class-wp-theme-json-schema-gutenberg.php | 489 ----- .../wordpress-5.9/default-editor-styles.php | 41 - .../wordpress-5.9/default-theme-supports.php | 23 - lib/compat/wordpress-5.9/edit-site-page.php | 240 -- .../global-styles-css-custom-properties.php | 20 - lib/compat/wordpress-5.9/json-file-decode.php | 54 - lib/compat/wordpress-5.9/kses.php | 82 - .../move-theme-editor-menu-item.php | 24 - lib/compat/wordpress-5.9/navigation.php | 243 --- lib/compat/wordpress-5.9/polyfills.php | 70 - .../register-global-styles-cpt.php | 49 - .../rest-active-global-styles.php | 33 - lib/compat/wordpress-5.9/rest-api.php | 148 -- lib/compat/wordpress-5.9/script-loader.php | 48 - lib/compat/wordpress-5.9/template-canvas.php | 27 - lib/compat/wordpress-5.9/template-parts.php | 129 -- lib/compat/wordpress-5.9/templates.php | 255 --- lib/compat/wordpress-5.9/theme-templates.php | 87 - lib/compat/wordpress-5.9/theme.php | 28 - .../translate-settings-using-i18n-schema.php | 50 - ...b-rest-widget-render-endpoint-polyfill.php | 121 - .../widget-render-api-endpoint/index.php | 22 - .../wordpress-6.0/block-editor-settings.php | 14 - .../wordpress-6.0/class-wp-theme-json-6-0.php | 2 +- .../class-wp-theme-json-resolver-6-0.php | 2 +- lib/load.php | 46 - .../block-library/src/gallery/deprecated.js | 63 +- .../block-library/src/gallery/edit-wrapper.js | 27 - .../src/gallery/gallery-styles.native.scss | 2 - packages/block-library/src/gallery/index.js | 2 +- packages/block-library/src/gallery/save.js | 10 - .../block-library/src/gallery/transforms.js | 169 +- .../block-library/src/gallery/v1/constants.js | 3 - .../src/gallery/v1/gallery-button.native.js | 47 - .../v1/gallery-image-style.native.scss | 109 - .../src/gallery/v1/gallery-image.js | 282 --- .../src/gallery/v1/gallery-styles.native.scss | 8 - .../block-library/src/gallery/v1/gallery.js | 125 -- .../src/gallery/v1/gallery.native.js | 162 -- packages/block-library/src/gallery/v1/save.js | 98 - .../block-library/src/gallery/v1/shared.js | 19 - .../src/gallery/v1/tiles-styles.native.scss | 11 - .../src/gallery/v1/tiles.native.js | 79 - 59 files changed, 57 insertions(+), 10748 deletions(-) delete mode 100644 lib/compat/wordpress-5.9/admin-menu.php delete mode 100644 lib/compat/wordpress-5.9/block-editor-settings.php delete mode 100644 lib/compat/wordpress-5.9/block-gallery.php delete mode 100644 lib/compat/wordpress-5.9/block-patterns.php delete mode 100644 lib/compat/wordpress-5.9/block-template-utils.php delete mode 100644 lib/compat/wordpress-5.9/block-template.php delete mode 100644 lib/compat/wordpress-5.9/blocks.php delete mode 100644 lib/compat/wordpress-5.9/class-gutenberg-block-template.php delete mode 100644 lib/compat/wordpress-5.9/class-wp-rest-global-styles-controller.php delete mode 100644 lib/compat/wordpress-5.9/class-wp-rest-menu-items-controller.php delete mode 100644 lib/compat/wordpress-5.9/class-wp-rest-menu-locations-controller.php delete mode 100644 lib/compat/wordpress-5.9/class-wp-rest-menus-controller.php delete mode 100644 lib/compat/wordpress-5.9/class-wp-rest-url-details-controller.php delete mode 100644 lib/compat/wordpress-5.9/class-wp-theme-json-5-9.php delete mode 100644 lib/compat/wordpress-5.9/class-wp-theme-json-resolver-5-9.php delete mode 100644 lib/compat/wordpress-5.9/class-wp-theme-json-schema-gutenberg.php delete mode 100644 lib/compat/wordpress-5.9/default-editor-styles.php delete mode 100644 lib/compat/wordpress-5.9/default-theme-supports.php delete mode 100644 lib/compat/wordpress-5.9/edit-site-page.php delete mode 100644 lib/compat/wordpress-5.9/global-styles-css-custom-properties.php delete mode 100644 lib/compat/wordpress-5.9/json-file-decode.php delete mode 100644 lib/compat/wordpress-5.9/kses.php delete mode 100644 lib/compat/wordpress-5.9/move-theme-editor-menu-item.php delete mode 100644 lib/compat/wordpress-5.9/navigation.php delete mode 100644 lib/compat/wordpress-5.9/polyfills.php delete mode 100644 lib/compat/wordpress-5.9/register-global-styles-cpt.php delete mode 100644 lib/compat/wordpress-5.9/rest-active-global-styles.php delete mode 100644 lib/compat/wordpress-5.9/rest-api.php delete mode 100644 lib/compat/wordpress-5.9/script-loader.php delete mode 100644 lib/compat/wordpress-5.9/template-canvas.php delete mode 100644 lib/compat/wordpress-5.9/template-parts.php delete mode 100644 lib/compat/wordpress-5.9/templates.php delete mode 100644 lib/compat/wordpress-5.9/theme-templates.php delete mode 100644 lib/compat/wordpress-5.9/theme.php delete mode 100644 lib/compat/wordpress-5.9/translate-settings-using-i18n-schema.php delete mode 100644 lib/compat/wordpress-5.9/widget-render-api-endpoint/class-gb-rest-widget-render-endpoint-polyfill.php delete mode 100644 lib/compat/wordpress-5.9/widget-render-api-endpoint/index.php delete mode 100644 packages/block-library/src/gallery/edit-wrapper.js delete mode 100644 packages/block-library/src/gallery/v1/constants.js delete mode 100644 packages/block-library/src/gallery/v1/gallery-button.native.js delete mode 100644 packages/block-library/src/gallery/v1/gallery-image-style.native.scss delete mode 100644 packages/block-library/src/gallery/v1/gallery-image.js delete mode 100644 packages/block-library/src/gallery/v1/gallery-styles.native.scss delete mode 100644 packages/block-library/src/gallery/v1/gallery.js delete mode 100644 packages/block-library/src/gallery/v1/gallery.native.js delete mode 100644 packages/block-library/src/gallery/v1/save.js delete mode 100644 packages/block-library/src/gallery/v1/shared.js delete mode 100644 packages/block-library/src/gallery/v1/tiles-styles.native.scss delete mode 100644 packages/block-library/src/gallery/v1/tiles.native.js diff --git a/gutenberg.php b/gutenberg.php index 3457ac3f6158a..3488ca7f516c2 100644 --- a/gutenberg.php +++ b/gutenberg.php @@ -3,7 +3,7 @@ * Plugin Name: Gutenberg * Plugin URI: https://github.com/WordPress/gutenberg * Description: Printing since 1440. This is the development plugin for the new block editor in core. - * Requires at least: 5.8 + * Requires at least: 5.9 * Requires PHP: 5.6 * Version: 13.6.0-rc.1 * Author: Gutenberg Team diff --git a/lib/compat/wordpress-5.9/admin-menu.php b/lib/compat/wordpress-5.9/admin-menu.php deleted file mode 100644 index c070761945092..0000000000000 --- a/lib/compat/wordpress-5.9/admin-menu.php +++ /dev/null @@ -1,112 +0,0 @@ - $menu_item ) { - if ( false !== strpos( $menu_item[2], 'customize.php' ) ) { - // Assume the first entry is the Customizer proper, if customize.php is linked > once. - if ( is_null( $customize_menu ) ) { - $indexes_to_remove[] = $index; - $customize_menu = $menu_item; - } - } - - if ( false !== strpos( $menu_item[2], 'site-editor.php' ) ) { - $indexes_to_remove[] = $index; - } - - if ( false !== strpos( $menu_item[2], 'gutenberg-widgets' ) ) { - $indexes_to_remove[] = $index; - } - } - - foreach ( $indexes_to_remove as $index ) { - unset( $submenu['themes.php'][ $index ] ); - } - - // Add Customizer back but with a new sub-menu position when a site requires this feature. - if ( gutenberg_site_requires_customizer() && $customize_menu ) { - $submenu['themes.php'][20] = $customize_menu; - } -} -add_action( 'admin_menu', 'gutenberg_remove_legacy_pages' ); - -/** - * Removes legacy adminbar items from FSE themes. - * - * @param WP_Admin_Bar $wp_admin_bar The admin-bar instance. - */ -function gutenberg_adminbar_items( $wp_admin_bar ) { - - // Early exit if not a block theme. - if ( ! wp_is_block_theme() ) { - return; - } - - // Remove customizer link, if this site does not rely on them for plugins or theme options. - if ( ! gutenberg_site_requires_customizer() ) { - $wp_admin_bar->remove_node( 'customize' ); - $wp_admin_bar->remove_node( 'customize-background' ); - $wp_admin_bar->remove_node( 'customize-header' ); - $wp_admin_bar->remove_node( 'widgets' ); - } - - // Add site-editor link. - if ( ! is_admin() && current_user_can( 'edit_theme_options' ) ) { - $wp_admin_bar->add_node( - array( - 'id' => 'site-editor', - 'title' => __( 'Edit site', 'gutenberg' ), - 'href' => admin_url( 'themes.php?page=gutenberg-edit-site' ), - ) - ); - } -} -add_action( 'admin_bar_menu', 'gutenberg_adminbar_items', 50 ); - -/** - * Override Site Editor URLs to use plugin page. - * - * @param string $url Admin URL link with path. - * @param string $path Path relative to the admin URL. - * @return string Modified Admin URL link. - */ -function gutenberg_override_site_editor_urls( $url, $path ) { - if ( 'site-editor.php' === $path ) { - $url = str_replace( $path, 'themes.php?page=gutenberg-edit-site', $url ); - } - - return $url; -} -add_filter( 'admin_url', 'gutenberg_override_site_editor_urls', 10, 2 ); - -/** - * Check if any plugin, or theme features, are using the Customizer. - * - * @return bool A boolean value indicating if Customizer support is needed. - */ -function gutenberg_site_requires_customizer() { - if ( has_action( 'customize_register' ) ) { - return true; - } - - return false; -} diff --git a/lib/compat/wordpress-5.9/block-editor-settings.php b/lib/compat/wordpress-5.9/block-editor-settings.php deleted file mode 100644 index 91849b0c6afb4..0000000000000 --- a/lib/compat/wordpress-5.9/block-editor-settings.php +++ /dev/null @@ -1,29 +0,0 @@ -= 5.9. - * - * @return void. - */ -function gutenberg_check_gallery_block_v2_compatibility() { - $use_balance_tags = (int) get_option( 'use_balanceTags' ); - $v2_gallery_enabled = boolval( 1 !== $use_balance_tags || is_wp_version_compatible( '5.9' ) ) ? 'true' : 'false'; - - wp_add_inline_script( - 'wp-dom-ready', - 'wp.galleryBlockV2Enabled = ' . $v2_gallery_enabled . ';', - 'after' - ); -} -add_action( 'init', 'gutenberg_check_gallery_block_v2_compatibility' ); - -/** - * Prevent use_balanceTags being enabled on WordPress 5.8 or earlier as it breaks - * the layout of the new Gallery block. - * - * @since 12.1.0 - * @todo This should be removed when the minimum required WP version is >= 5.9. - * - * @param int $new_value The new value for use_balanceTags. - */ -function gutenberg_use_balancetags_check( $new_value ) { - global $wp_version; - - if ( 1 === (int) $new_value && version_compare( $wp_version, '5.9', '<' ) ) { - /* translators: %s: Minimum required version */ - $message = sprintf( __( 'Gutenberg requires WordPress %s or later in order to enable the “Correct invalidly nested XHTML automatically” option. Please upgrade WordPress before enabling.', 'gutenberg' ), '5.9' ); - add_settings_error( 'gutenberg_use_balancetags_check', 'gutenberg_use_balancetags_check', $message, 'error' ); - if ( class_exists( 'WP_CLI' ) ) { - WP_CLI::error( $message ); - } - return 0; - } - - return $new_value; -} -add_filter( 'pre_update_option_use_balanceTags', 'gutenberg_use_balancetags_check' ); diff --git a/lib/compat/wordpress-5.9/block-patterns.php b/lib/compat/wordpress-5.9/block-patterns.php deleted file mode 100644 index f92a6203057e6..0000000000000 --- a/lib/compat/wordpress-5.9/block-patterns.php +++ /dev/null @@ -1,45 +0,0 @@ -is_error() ) { - return; - } - $patterns = $response->get_data(); - foreach ( $patterns as $pattern ) { - $pattern_name = sanitize_title( $pattern['title'] ); - $registry = WP_Block_Patterns_Registry::get_instance(); - // Some patterns might be already registered as core patterns with the `core` prefix. - $is_registered = $registry->is_registered( $pattern_name ) || $registry->is_registered( "core/$pattern_name" ); - if ( ! $is_registered ) { - register_block_pattern( $pattern_name, (array) $pattern ); - } - } - } -} diff --git a/lib/compat/wordpress-5.9/block-template-utils.php b/lib/compat/wordpress-5.9/block-template-utils.php deleted file mode 100644 index cfd6910e3859c..0000000000000 --- a/lib/compat/wordpress-5.9/block-template-utils.php +++ /dev/null @@ -1,630 +0,0 @@ - 'block-templates', - 'wp_template_part' => 'block-template-parts', - ); - } - - return array( - 'wp_template' => 'templates', - 'wp_template_part' => 'parts', - ); - } -} - -if ( ! function_exists( 'get_allowed_block_template_part_areas' ) ) { - /** - * Returns a filtered list of allowed area values for template parts. - * - * @return array The supported template part area values. - */ - function get_allowed_block_template_part_areas() { - $default_area_definitions = array( - array( - 'area' => WP_TEMPLATE_PART_AREA_UNCATEGORIZED, - 'label' => __( 'General', 'gutenberg' ), - 'description' => __( - 'General templates often perform a specific role like displaying post content, and are not tied to any particular area.', - 'gutenberg' - ), - 'icon' => 'layout', - 'area_tag' => 'div', - ), - array( - 'area' => WP_TEMPLATE_PART_AREA_HEADER, - 'label' => __( 'Header', 'gutenberg' ), - 'description' => __( - 'The Header template defines a page area that typically contains a title, logo, and main navigation.', - 'gutenberg' - ), - 'icon' => 'header', - 'area_tag' => 'header', - ), - array( - 'area' => WP_TEMPLATE_PART_AREA_FOOTER, - 'label' => __( 'Footer', 'gutenberg' ), - 'description' => __( - 'The Footer template defines a page area that typically contains site credits, social links, or any other combination of blocks.', - 'gutenberg' - ), - 'icon' => 'footer', - 'area_tag' => 'footer', - ), - ); - - /** - * Filters the list of allowed template part area values. - * - * @param array $default_areas An array of supported area objects. - */ - return apply_filters( 'default_wp_template_part_areas', $default_area_definitions ); - } -} - -if ( ! function_exists( 'get_default_block_template_types' ) ) { - /** - * Returns a filtered list of default template types, containing their - * localized titles and descriptions. - * - * @return array The default template types. - */ - function get_default_block_template_types() { - $default_template_types = array( - 'index' => array( - 'title' => _x( 'Index', 'Template name', 'gutenberg' ), - 'description' => __( 'Displays posts.', 'gutenberg' ), - ), - 'home' => array( - 'title' => _x( 'Home', 'Template name', 'gutenberg' ), - 'description' => __( 'Displays posts on the homepage, or on the Posts page if a static homepage is set.', 'gutenberg' ), - ), - 'front-page' => array( - 'title' => _x( 'Front Page', 'Template name', 'gutenberg' ), - 'description' => __( 'Displays the homepage.', 'gutenberg' ), - ), - 'singular' => array( - 'title' => _x( 'Singular', 'Template name', 'gutenberg' ), - 'description' => __( 'Displays a single post or page.', 'gutenberg' ), - ), - 'single' => array( - 'title' => _x( 'Single Post', 'Template name', 'gutenberg' ), - 'description' => __( 'Displays a single post.', 'gutenberg' ), - ), - 'page' => array( - 'title' => _x( 'Page', 'Template name', 'gutenberg' ), - 'description' => __( 'Displays a single page.', 'gutenberg' ), - ), - 'archive' => array( - 'title' => _x( 'Archive', 'Template name', 'gutenberg' ), - 'description' => __( 'Displays post categories, tags, and other archives.', 'gutenberg' ), - ), - 'author' => array( - 'title' => _x( 'Author', 'Template name', 'gutenberg' ), - 'description' => __( 'Displays latest posts written by a single author.', 'gutenberg' ), - ), - 'category' => array( - 'title' => _x( 'Category', 'Template name', 'gutenberg' ), - 'description' => __( 'Displays latest posts in single post category.', 'gutenberg' ), - ), - 'taxonomy' => array( - 'title' => _x( 'Taxonomy', 'Template name', 'gutenberg' ), - 'description' => __( 'Displays latest posts from a single post taxonomy.', 'gutenberg' ), - ), - 'date' => array( - 'title' => _x( 'Date', 'Template name', 'gutenberg' ), - 'description' => __( 'Displays posts from a specific date.', 'gutenberg' ), - ), - 'tag' => array( - 'title' => _x( 'Tag', 'Template name', 'gutenberg' ), - 'description' => __( 'Displays latest posts with a single post tag.', 'gutenberg' ), - ), - 'attachment' => array( - 'title' => __( 'Media', 'gutenberg' ), - 'description' => __( 'Displays individual media items or attachments.', 'gutenberg' ), - ), - 'search' => array( - 'title' => _x( 'Search', 'Template name', 'gutenberg' ), - 'description' => __( 'Displays search results.', 'gutenberg' ), - ), - 'privacy-policy' => array( - 'title' => __( 'Privacy Policy', 'gutenberg' ), - 'description' => __( 'Displays the privacy policy page.', 'gutenberg' ), - ), - '404' => array( - 'title' => _x( '404', 'Template name', 'gutenberg' ), - 'description' => __( 'Displays when no content is found.', 'gutenberg' ), - ), - ); - - /** - * Filters the list of template types. - * - * @param array $default_template_types An array of template types, formatted as [ slug => [ title, description ] ]. - * - * @since 5.x.x - */ - return apply_filters( 'default_template_types', $default_template_types ); - } -} - -if ( ! function_exists( '_filter_block_template_part_area' ) ) { - /** - * Checks whether the input 'area' is a supported value. - * Returns the input if supported, otherwise returns the 'uncategorized' value. - * - * @param string $type Template part area name. - * - * @return string Input if supported, else the uncategorized value. - */ - function _filter_block_template_part_area( $type ) { - $allowed_areas = array_map( - static function ( $item ) { - return $item['area']; - }, - get_allowed_block_template_part_areas() - ); - if ( in_array( $type, $allowed_areas, true ) ) { - return $type; - } - - /* translators: %1$s: Template area type, %2$s: the uncategorized template area value. */ - $warning_message = sprintf( __( '"%1$s" is not a supported wp_template_part area value and has been added as "%2$s".', 'gutenberg' ), $type, WP_TEMPLATE_PART_AREA_UNCATEGORIZED ); - trigger_error( $warning_message, E_USER_NOTICE ); - return WP_TEMPLATE_PART_AREA_UNCATEGORIZED; - } -} - -if ( ! function_exists( '_get_block_templates_paths' ) ) { - /** - * Finds all nested template part file paths in a theme's directory. - * - * @access private - * - * @param string $base_directory The theme's file path. - * @return array $path_list A list of paths to all template part files. - */ - function _get_block_templates_paths( $base_directory ) { - $path_list = array(); - if ( file_exists( $base_directory ) ) { - $nested_files = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $base_directory ) ); - $nested_html_files = new RegexIterator( $nested_files, '/^.+\.html$/i', RecursiveRegexIterator::GET_MATCH ); - foreach ( $nested_html_files as $path => $file ) { - $path_list[] = $path; - } - } - return $path_list; - } -} - -if ( ! function_exists( '_get_block_template_file' ) ) { - /** - * Retrieves the template file from the theme for a given slug. - * - * @access private - * @internal - * - * @param string $template_type wp_template or wp_template_part. - * @param string $slug template slug. - * - * @return array|null Template. - */ - function _get_block_template_file( $template_type, $slug ) { - if ( 'wp_template' !== $template_type && 'wp_template_part' !== $template_type ) { - return null; - } - - $themes = array( - get_stylesheet() => get_stylesheet_directory(), - get_template() => get_template_directory(), - ); - foreach ( $themes as $theme_slug => $theme_dir ) { - $template_base_paths = get_block_theme_folders( $theme_slug ); - $file_path = $theme_dir . '/' . $template_base_paths[ $template_type ] . '/' . $slug . '.html'; - if ( file_exists( $file_path ) ) { - $new_template_item = array( - 'slug' => $slug, - 'path' => $file_path, - 'theme' => $theme_slug, - 'type' => $template_type, - ); - - if ( 'wp_template_part' === $template_type ) { - return _add_block_template_part_area_info( $new_template_item ); - } - - if ( 'wp_template' === $template_type ) { - return _add_block_template_info( $new_template_item ); - } - - return $new_template_item; - } - } - - return null; - } -} - -if ( ! function_exists( '_get_block_templates_files' ) ) { - /** - * Retrieves the template files from the theme. - * - * @access private - * @internal - * - * @param string $template_type wp_template or wp_template_part. - * - * @return array Template. - */ - function _get_block_templates_files( $template_type ) { - if ( 'wp_template' !== $template_type && 'wp_template_part' !== $template_type ) { - return null; - } - - $themes = array( - get_stylesheet() => get_stylesheet_directory(), - get_template() => get_template_directory(), - ); - $template_files = array(); - foreach ( $themes as $theme_slug => $theme_dir ) { - $template_base_paths = get_block_theme_folders( $theme_slug ); - $theme_template_files = _get_block_templates_paths( $theme_dir . '/' . $template_base_paths[ $template_type ] ); - foreach ( $theme_template_files as $template_file ) { - $template_base_path = $template_base_paths[ $template_type ]; - $template_slug = substr( - $template_file, - // Starting position of slug. - strpos( $template_file, $template_base_path . DIRECTORY_SEPARATOR ) + 1 + strlen( $template_base_path ), - // Subtract ending '.html'. - -5 - ); - $new_template_item = array( - 'slug' => $template_slug, - 'path' => $template_file, - 'theme' => $theme_slug, - 'type' => $template_type, - ); - - if ( 'wp_template_part' === $template_type ) { - $template_files[] = _add_block_template_part_area_info( $new_template_item ); - } - - if ( 'wp_template' === $template_type ) { - $template_files[] = _add_block_template_info( $new_template_item ); - } - } - } - - return $template_files; - } -} - -if ( ! function_exists( '_add_block_template_info' ) ) { - /** - * Attempts to add custom template information to the template item. - * - * @param array $template_item Template to add information to (requires 'slug' field). - * @return array Template - */ - function _add_block_template_info( $template_item ) { - if ( ! WP_Theme_JSON_Resolver_Gutenberg::theme_has_support() ) { - return $template_item; - } - - $theme_data = WP_Theme_JSON_Resolver_Gutenberg::get_theme_data()->get_custom_templates(); - if ( isset( $theme_data[ $template_item['slug'] ] ) ) { - $template_item['title'] = $theme_data[ $template_item['slug'] ]['title']; - $template_item['postTypes'] = $theme_data[ $template_item['slug'] ]['postTypes']; - } - - return $template_item; - } -} - -if ( ! function_exists( '_add_block_template_part_area_info' ) ) { - /** - * Attempts to add the template part's area information to the input template. - * - * @param array $template_info Template to add information to (requires 'type' and 'slug' fields). - * - * @return array Template. - */ - function _add_block_template_part_area_info( $template_info ) { - if ( WP_Theme_JSON_Resolver_Gutenberg::theme_has_support() ) { - $theme_data = WP_Theme_JSON_Resolver_Gutenberg::get_theme_data()->get_template_parts(); - } - - if ( isset( $theme_data[ $template_info['slug'] ]['area'] ) ) { - $template_info['title'] = $theme_data[ $template_info['slug'] ]['title']; - $template_info['area'] = _filter_block_template_part_area( $theme_data[ $template_info['slug'] ]['area'] ); - } else { - $template_info['area'] = WP_TEMPLATE_PART_AREA_UNCATEGORIZED; - } - - return $template_info; - } -} - -if ( ! function_exists( '_flatten_blocks' ) ) { - /** - * Returns an array containing the references of - * the passed blocks and their inner blocks. - * - * @param array $blocks array of blocks. - * - * @return array block references to the passed blocks and their inner blocks. - */ - function _flatten_blocks( &$blocks ) { - $all_blocks = array(); - $queue = array(); - foreach ( $blocks as &$block ) { - $queue[] = &$block; - } - - while ( count( $queue ) > 0 ) { - $block = &$queue[0]; - array_shift( $queue ); - $all_blocks[] = &$block; - - if ( ! empty( $block['innerBlocks'] ) ) { - foreach ( $block['innerBlocks'] as &$inner_block ) { - $queue[] = &$inner_block; - } - } - } - - return $all_blocks; - } -} - -if ( ! function_exists( '_inject_theme_attribute_in_block_template_content' ) ) { - /** - * Parses wp_template content and injects the current theme's - * stylesheet as a theme attribute into each wp_template_part - * - * @param string $template_content serialized wp_template content. - * - * @return string Updated wp_template content. - */ - function _inject_theme_attribute_in_block_template_content( $template_content ) { - $has_updated_content = false; - $new_content = ''; - $template_blocks = parse_blocks( $template_content ); - - $blocks = _flatten_blocks( $template_blocks ); - foreach ( $blocks as &$block ) { - if ( - 'core/template-part' === $block['blockName'] && - ! isset( $block['attrs']['theme'] ) - ) { - $block['attrs']['theme'] = wp_get_theme()->get_stylesheet(); - $has_updated_content = true; - } - } - - if ( $has_updated_content ) { - foreach ( $template_blocks as &$block ) { - $new_content .= serialize_block( $block ); - } - - return $new_content; - } - - return $template_content; - } -} - -if ( ! function_exists( '_remove_theme_attribute_in_block_template_content' ) ) { - /** - * Parses wp_template content and removes the theme attribute from - * each wp_template_part - * - * @param string $template_content serialized wp_template content. - * - * @return string Updated wp_template content. - */ - function _remove_theme_attribute_in_block_template_content( $template_content ) { - $has_updated_content = false; - $new_content = ''; - $template_blocks = parse_blocks( $template_content ); - - $blocks = _flatten_blocks( $template_blocks ); - foreach ( $blocks as $key => $block ) { - if ( 'core/template-part' === $block['blockName'] && isset( $block['attrs']['theme'] ) ) { - unset( $blocks[ $key ]['attrs']['theme'] ); - $has_updated_content = true; - } - } - - if ( ! $has_updated_content ) { - return $template_content; - } - - foreach ( $template_blocks as $block ) { - $new_content .= serialize_block( $block ); - } - - return $new_content; - } -} - -if ( ! function_exists( '_build_block_template_result_from_file' ) ) { - /** - * Build a unified template object based on a theme file. - * - * @param array $template_file Theme file. - * @param array $template_type wp_template or wp_template_part. - * - * @return Gutenberg_Block_Template Template. - */ - function _build_block_template_result_from_file( $template_file, $template_type ) { - $default_template_types = get_default_block_template_types(); - $template_content = file_get_contents( $template_file['path'] ); - $theme = wp_get_theme()->get_stylesheet(); - - $template = new Gutenberg_Block_Template(); - $template->id = $theme . '//' . $template_file['slug']; - $template->theme = $theme; - $template->content = _inject_theme_attribute_in_block_template_content( $template_content ); - $template->slug = $template_file['slug']; - $template->source = 'theme'; - $template->type = $template_type; - $template->title = ! empty( $template_file['title'] ) ? $template_file['title'] : $template_file['slug']; - $template->status = 'publish'; - $template->has_theme_file = true; - $template->is_custom = true; - - if ( 'wp_template' === $template_type && isset( $default_template_types[ $template_file['slug'] ] ) ) { - $template->description = $default_template_types[ $template_file['slug'] ]['description']; - $template->title = $default_template_types[ $template_file['slug'] ]['title']; - $template->is_custom = false; - } - - if ( 'wp_template' === $template_type && isset( $template_file['postTypes'] ) ) { - $template->post_types = $template_file['postTypes']; - } - - if ( 'wp_template_part' === $template_type && isset( $template_file['area'] ) ) { - $template->area = $template_file['area']; - } - - return $template; - } -} - -if ( ! function_exists( 'get_block_file_template' ) ) { - /** - * Retrieves a single unified template object using its id. - * Retrieves the file template. - * - * @param string $id Template unique identifier (example: theme_slug//template_slug). - * @param array $template_type wp_template or wp_template_part. - * - * @return Gutenberg_Block_Template|null File template. - */ - function get_block_file_template( $id, $template_type = 'wp_template' ) { - /** - * Filters the block templates array before the query takes place. - * - * Return a non-null value to bypass the WordPress queries. - * - * @since 10.8 - * - * @param Gutenberg_Block_Template|null $block_template Return block template object to short-circuit the default query, - * or null to allow WP to run it's normal queries. - * @param string $id Template unique identifier (example: theme_slug//template_slug). - * @param array $template_type wp_template or wp_template_part. - */ - $block_template = apply_filters( 'pre_get_block_file_template', null, $id, $template_type ); - if ( ! is_null( $block_template ) ) { - return $block_template; - } - - $parts = explode( '//', $id, 2 ); - if ( count( $parts ) < 2 ) { - /** This filter is documented at the end of this function */ - return apply_filters( 'get_block_file_template', null, $id, $template_type ); - } - list( $theme, $slug ) = $parts; - - if ( wp_get_theme()->get_stylesheet() !== $theme ) { - /** This filter is documented at the end of this function */ - return apply_filters( 'get_block_file_template', null, $id, $template_type ); - } - - $template_file = _get_block_template_file( $template_type, $slug ); - if ( null === $template_file ) { - /** This filter is documented at the end of this function */ - return apply_filters( 'get_block_file_template', null, $id, $template_type ); - } - - $block_template = _build_block_template_result_from_file( $template_file, $template_type ); - - /** - * Filters the array of queried block templates array after they've been fetched. - * - * @since 10.8 - * - * @param null|Gutenberg_Block_Template $block_template The found block template. - * @param string $id Template unique identifier (example: theme_slug//template_slug). - * @param array $template_type wp_template or wp_template_part. - */ - return apply_filters( 'get_block_file_template', $block_template, $id, $template_type ); - } -} - -if ( ! function_exists( 'block_template_part' ) ) { - /** - * Print a template-part. - * - * @param string $part The template-part to print. Use "header" or "footer". - * - * @return void - */ - function block_template_part( $part ) { - $template_part = gutenberg_get_block_template( get_stylesheet() . '//' . $part, 'wp_template_part' ); - if ( ! $template_part || empty( $template_part->content ) ) { - return; - } - echo do_blocks( $template_part->content ); - } -} - -if ( ! function_exists( 'block_header_area' ) ) { - /** - * Print the header template-part. - * - * @return void - */ - function block_header_area() { - block_template_part( 'header' ); - } -} - -if ( ! function_exists( 'block_footer_area' ) ) { - /** - * Print the footer template-part. - * - * @return void - */ - function block_footer_area() { - block_template_part( 'footer' ); - } -} diff --git a/lib/compat/wordpress-5.9/block-template.php b/lib/compat/wordpress-5.9/block-template.php deleted file mode 100644 index fd4329c2cbf32..0000000000000 --- a/lib/compat/wordpress-5.9/block-template.php +++ /dev/null @@ -1,310 +0,0 @@ -content ) && is_user_logged_in() ) { - $_wp_current_template_content = - sprintf( - /* translators: %s: Template title */ - __( 'Empty template: %s', 'gutenberg' ), - $block_template->title - ); - } elseif ( ! empty( $block_template->content ) ) { - $_wp_current_template_content = $block_template->content; - } - if ( isset( $_GET['_wp-find-template'] ) ) { - wp_send_json_success( $block_template ); - } - } else { - if ( $template ) { - return $template; - } - - if ( 'index' === $type ) { - if ( isset( $_GET['_wp-find-template'] ) ) { - wp_send_json_error( array( 'message' => __( 'No matching template found.', 'gutenberg' ) ) ); - } - } else { - return false; // So that the template loader keeps looking for templates. - } - } - - // Add hooks for template canvas. - // Add viewport meta tag. - add_action( 'wp_head', 'gutenberg_viewport_meta_tag', 0 ); - - // Render title tag with content, regardless of whether theme has title-tag support. - remove_action( 'wp_head', '_wp_render_title_tag', 1 ); // Remove conditional title tag rendering... - // Override WP 5.8 title-tag support. - remove_action( 'wp_head', '_block_template_render_title_tag', 1 ); - add_action( 'wp_head', 'gutenberg_render_title_tag', 1 ); // ...and make it unconditional. - - // This file will be included instead of the theme's template file. - return gutenberg_dir_path() . 'lib/compat/wordpress-5.9/template-canvas.php'; -} - -/** - * Return the correct 'wp_template' to render for the request template type. - * - * Accepts an optional $template_hierarchy argument as a hint. - * - * @since 5.9.0 Added the `$fallback_template` parameter. - * - * @param string $template_type The current template type. - * @param string[] $template_hierarchy (optional) The current template hierarchy, ordered by priority. - * @param string $fallback_template A PHP fallback template to use if no matching block template is found. - * @return null|Gutenberg_Block_Template A block template if found. Null if not. - */ -function gutenberg_resolve_template( $template_type, $template_hierarchy, $fallback_template ) { - if ( ! $template_type ) { - return null; - } - - if ( empty( $template_hierarchy ) ) { - $template_hierarchy = array( $template_type ); - } - - $slugs = array_map( - 'gutenberg_strip_template_file_suffix', - $template_hierarchy - ); - - // Find all potential templates 'wp_template' post matching the hierarchy. - $query = array( - 'theme' => wp_get_theme()->get_stylesheet(), - 'slug__in' => $slugs, - ); - $templates = gutenberg_get_block_templates( $query ); - - // Order these templates per slug priority. - // Build map of template slugs to their priority in the current hierarchy. - $slug_priorities = array_flip( $slugs ); - - usort( - $templates, - function ( $template_a, $template_b ) use ( $slug_priorities ) { - return $slug_priorities[ $template_a->slug ] - $slug_priorities[ $template_b->slug ]; - } - ); - - $theme_base_path = get_stylesheet_directory() . DIRECTORY_SEPARATOR; - $parent_theme_base_path = get_template_directory() . DIRECTORY_SEPARATOR; - - // Is the current theme a child theme, and is the PHP fallback template part of it? - if ( - strpos( $fallback_template, $theme_base_path ) === 0 && - strpos( $fallback_template, $parent_theme_base_path ) === false - ) { - $fallback_template_slug = substr( - $fallback_template, - // Starting position of slug. - strpos( $fallback_template, $theme_base_path ) + strlen( $theme_base_path ), - // Remove '.php' suffix. - -4 - ); - - // Is our candidate block template's slug identical to our PHP fallback template's? - if ( - count( $templates ) && - $fallback_template_slug === $templates[0]->slug && - 'theme' === $templates[0]->source - ) { - // Unfortunately, we cannot trust $templates[0]->theme, since it will always - // be set to the current theme's slug by _build_block_template_result_from_file(), - // even if the block template is really coming from the current theme's parent. - // (The reason for this is that we want it to be associated with the current theme - // -- not its parent -- once we edit it and store it to the DB as a wp_template CPT.) - // Instead, we use _get_block_template_file() to locate the block template file. - $template_file = _get_block_template_file( 'wp_template', $fallback_template_slug ); - if ( $template_file && get_template() === $template_file['theme'] ) { - // The block template is part of the parent theme, so we - // have to give precedence to the child theme's PHP template. - array_shift( $templates ); - } - } - } - - return count( $templates ) ? $templates[0] : null; -} - -/** - * Displays title tag with content, regardless of whether theme has title-tag support. - * - * @see _wp_render_title_tag() - */ -function gutenberg_render_title_tag() { - echo '' . wp_get_document_title() . '' . "\n"; -} - -/** - * Returns the markup for the current template. - */ -function gutenberg_get_the_template_html() { - global $_wp_current_template_content; - global $wp_embed; - - if ( ! $_wp_current_template_content ) { - if ( is_user_logged_in() ) { - return '

' . esc_html__( 'No matching template found', 'gutenberg' ) . '

'; - } - return; - } - - $content = $wp_embed->run_shortcode( $_wp_current_template_content ); - $content = $wp_embed->autoembed( $content ); - $content = do_blocks( $content ); - $content = wptexturize( $content ); - $content = convert_smilies( $content ); - $content = shortcode_unautop( $content ); - $content = wp_filter_content_tags( $content ); - $content = do_shortcode( $content ); - $content = str_replace( ']]>', ']]>', $content ); - - // Wrap block template in .wp-site-blocks to allow for specific descendant styles - // (e.g. `.wp-site-blocks > *`). - return '
' . $content . '
'; -} - -/** - * Renders a 'viewport' meta tag. - * - * This is hooked into {@see 'wp_head'} to decouple its output from the default template canvas. - */ -function gutenberg_viewport_meta_tag() { - echo '' . "\n"; -} - -/** - * Strips .php suffix from template file names. - * - * @access private - * - * @param string $template_file Template file name. - * @return string Template file name without extension. - */ -function gutenberg_strip_template_file_suffix( $template_file ) { - return preg_replace( '/\.(php|html)$/', '', $template_file ); -} - -/** - * Removes post details from block context when rendering a block template. - * - * @param array $context Default context. - * - * @return array Filtered context. - */ -function gutenberg_template_render_without_post_block_context( $context ) { - /* - * When loading a template or template part directly and not through a page - * that resolves it, the top-level post ID and type context get set to that - * of the template part. Templates are just the structure of a site, and - * they should not be available as post context because blocks like Post - * Content would recurse infinitely. - */ - if ( isset( $context['postType'] ) && - ( 'wp_template' === $context['postType'] || 'wp_template_part' === $context['postType'] ) ) { - unset( $context['postId'] ); - unset( $context['postType'] ); - } - - return $context; -} - -// override WordPress 5.8 filters. -remove_filter( 'render_block_context', '_block_template_render_without_post_block_context' ); -add_filter( 'render_block_context', 'gutenberg_template_render_without_post_block_context' ); - -/** - * Sets the current WP_Query to return auto-draft posts. - * - * The auto-draft status indicates a new post, so allow the the WP_Query instance to - * return an auto-draft post for template resolution when editing a new post. - * - * @param WP_Query $wp_query Current WP_Query instance, passed by reference. - * @return void - */ -function gutenberg_resolve_template_for_new_post( $wp_query ) { - if ( ! $wp_query->is_main_query() ) { - return; - } - remove_filter( 'pre_get_posts', 'gutenberg_resolve_template_for_new_post' ); - - // Pages. - $page_id = isset( $wp_query->query['page_id'] ) ? $wp_query->query['page_id'] : null; - - // Posts, including custom post types. - $p = isset( $wp_query->query['p'] ) ? $wp_query->query['p'] : null; - - $post_id = $page_id ? $page_id : $p; - $post = get_post( $post_id ); - - if ( - $post && - 'auto-draft' === $post->post_status && - current_user_can( 'edit_post', $post->ID ) - ) { - $wp_query->set( 'post_status', 'auto-draft' ); - } -} diff --git a/lib/compat/wordpress-5.9/blocks.php b/lib/compat/wordpress-5.9/blocks.php deleted file mode 100644 index 5793042079871..0000000000000 --- a/lib/compat/wordpress-5.9/blocks.php +++ /dev/null @@ -1,247 +0,0 @@ - '', - 'arrow' => array( - 'next' => '→', - 'previous' => '←', - ), - 'chevron' => array( - 'next' => '»', - 'previous' => '«', - ), - ); - if ( ! empty( $block->context['paginationArrow'] ) && array_key_exists( $block->context['paginationArrow'], $arrow_map ) && ! empty( $arrow_map[ $block->context['paginationArrow'] ] ) ) { - $pagination_type = $is_next ? 'next' : 'previous'; - $arrow_attribute = $block->context['paginationArrow']; - $arrow = $arrow_map[ $block->context['paginationArrow'] ][ $pagination_type ]; - $arrow_classes = "wp-block-query-pagination-$pagination_type-arrow is-arrow-$arrow_attribute"; - return "$arrow"; - } - return null; - } -} - -/** - * Update allowed inline style attributes list. - * - * Note: This should be removed when the minimum required WP version is >= 5.9. - * - * @param string[] $attrs Array of allowed CSS attributes. - * @return string[] CSS attributes. - */ -function gutenberg_safe_style_attrs( $attrs ) { - $attrs[] = 'object-position'; - $attrs[] = 'border-top-left-radius'; - $attrs[] = 'border-top-right-radius'; - $attrs[] = 'border-bottom-right-radius'; - $attrs[] = 'border-bottom-left-radius'; - $attrs[] = 'filter'; - - return $attrs; -} -add_filter( 'safe_style_css', 'gutenberg_safe_style_attrs' ); - -if ( ! function_exists( '_wp_normalize_relative_css_links' ) ) { - /** - * Make URLs relative to the WordPress installation. - * - * @since 5.9.0 - * - * @param string $css The CSS to make URLs relative to the WordPress installation. - * @param string $stylesheet_url The URL to the stylesheet. - * - * @return string The CSS with URLs made relative to the WordPress installation. - */ - function _wp_normalize_relative_css_links( $css, $stylesheet_url ) { - $has_src_results = preg_match_all( '#url\s*\(\s*[\'"]?\s*([^\'"\)]+)#', $css, $src_results ); - if ( $has_src_results ) { - // Loop through the URLs to find relative ones. - foreach ( $src_results[1] as $src_index => $src_result ) { - // Skip if this is an absolute URL. - if ( 0 === strpos( $src_result, 'http' ) || 0 === strpos( $src_result, '//' ) ) { - continue; - } - - // Build the absolute URL. - $absolute_url = dirname( $stylesheet_url ) . '/' . $src_result; - $absolute_url = str_replace( '/./', '/', $absolute_url ); - // Convert to URL related to the site root. - $relative_url = wp_make_link_relative( $absolute_url ); - - // Replace the URL in the CSS. - $css = str_replace( - $src_results[0][ $src_index ], - str_replace( $src_result, $relative_url, $src_results[0][ $src_index ] ), - $css - ); - } - } - - return $css; - } -} - -if ( ! function_exists( 'wp_enqueue_block_style' ) ) { - /** - * Enqueue a stylesheet for a specific block. - * - * If the theme has opted-in to separate-styles loading, - * then the stylesheet will be enqueued on-render, - * otherwise when the block inits. - * - * @since 5.9.0 - * - * @param string $block_name The block-name, including namespace. - * @param array $args An array of arguments [handle,src,deps,ver,media]. - * - * @return void - */ - function wp_enqueue_block_style( $block_name, $args ) { - $args = wp_parse_args( - $args, - array( - 'handle' => '', - 'src' => '', - 'deps' => array(), - 'ver' => false, - 'media' => 'all', - ) - ); - - /** - * Callback function to register and enqueue styles. - * - * @param string $content When the callback is used for the render_block filter, - * the content needs to be returned so the function parameter - * is to ensure the content exists. - * @return string Block content. - */ - $callback = static function( $content ) use ( $args ) { - // Register the stylesheet. - if ( ! empty( $args['src'] ) ) { - wp_register_style( $args['handle'], $args['src'], $args['deps'], $args['ver'], $args['media'] ); - } - - // Add `path` data if provided. - if ( isset( $args['path'] ) ) { - wp_style_add_data( $args['handle'], 'path', $args['path'] ); - - // Get the RTL file path. - $rtl_file_path = str_replace( '.css', '-rtl.css', $args['path'] ); - - // Add RTL stylesheet. - if ( file_exists( $rtl_file_path ) ) { - wp_style_add_data( $args['handle'], 'rtl', 'replace' ); - - if ( is_rtl() ) { - wp_style_add_data( $args['handle'], 'path', $rtl_file_path ); - } - } - } - - // Enqueue the stylesheet. - wp_enqueue_style( $args['handle'] ); - - return $content; - }; - - $hook = did_action( 'wp_enqueue_scripts' ) ? 'wp_footer' : 'wp_enqueue_scripts'; - if ( wp_should_load_separate_core_block_assets() ) { - /** - * Callback function to register and enqueue styles. - * - * @param string $content The block content. - * @param array $block The full block, including name and attributes. - * @return string Block content. - */ - $callback_separate = static function( $content, $block ) use ( $block_name, $callback ) { - if ( ! empty( $block['blockName'] ) && $block_name === $block['blockName'] ) { - return $callback( $content ); - } - return $content; - }; - - /* - * The filter's callback here is an anonymous function because - * using a named function in this case is not possible. - * - * The function cannot be unhooked, however, users are still able - * to dequeue the stylesheets registered/enqueued by the callback - * which is why in this case, using an anonymous function - * was deemed acceptable. - */ - add_filter( 'render_block', $callback_separate, 10, 2 ); - return; - } - - /* - * The filter's callback here is an anonymous function because - * using a named function in this case is not possible. - * - * The function cannot be unhooked, however, users are still able - * to dequeue the stylesheets registered/enqueued by the callback - * which is why in this case, using an anonymous function - * was deemed acceptable. - */ - add_filter( $hook, $callback ); - - // Enqueue assets in the editor. - add_action( 'enqueue_block_assets', $callback ); - } -} - -/** - * Allow multiple block styles. - * - * @since 5.9.0 - * - * @param array $metadata Metadata for registering a block type. - * - * @return array - */ -function gutenberg_multiple_block_styles( $metadata ) { - foreach ( array( 'style', 'editorStyle' ) as $key ) { - if ( ! empty( $metadata[ $key ] ) && is_array( $metadata[ $key ] ) ) { - $default_style = array_shift( $metadata[ $key ] ); - foreach ( $metadata[ $key ] as $handle ) { - $args = array( 'handle' => $handle ); - if ( 0 === strpos( $handle, 'file:' ) && isset( $metadata['file'] ) ) { - $style_path = remove_block_asset_path_prefix( $handle ); - $args = array( - 'handle' => sanitize_key( "{$metadata['name']}-{$style_path}" ), - 'src' => plugins_url( $style_path, $metadata['file'] ), - ); - } - - wp_enqueue_block_style( $metadata['name'], $args ); - } - - // Only return the 1st item in the array. - $metadata[ $key ] = $default_style; - } - } - return $metadata; -} -add_filter( 'block_type_metadata', 'gutenberg_multiple_block_styles' ); diff --git a/lib/compat/wordpress-5.9/class-gutenberg-block-template.php b/lib/compat/wordpress-5.9/class-gutenberg-block-template.php deleted file mode 100644 index 4259457e77231..0000000000000 --- a/lib/compat/wordpress-5.9/class-gutenberg-block-template.php +++ /dev/null @@ -1,131 +0,0 @@ -namespace = 'wp/v2'; - $this->rest_base = 'global-styles'; - $this->post_type = 'wp_global_styles'; - } - - /** - * Registers the controllers routes. - * - * @since 5.9.0 - * - * @return void - */ - public function register_routes() { - // List themes global styles. - register_rest_route( - $this->namespace, - // The route. - sprintf( - '/%s/themes/(?P%s)', - $this->rest_base, - // Matches theme's directory: `/themes///` or `/themes//`. - // Excludes invalid directory name characters: `/:<>*?"|`. - '[^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?' - ), - array( - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_theme_item' ), - 'permission_callback' => array( $this, 'get_theme_item_permissions_check' ), - 'args' => array( - 'stylesheet' => array( - 'description' => __( 'The theme identifier', 'gutenberg' ), - 'type' => 'string', - 'sanitize_callback' => array( $this, '_sanitize_global_styles_callback' ), - ), - ), - ), - ) - ); - - // Lists/updates a single global style variation based on the given id. - register_rest_route( - $this->namespace, - '/' . $this->rest_base . '/(?P[\/\w-]+)', - array( - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_item' ), - 'permission_callback' => array( $this, 'get_item_permissions_check' ), - 'args' => array( - 'id' => array( - 'description' => __( 'The id of a template', 'gutenberg' ), - 'type' => 'string', - 'sanitize_callback' => array( $this, '_sanitize_global_styles_callback' ), - ), - ), - ), - array( - 'methods' => WP_REST_Server::EDITABLE, - 'callback' => array( $this, 'update_item' ), - 'permission_callback' => array( $this, 'update_item_permissions_check' ), - 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), - ), - 'schema' => array( $this, 'get_public_item_schema' ), - ) - ); - } - - /** - * Sanitize the global styles ID or stylesheet to decode endpoint. - * For example, `wp/v2/global-styles/twentytwentytwo%200.4.0` - * would be decoded to `twentytwentytwo 0.4.0`. - * - * @since 5.9.0 - * - * @param string $id_or_stylesheet Global styles ID or stylesheet. - * @return string Sanitized global styles ID or stylesheet. - */ - public function _sanitize_global_styles_callback( $id_or_stylesheet ) { - return urldecode( $id_or_stylesheet ); - } - - /** - * Checks if a given request has access to read a single global style. - * - * @since 5.9.0 - * - * @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 ) { - $post = $this->get_post( $request['id'] ); - if ( is_wp_error( $post ) ) { - return $post; - } - - if ( 'edit' === $request['context'] && $post && ! $this->check_update_permission( $post ) ) { - return new WP_Error( - 'rest_forbidden_context', - __( 'Sorry, you are not allowed to edit this global style.', 'gutenberg' ), - array( 'status' => rest_authorization_required_code() ) - ); - } - - if ( ! $this->check_read_permission( $post ) ) { - return new WP_Error( - 'rest_cannot_view', - __( 'Sorry, you are not allowed to view this global style.', 'gutenberg' ), - array( 'status' => rest_authorization_required_code() ) - ); - } - - return true; - } - - /** - * Checks if a global style can be read. - * - * @since 5.9.0 - * - * @param WP_Post $post Post object. - * @return bool Whether the post can be read. - */ - protected function check_read_permission( $post ) { - return current_user_can( 'read_post', $post->ID ); - } - - /** - * Returns the given global styles config. - * - * @since 5.9.0 - * - * @param WP_REST_Request $request The request instance. - * - * @return WP_REST_Response|WP_Error - */ - public function get_item( $request ) { - $post = $this->get_post( $request['id'] ); - if ( is_wp_error( $post ) ) { - return $post; - } - - return $this->prepare_item_for_response( $post, $request ); - } - - /** - * Checks if a given request has access to write a single global styles config. - * - * @since 5.9.0 - * - * @param WP_REST_Request $request Full details about the request. - * @return true|WP_Error True if the request has write access for the item, WP_Error object otherwise. - */ - public function update_item_permissions_check( $request ) { - $post = $this->get_post( $request['id'] ); - if ( is_wp_error( $post ) ) { - return $post; - } - - if ( $post && ! $this->check_update_permission( $post ) ) { - return new WP_Error( - 'rest_cannot_edit', - __( 'Sorry, you are not allowed to edit this global style.', 'gutenberg' ), - array( 'status' => rest_authorization_required_code() ) - ); - } - - return true; - } - - /** - * Checks if a global style can be edited. - * - * @since 5.9.0 - * - * @param WP_Post $post Post object. - * @return bool Whether the post can be edited. - */ - protected function check_update_permission( $post ) { - return current_user_can( 'edit_post', $post->ID ); - } - - /** - * Updates a single global style config. - * - * @since 5.9.0 - * - * @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 update_item( $request ) { - $post_before = $this->get_post( $request['id'] ); - if ( is_wp_error( $post_before ) ) { - return $post_before; - } - - $changes = $this->prepare_item_for_database( $request ); - $result = wp_update_post( wp_slash( (array) $changes ), true, false ); - if ( is_wp_error( $result ) ) { - return $result; - } - - $post = get_post( $request['id'] ); - $fields_update = $this->update_additional_fields_for_object( $post, $request ); - if ( is_wp_error( $fields_update ) ) { - return $fields_update; - } - - wp_after_insert_post( $post, true, $post_before ); - - $response = $this->prepare_item_for_response( $post, $request ); - - return rest_ensure_response( $response ); - } - - /** - * Prepares a single global styles config for update. - * - * @since 5.9.0 - * - * @param WP_REST_Request $request Request object. - * @return stdClass Changes to pass to wp_update_post. - */ - protected function prepare_item_for_database( $request ) { - $changes = new stdClass(); - $changes->ID = $request['id']; - - $post = get_post( $request['id'] ); - $existing_config = array(); - if ( $post ) { - $existing_config = json_decode( $post->post_content, true ); - $json_decoding_error = json_last_error(); - if ( JSON_ERROR_NONE !== $json_decoding_error || ! isset( $existing_config['isGlobalStylesUserThemeJSON'] ) || - ! $existing_config['isGlobalStylesUserThemeJSON'] ) { - $existing_config = array(); - } - } - - if ( isset( $request['styles'] ) || isset( $request['settings'] ) ) { - $config = array(); - if ( isset( $request['styles'] ) ) { - $config['styles'] = $request['styles']; - } elseif ( isset( $existing_config['styles'] ) ) { - $config['styles'] = $existing_config['styles']; - } - if ( isset( $request['settings'] ) ) { - $config['settings'] = $request['settings']; - } elseif ( isset( $existing_config['settings'] ) ) { - $config['settings'] = $existing_config['settings']; - } - $config['isGlobalStylesUserThemeJSON'] = true; - $config['version'] = WP_Theme_JSON_Gutenberg::LATEST_SCHEMA; - $changes->post_content = wp_json_encode( $config ); - } - - // Post title. - if ( isset( $request['title'] ) ) { - if ( is_string( $request['title'] ) ) { - $changes->post_title = $request['title']; - } elseif ( ! empty( $request['title']['raw'] ) ) { - $changes->post_title = $request['title']['raw']; - } - } - - return $changes; - } - - /** - * Prepare a global styles config output for response. - * - * @since 5.9.0 - * - * @param WP_Post $post Global Styles post object. - * @param WP_REST_Request $request Request object. - * @return WP_REST_Response Response object. - */ - public function prepare_item_for_response( $post, $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable - $raw_config = json_decode( $post->post_content, true ); - $is_global_styles_user_theme_json = isset( $raw_config['isGlobalStylesUserThemeJSON'] ) && true === $raw_config['isGlobalStylesUserThemeJSON']; - $config = array(); - if ( $is_global_styles_user_theme_json ) { - $config = ( new WP_Theme_JSON_Gutenberg( $raw_config, 'custom' ) )->get_raw_data(); - } - - // Base fields for every post. - $data = array(); - $fields = $this->get_fields_for_response( $request ); - - if ( rest_is_field_included( 'id', $fields ) ) { - $data['id'] = $post->ID; - } - - if ( rest_is_field_included( 'title', $fields ) ) { - $data['title'] = array(); - } - if ( rest_is_field_included( 'title.raw', $fields ) ) { - $data['title']['raw'] = $post->post_title; - } - if ( rest_is_field_included( 'title.rendered', $fields ) ) { - add_filter( 'protected_title_format', array( $this, 'protected_title_format' ) ); - - $data['title']['rendered'] = get_the_title( $post->ID ); - - remove_filter( 'protected_title_format', array( $this, 'protected_title_format' ) ); - } - - if ( rest_is_field_included( 'settings', $fields ) ) { - $data['settings'] = ! empty( $config['settings'] ) && $is_global_styles_user_theme_json ? $config['settings'] : new stdClass(); - } - - if ( rest_is_field_included( 'styles', $fields ) ) { - $data['styles'] = ! empty( $config['styles'] ) && $is_global_styles_user_theme_json ? $config['styles'] : new stdClass(); - } - - $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; - $data = $this->add_additional_fields_to_object( $data, $request ); - $data = $this->filter_response_by_context( $data, $context ); - - // Wrap the data in a response object. - $response = rest_ensure_response( $data ); - - $links = $this->prepare_links( $post->ID ); - $response->add_links( $links ); - if ( ! empty( $links['self']['href'] ) ) { - $actions = $this->get_available_actions(); - $self = $links['self']['href']; - foreach ( $actions as $rel ) { - $response->add_link( $rel, $self ); - } - } - - return $response; - } - - /** - * Get the post, if the ID is valid. - * - * @since 5.9.0 - * - * @param int $id Supplied ID. - * @return WP_Post|WP_Error Post object if ID is valid, WP_Error otherwise. - */ - protected function get_post( $id ) { - $error = new WP_Error( - 'rest_global_styles_not_found', - __( 'No global styles config exist with that id.', 'gutenberg' ), - array( 'status' => 404 ) - ); - - $id = (int) $id; - if ( $id <= 0 ) { - return $error; - } - - $post = get_post( $id ); - if ( empty( $post ) || empty( $post->ID ) || $this->post_type !== $post->post_type ) { - return $error; - } - - return $post; - } - - - /** - * Prepares links for the request. - * - * @since 5.9.0 - * - * @param integer $id ID. - * @return array Links for the given post. - */ - protected function prepare_links( $id ) { - $base = sprintf( '%s/%s', $this->namespace, $this->rest_base ); - - $links = array( - 'self' => array( - 'href' => rest_url( trailingslashit( $base ) . $id ), - ), - ); - - return $links; - } - - /** - * Get the link relations available for the post and current user. - * - * @since 5.9.0 - * - * @return array List of link relations. - */ - protected function get_available_actions() { - $rels = array(); - - $post_type = get_post_type_object( $this->post_type ); - if ( current_user_can( $post_type->cap->publish_posts ) ) { - $rels[] = 'https://api.w.org/action-publish'; - } - - return $rels; - } - - /** - * Overwrites the default protected title format. - * - * By default, WordPress will show password protected posts with a title of - * "Protected: %s", as the REST API communicates the protected status of a post - * in a machine readable format, we remove the "Protected: " prefix. - * - * @since 5.9.0 - * - * @return string Protected title format. - */ - public function protected_title_format() { - return '%s'; - } - - /** - * Retrieves the query params for the global styles collection. - * - * @since 5.9.0 - * - * @return array Collection parameters. - */ - public function get_collection_params() { - return array(); - } - - /** - * Retrieves the global styles type' schema, conforming to JSON Schema. - * - * @since 5.9.0 - * - * @return array Item schema data. - */ - public function get_item_schema() { - if ( $this->schema ) { - return $this->add_additional_fields_schema( $this->schema ); - } - - $schema = array( - '$schema' => 'http://json-schema.org/draft-04/schema#', - 'title' => $this->post_type, - 'type' => 'object', - 'properties' => array( - 'id' => array( - 'description' => __( 'ID of global styles config.', 'gutenberg' ), - 'type' => 'string', - 'context' => array( 'embed', 'view', 'edit' ), - 'readonly' => true, - ), - 'styles' => array( - 'description' => __( 'Global styles.', 'gutenberg' ), - 'type' => array( 'object' ), - 'context' => array( 'view', 'edit' ), - ), - 'settings' => array( - 'description' => __( 'Global settings.', 'gutenberg' ), - 'type' => array( 'object' ), - 'context' => array( 'view', 'edit' ), - ), - 'title' => array( - 'description' => __( 'Title of the global styles variation.', 'gutenberg' ), - 'type' => array( 'object', 'string' ), - 'default' => '', - 'context' => array( 'embed', 'view', 'edit' ), - 'properties' => array( - 'raw' => array( - 'description' => __( 'Title for the global styles variation, as it exists in the database.', 'gutenberg' ), - 'type' => 'string', - 'context' => array( 'view', 'edit', 'embed' ), - ), - 'rendered' => array( - 'description' => __( 'HTML title for the post, transformed for display.', 'gutenberg' ), - 'type' => 'string', - 'context' => array( 'view', 'edit', 'embed' ), - 'readonly' => true, - ), - ), - ), - ), - ); - - $this->schema = $schema; - - return $this->add_additional_fields_schema( $this->schema ); - } - - /** - * Checks if a given request has access to read a single theme global styles config. - * - * @since 5.9.0 - * - * @param WP_REST_Request $request Full details about the request. - * @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise. - */ - public function get_theme_item_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable - // Verify if the current user has edit_theme_options capability. - // This capability is required to edit/view/delete templates. - if ( ! current_user_can( 'edit_theme_options' ) ) { - return new WP_Error( - 'rest_cannot_manage_global_styles', - __( 'Sorry, you are not allowed to access the global styles on this site.', 'gutenberg' ), - array( - 'status' => rest_authorization_required_code(), - ) - ); - } - - return true; - } - - /** - * Returns the given theme global styles config. - * - * @since 5.9.0 - * - * @param WP_REST_Request $request The request instance. - * @return WP_REST_Response|WP_Error - */ - public function get_theme_item( $request ) { - if ( wp_get_theme()->get_stylesheet() !== $request['stylesheet'] ) { - // This endpoint only supports the active theme for now. - return new WP_Error( - 'rest_theme_not_found', - __( 'Theme not found.', 'gutenberg' ), - array( 'status' => 404 ) - ); - } - - $theme = WP_Theme_JSON_Resolver_Gutenberg::get_merged_data( 'theme' ); - $data = array(); - $fields = $this->get_fields_for_response( $request ); - - if ( rest_is_field_included( 'settings', $fields ) ) { - $data['settings'] = $theme->get_settings(); - } - - if ( rest_is_field_included( 'styles', $fields ) ) { - $raw_data = $theme->get_raw_data(); - if ( isset( $raw_data['styles'] ) ) { - $data['styles'] = $raw_data['styles']; - } - } - - $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; - $data = $this->add_additional_fields_to_object( $data, $request ); - $data = $this->filter_response_by_context( $data, $context ); - - $response = rest_ensure_response( $data ); - - $links = array( - 'self' => array( - 'href' => rest_url( sprintf( '%s/%s/themes/%s', $this->namespace, $this->rest_base, $request['stylesheet'] ) ), - ), - ); - - $response->add_links( $links ); - - return $response; - } - } -} diff --git a/lib/compat/wordpress-5.9/class-wp-rest-menu-items-controller.php b/lib/compat/wordpress-5.9/class-wp-rest-menu-items-controller.php deleted file mode 100644 index 4102184864d78..0000000000000 --- a/lib/compat/wordpress-5.9/class-wp-rest-menu-items-controller.php +++ /dev/null @@ -1,1169 +0,0 @@ - true ); - - /** - * Overrides the route registration to support "allow_batch". - * - * @since 9.2.0 - */ - public function register_routes() { - register_rest_route( - $this->namespace, - '/' . $this->rest_base, - array( - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_items' ), - 'permission_callback' => array( $this, 'get_items_permissions_check' ), - 'args' => $this->get_collection_params(), - ), - array( - 'methods' => WP_REST_Server::CREATABLE, - 'callback' => array( $this, 'create_item' ), - 'permission_callback' => array( $this, 'create_item_permissions_check' ), - 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), - ), - 'allow_batch' => $this->allow_batch, - 'schema' => array( $this, 'get_public_item_schema' ), - ) - ); - - $schema = $this->get_item_schema(); - $get_item_args = array( - 'context' => $this->get_context_param( array( 'default' => 'view' ) ), - ); - if ( isset( $schema['properties']['password'] ) ) { - $get_item_args['password'] = array( - 'description' => __( 'The password for the post if it is password protected.', 'gutenberg' ), - 'type' => 'string', - ); - } - register_rest_route( - $this->namespace, - '/' . $this->rest_base . '/(?P[\d]+)', - array( - 'args' => array( - 'id' => array( - 'description' => __( 'Unique identifier for the object.', 'gutenberg' ), - 'type' => 'integer', - ), - ), - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_item' ), - 'permission_callback' => array( $this, 'get_item_permissions_check' ), - 'args' => $get_item_args, - ), - array( - 'methods' => WP_REST_Server::EDITABLE, - 'callback' => array( $this, 'update_item' ), - 'permission_callback' => array( $this, 'update_item_permissions_check' ), - 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), - ), - array( - 'methods' => WP_REST_Server::DELETABLE, - 'callback' => array( $this, 'delete_item' ), - 'permission_callback' => array( $this, 'delete_item_permissions_check' ), - 'args' => array( - 'force' => array( - 'type' => 'boolean', - 'default' => false, - 'description' => __( 'Whether to bypass Trash and force deletion.', 'gutenberg' ), - ), - ), - ), - 'allow_batch' => $this->allow_batch, - 'schema' => array( $this, 'get_public_item_schema' ), - ) - ); - } - - /** - * Get the post, if the ID is valid. - * - * @param int $id Supplied ID. - * - * @return object|WP_Error Post object if ID is valid, WP_Error otherwise. - */ - protected function get_post( $id ) { - return $this->get_nav_menu_item( $id ); - } - - /** - * Get the nav menu item, if the ID is valid. - * - * @param int $id Supplied ID. - * - * @return object|WP_Error Post object if ID is valid, WP_Error otherwise. - */ - protected function get_nav_menu_item( $id ) { - $post = parent::get_post( $id ); - if ( is_wp_error( $post ) ) { - return $post; - } - $nav_item = wp_setup_nav_menu_item( $post ); - - return $nav_item; - } - - /** - * Checks if a given request has access to read a menu item if they have access to edit them. - * - * @param WP_REST_Request $request Full details about the request. - * @return bool|WP_Error True if the request has read access for the item, WP_Error object otherwise. - */ - public function get_item_permissions_check( $request ) { - $post = $this->get_post( $request['id'] ); - if ( is_wp_error( $post ) ) { - return $post; - } - if ( $post && ! $this->check_update_permission( $post ) ) { - return new WP_Error( 'rest_cannot_view', __( 'Sorry, you cannot view this menu item, unless you have access to permission edit it. ', 'gutenberg' ), array( 'status' => rest_authorization_required_code() ) ); - } - - return parent::get_item_permissions_check( $request ); - } - - /** - * Checks if a given request has access to read menu items if they have access to edit them. - * - * @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 ) { - $post_type = get_post_type_object( $this->post_type ); - if ( ! current_user_can( $post_type->cap->edit_posts ) ) { - if ( 'edit' === $request['context'] ) { - return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you are not allowed to edit posts in this post type.', 'gutenberg' ), array( 'status' => rest_authorization_required_code() ) ); - } - return new WP_Error( 'rest_cannot_view', __( 'Sorry, you cannot view these menu items, unless you have access to permission edit them. ', 'gutenberg' ), array( 'status' => rest_authorization_required_code() ) ); - } - return true; - } - - /** - * Creates a single post. - * - * @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 create_item( $request ) { - if ( ! empty( $request['id'] ) ) { - return new WP_Error( 'rest_post_exists', __( 'Cannot create existing post.', 'gutenberg' ), array( 'status' => 400 ) ); - } - - $prepared_nav_item = $this->prepare_item_for_database( $request ); - - if ( is_wp_error( $prepared_nav_item ) ) { - return $prepared_nav_item; - } - $prepared_nav_item = (array) $prepared_nav_item; - - $nav_menu_item_id = wp_update_nav_menu_item( $prepared_nav_item['menu-id'], $prepared_nav_item['menu-item-db-id'], wp_slash( $prepared_nav_item ) ); - if ( is_wp_error( $nav_menu_item_id ) ) { - if ( 'db_insert_error' === $nav_menu_item_id->get_error_code() ) { - $nav_menu_item_id->add_data( array( 'status' => 500 ) ); - } else { - $nav_menu_item_id->add_data( array( 'status' => 400 ) ); - } - - return $nav_menu_item_id; - } - - $nav_menu_item = $this->get_nav_menu_item( $nav_menu_item_id ); - if ( is_wp_error( $nav_menu_item ) ) { - $nav_menu_item->add_data( array( 'status' => 404 ) ); - - return $nav_menu_item; - } - - /** - * Fires after a single nav menu item is created or updated via the REST API. - * - * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug. - * - * @param object $nav_menu_item Inserted or updated nav item object. - * @param WP_REST_Request $request Request object. - * @param bool $creating True when creating a post, false when updating. - * SA - */ - do_action( "rest_insert_{$this->post_type}", $nav_menu_item, $request, true ); - - $schema = $this->get_item_schema(); - - if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) { - $meta_update = $this->meta->update_value( $request['meta'], $nav_menu_item_id ); - - if ( is_wp_error( $meta_update ) ) { - return $meta_update; - } - } - - $nav_menu_item = $this->get_nav_menu_item( $nav_menu_item_id ); - $fields_update = $this->update_additional_fields_for_object( $nav_menu_item, $request ); - - if ( is_wp_error( $fields_update ) ) { - return $fields_update; - } - - $request->set_param( 'context', 'edit' ); - - /** - * Fires after a single nav menu item is completely created or updated via the REST API. - * - * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug. - * - * @param object $nav_menu_item Inserted or updated nav item object. - * @param WP_REST_Request $request Request object. - * @param bool $creating True when creating a post, false when updating. - */ - do_action( "rest_after_insert_{$this->post_type}", $nav_menu_item, $request, true ); - - $response = $this->prepare_item_for_response( $nav_menu_item, $request ); - $response = rest_ensure_response( $response ); - - $response->set_status( 201 ); - $response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $nav_menu_item_id ) ) ); - - return $response; - } - - /** - * Updates a single nav menu item. - * - * @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 update_item( $request ) { - $valid_check = $this->get_nav_menu_item( $request['id'] ); - if ( is_wp_error( $valid_check ) ) { - return $valid_check; - } - - $prepared_nav_item = $this->prepare_item_for_database( $request ); - - if ( is_wp_error( $prepared_nav_item ) ) { - return $prepared_nav_item; - } - - $prepared_nav_item = (array) $prepared_nav_item; - - $nav_menu_item_id = wp_update_nav_menu_item( $prepared_nav_item['menu-id'], $prepared_nav_item['menu-item-db-id'], wp_slash( $prepared_nav_item ) ); - - if ( is_wp_error( $nav_menu_item_id ) ) { - if ( 'db_update_error' === $nav_menu_item_id->get_error_code() ) { - $nav_menu_item_id->add_data( array( 'status' => 500 ) ); - } else { - $nav_menu_item_id->add_data( array( 'status' => 400 ) ); - } - - return $nav_menu_item_id; - } - - $nav_menu_item = $this->get_nav_menu_item( $nav_menu_item_id ); - if ( is_wp_error( $nav_menu_item ) ) { - $nav_menu_item->add_data( array( 'status' => 404 ) ); - - return $nav_menu_item; - } - - /** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */ - do_action( "rest_insert_{$this->post_type}", $nav_menu_item, $request, false ); - - $schema = $this->get_item_schema(); - - if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) { - $meta_update = $this->meta->update_value( $request['meta'], $nav_menu_item->ID ); - - if ( is_wp_error( $meta_update ) ) { - return $meta_update; - } - } - - $nav_menu_item = $this->get_nav_menu_item( $nav_menu_item_id ); - $fields_update = $this->update_additional_fields_for_object( $nav_menu_item, $request ); - - if ( is_wp_error( $fields_update ) ) { - return $fields_update; - } - - $request->set_param( 'context', 'edit' ); - - /** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php */ - do_action( "rest_after_insert_{$this->post_type}", $nav_menu_item, $request, false ); - - $response = $this->prepare_item_for_response( $nav_menu_item, $request ); - - return rest_ensure_response( $response ); - } - - /** - * Deletes a single menu item. - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_REST_Response|WP_Error True on success, or WP_Error object on failure. - */ - public function delete_item( $request ) { - $menu_item = $this->get_nav_menu_item( $request['id'] ); - if ( is_wp_error( $menu_item ) ) { - return $menu_item; - } - - $force = isset( $request['force'] ) ? (bool) $request['force'] : false; - - // We don't support trashing for menu items. - if ( ! $force ) { - /* translators: %s: force=true */ - return new WP_Error( 'rest_trash_not_supported', sprintf( __( "Menu items do not support trashing. Set '%s' to delete.", 'gutenberg' ), 'force=true' ), array( 'status' => 501 ) ); - } - - $previous = $this->prepare_item_for_response( $menu_item, $request ); - - $result = wp_delete_post( $request['id'], true ); - - if ( ! $result ) { - return new WP_Error( 'rest_cannot_delete', __( 'The post cannot be deleted.', 'gutenberg' ), array( 'status' => 500 ) ); - } - - $response = new WP_REST_Response(); - $response->set_data( - array( - 'deleted' => true, - 'previous' => $previous->get_data(), - ) - ); - - /** - * Fires immediately after a single menu item is deleted or trashed via the REST API. - * - * They dynamic portion of the hook name, `$this->post_type`, refers to the post type slug. - * - * @param Object $menu_item The deleted or trashed menu item. - * @param WP_REST_Response $response The response data. - * @param WP_REST_Request $request The request sent to the API. - */ - do_action( "rest_delete_{$this->post_type}", $menu_item, $response, $request ); - - return $response; - } - - /** - * Prepares a single post for create or update. - * - * @param WP_REST_Request $request Request object. - * - * @return stdClass|WP_Error - */ - protected function prepare_item_for_database( $request ) { - $menu_item_db_id = $request['id']; - $menu_item_obj = $this->get_nav_menu_item( $menu_item_db_id ); - // Need to persist the menu item data. See https://core.trac.wordpress.org/ticket/28138 . - if ( ! is_wp_error( $menu_item_obj ) ) { - // Correct the menu position if this was the first item. See https://core.trac.wordpress.org/ticket/28140 . - $position = ( 0 === $menu_item_obj->menu_order ) ? 1 : $menu_item_obj->menu_order; - - $prepared_nav_item = array( - 'menu-item-db-id' => $menu_item_db_id, - 'menu-item-object-id' => $menu_item_obj->object_id, - 'menu-item-object' => $menu_item_obj->object, - 'menu-item-parent-id' => $menu_item_obj->menu_item_parent, - 'menu-item-position' => $position, - 'menu-item-type' => $menu_item_obj->type, - 'menu-item-title' => $menu_item_obj->title, - 'menu-item-url' => $menu_item_obj->url, - 'menu-item-description' => $menu_item_obj->description, - 'menu-item-content' => $menu_item_obj->menu_item_content, - 'menu-item-attr-title' => $menu_item_obj->attr_title, - 'menu-item-target' => $menu_item_obj->target, - // Stored in the database as a string. - 'menu-item-classes' => implode( ' ', $menu_item_obj->classes ), - 'menu-item-xfn' => $menu_item_obj->xfn, - 'menu-item-status' => $menu_item_obj->post_status, - 'menu-id' => $this->get_menu_id( $menu_item_db_id ), - ); - } else { - $prepared_nav_item = array( - 'menu-id' => 0, - 'menu-item-db-id' => 0, - 'menu-item-object-id' => 0, - 'menu-item-object' => '', - 'menu-item-parent-id' => 0, - 'menu-item-position' => 1, - 'menu-item-type' => 'custom', - 'menu-item-title' => '', - 'menu-item-url' => '', - 'menu-item-description' => '', - 'menu-item-content' => '', - 'menu-item-attr-title' => '', - 'menu-item-target' => '', - 'menu-item-classes' => '', - 'menu-item-xfn' => '', - 'menu-item-status' => 'publish', - ); - } - - $mapping = array( - 'menu-item-db-id' => 'id', - 'menu-item-object-id' => 'object_id', - 'menu-item-object' => 'object', - 'menu-item-parent-id' => 'parent', - 'menu-item-position' => 'menu_order', - 'menu-item-type' => 'type', - 'menu-item-url' => 'url', - 'menu-item-description' => 'description', - 'menu-item-attr-title' => 'attr_title', - 'menu-item-target' => 'target', - 'menu-item-classes' => 'classes', - 'menu-item-xfn' => 'xfn', - 'menu-item-status' => 'status', - ); - - $schema = $this->get_item_schema(); - - foreach ( $mapping as $original => $api_request ) { - if ( ! empty( $schema['properties'][ $api_request ] ) && isset( $request[ $api_request ] ) ) { - $check = rest_validate_value_from_schema( $request[ $api_request ], $schema['properties'][ $api_request ] ); - if ( is_wp_error( $check ) ) { - $check->add_data( array( 'status' => 400 ) ); - return $check; - } - $prepared_nav_item[ $original ] = rest_sanitize_value_from_schema( $request[ $api_request ], $schema['properties'][ $api_request ] ); - } - } - - $taxonomy = get_taxonomy( 'nav_menu' ); - $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name; - // If menus submitted, cast to int. - if ( isset( $request[ $base ] ) && ! empty( $request[ $base ] ) ) { - $prepared_nav_item['menu-id'] = absint( $request[ $base ] ); - } - - // Nav menu title. - if ( ! empty( $schema['properties']['title'] ) && isset( $request['title'] ) ) { - if ( is_string( $request['title'] ) ) { - $prepared_nav_item['menu-item-title'] = $request['title']; - } elseif ( ! empty( $request['title']['raw'] ) ) { - $prepared_nav_item['menu-item-title'] = $request['title']['raw']; - } - } - - // Nav menu content. - if ( ! empty( $schema['properties']['content'] ) && isset( $request['content'] ) ) { - if ( is_string( $request['content'] ) ) { - $prepared_nav_item['menu-item-content'] = $request['content']; - } elseif ( isset( $request['content']['raw'] ) ) { - $prepared_nav_item['menu-item-content'] = $request['content']['raw']; - } - } - - $error = new WP_Error(); - - // Check if object id exists before saving. - if ( ! $prepared_nav_item['menu-item-object'] ) { - // If taxonony, check if term exists. - if ( 'taxonomy' === $prepared_nav_item['menu-item-type'] ) { - $original = get_term( absint( $prepared_nav_item['menu-item-object-id'] ) ); - if ( empty( $original ) || is_wp_error( $original ) ) { - $error->add( 'rest_term_invalid_id', __( 'Invalid term ID.', 'gutenberg' ), array( 'status' => 400 ) ); - } - $prepared_nav_item['menu-item-object'] = get_term_field( 'taxonomy', $original ); - - // If post, check if post object exists. - } elseif ( 'post_type' === $prepared_nav_item['menu-item-type'] ) { - $original = get_post( absint( $prepared_nav_item['menu-item-object-id'] ) ); - if ( empty( $original ) ) { - $error->add( 'rest_post_invalid_id', __( 'Invalid post ID.', 'gutenberg' ), array( 'status' => 400 ) ); - } - $prepared_nav_item['menu-item-object'] = get_post_type( $original ); - } - } - - // If post type archive, check if post type exists. - if ( 'post_type_archive' === $prepared_nav_item['menu-item-type'] ) { - $post_type = ( $prepared_nav_item['menu-item-object'] ) ? $prepared_nav_item['menu-item-object'] : false; - $original = get_post_type_object( $post_type ); - if ( empty( $original ) ) { - $error->add( 'rest_post_invalid_type', __( 'Invalid post type.', 'gutenberg' ), array( 'status' => 400 ) ); - } - } - - // Check if menu item is type custom, then title and url are required. - if ( 'custom' === $prepared_nav_item['menu-item-type'] ) { - if ( '' === $prepared_nav_item['menu-item-title'] ) { - $error->add( 'rest_title_required', __( 'Title required if menu item of type custom.', 'gutenberg' ), array( 'status' => 400 ) ); - } - if ( empty( $prepared_nav_item['menu-item-url'] ) ) { - $error->add( 'rest_url_required', __( 'URL required if menu item of type custom.', 'gutenberg' ), array( 'status' => 400 ) ); - } - } - - // If menu item is type block, then content is required. - if ( 'block' === $prepared_nav_item['menu-item-type'] && empty( $prepared_nav_item['menu-item-content'] ) ) { - $error->add( 'rest_content_required', __( 'Content required if menu item of type block.', 'gutenberg' ), array( 'status' => 400 ) ); - } - - // Valid url. - if ( '' !== $prepared_nav_item['menu-item-url'] ) { - $prepared_nav_item['menu-item-url'] = esc_url_raw( $prepared_nav_item['menu-item-url'] ); - if ( '' === $prepared_nav_item['menu-item-url'] ) { - // Fail sanitization if URL is invalid. - $error->add( 'invalid_url', __( 'Invalid URL.', 'gutenberg' ), array( 'status' => 400 ) ); - } - } - - if ( $error->has_errors() ) { - return $error; - } - - foreach ( array( 'menu-item-object-id', 'menu-item-parent-id' ) as $key ) { - // Note we need to allow negative-integer IDs for previewed objects not inserted yet. - $prepared_nav_item[ $key ] = (int) $prepared_nav_item[ $key ]; - } - - foreach ( array( 'menu-item-type', 'menu-item-object', 'menu-item-target' ) as $key ) { - $prepared_nav_item[ $key ] = sanitize_key( $prepared_nav_item[ $key ] ); - } - - // Valid xfn and classes are an array. - foreach ( array( 'menu-item-xfn', 'menu-item-classes' ) as $key ) { - $value = $prepared_nav_item[ $key ]; - if ( ! is_array( $value ) ) { - $value = wp_parse_list( $value ); - } - $prepared_nav_item[ $key ] = implode( ' ', array_map( 'sanitize_html_class', $value ) ); - } - - // Only draft / publish are valid post status for menu items. - if ( 'publish' !== $prepared_nav_item['menu-item-status'] ) { - $prepared_nav_item['menu-item-status'] = 'draft'; - } - - $prepared_nav_item = (object) $prepared_nav_item; - - /** - * Filters a post before it is inserted via the REST API. - * - * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug. - * - * @param stdClass $prepared_post An object representing a single post prepared - * for inserting or updating the database. - * @param WP_REST_Request $request Request object. - */ - return apply_filters( "rest_pre_insert_{$this->post_type}", $prepared_nav_item, $request ); - } - - /** - * Prepares a single post output for response. - * - * @param object $post Post object. - * @param WP_REST_Request $request Request object. - * - * @return WP_REST_Response Response object. - */ - public function prepare_item_for_response( $post, $request ) { - $fields = $this->get_fields_for_response( $request ); - - // Base fields for every post. - $menu_item = wp_setup_nav_menu_item( $post ); - $data = array(); - if ( rest_is_field_included( 'id', $fields ) ) { - $data['id'] = $menu_item->ID; - } - - if ( rest_is_field_included( 'title', $fields ) ) { - $data['title'] = array(); - } - if ( rest_is_field_included( 'title.raw', $fields ) ) { - $data['title']['raw'] = $menu_item->title; - } - if ( rest_is_field_included( 'title.rendered', $fields ) ) { - add_filter( 'protected_title_format', array( $this, 'protected_title_format' ) ); - - /** This filter is documented in wp-includes/post-template.php */ - $title = apply_filters( 'the_title', $menu_item->title, $menu_item->ID ); - - /** This filter is documented in wp-includes/class-walker-nav-menu.php */ - $title = apply_filters( 'nav_menu_item_title', $title, $menu_item, null, 0 ); - - $data['title']['rendered'] = $title; - - remove_filter( 'protected_title_format', array( $this, 'protected_title_format' ) ); - } - - if ( rest_is_field_included( 'status', $fields ) ) { - $data['status'] = $menu_item->post_status; - } - - if ( rest_is_field_included( 'url', $fields ) ) { - $data['url'] = $menu_item->url; - } - - if ( rest_is_field_included( 'attr_title', $fields ) ) { - // Same as post_excerpt. - $data['attr_title'] = $menu_item->attr_title; - } - - if ( rest_is_field_included( 'description', $fields ) ) { - // Same as post_content. - $data['description'] = $menu_item->description; - } - - if ( rest_is_field_included( 'type', $fields ) ) { - // Using 'item_type' since 'type' already exists. - $data['type'] = $menu_item->type; - } - - if ( rest_is_field_included( 'type_label', $fields ) ) { - // Using 'item_type_label' to match up with 'item_type' - IS READ ONLY! - $data['type_label'] = $menu_item->type_label; - } - - if ( rest_is_field_included( 'object', $fields ) ) { - $data['object'] = $menu_item->object; - } - - if ( rest_is_field_included( 'object_id', $fields ) ) { - // Usually is a string, but lets expose as an integer. - $data['object_id'] = absint( $menu_item->object_id ); - } - - if ( rest_is_field_included( 'content', $fields ) ) { - $data['content'] = array(); - } - if ( rest_is_field_included( 'content.raw', $fields ) ) { - $data['content']['raw'] = $menu_item->content; - } - if ( rest_is_field_included( 'content.rendered', $fields ) ) { - /** This filter is documented in wp-includes/post-template.php */ - $data['content']['rendered'] = apply_filters( 'the_content', $menu_item->content ); - } - if ( rest_is_field_included( 'content.block_version', $fields ) ) { - $data['content']['block_version'] = block_version( $menu_item->content ); - } - - if ( rest_is_field_included( 'parent', $fields ) ) { - // Same as post_parent, expose as integer. - $data['parent'] = (int) $menu_item->menu_item_parent; - } - - if ( rest_is_field_included( 'menu_order', $fields ) ) { - // Same as post_parent, expose as integer. - $data['menu_order'] = (int) $menu_item->menu_order; - } - - if ( rest_is_field_included( 'menu_id', $fields ) ) { - $data['menu_id'] = $this->get_menu_id( $menu_item->ID ); - } - - if ( rest_is_field_included( 'target', $fields ) ) { - $data['target'] = $menu_item->target; - } - - if ( rest_is_field_included( 'classes', $fields ) ) { - $data['classes'] = (array) $menu_item->classes; - } - - if ( rest_is_field_included( 'xfn', $fields ) ) { - $data['xfn'] = array_map( 'sanitize_html_class', explode( ' ', $menu_item->xfn ) ); - } - - if ( rest_is_field_included( 'invalid', $fields ) ) { - $data['invalid'] = (bool) $menu_item->_invalid; - } - - if ( rest_is_field_included( 'meta', $fields ) ) { - $data['meta'] = $this->meta->get_value( $menu_item->ID, $request ); - } - - $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) ); - - foreach ( $taxonomies as $taxonomy ) { - $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name; - - if ( rest_is_field_included( $base, $fields ) ) { - $terms = get_the_terms( $post, $taxonomy->name ); - $term_ids = $terms ? array_values( wp_list_pluck( $terms, 'term_id' ) ) : array(); - if ( 'nav_menu' === $taxonomy->name ) { - $data[ $base ] = $term_ids ? array_shift( $term_ids ) : 0; - } else { - $data[ $base ] = $term_ids; - } - } - } - - $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; - $data = $this->add_additional_fields_to_object( $data, $request ); - $data = $this->filter_response_by_context( $data, $context ); - - // Wrap the data in a response object. - $response = rest_ensure_response( $data ); - - $links = $this->prepare_links( $menu_item ); - $response->add_links( $links ); - - if ( ! empty( $links['self']['href'] ) ) { - $actions = $this->get_available_actions( $menu_item, $request ); - - $self = $links['self']['href']; - - foreach ( $actions as $rel ) { - $response->add_link( $rel, $self ); - } - } - - /** - * Filters the post data for a response. - * - * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug. - * - * @param WP_REST_Response $response The response object. - * @param object $post Post object. - * @param WP_REST_Request $request Request object. - */ - return apply_filters( "rest_prepare_{$this->post_type}", $response, $post, $request ); - } - - /** - * Prepares links for the request. - * - * @param object $menu_item Menu object. - * - * @return array Links for the given post. - */ - protected function prepare_links( $menu_item ) { - $links = parent::prepare_links( $menu_item ); - if ( empty( $menu_item->object_id ) ) { - return $links; - } - - $path = ''; - $type = ''; - $key = $menu_item->type; - if ( 'post_type' === $menu_item->type ) { - $path = rest_get_route_for_post( $menu_item->object_id ); - $type = get_post_type( $menu_item->object_id ); - } elseif ( 'taxonomy' === $menu_item->type ) { - $path = rest_get_route_for_term( $menu_item->object_id ); - $type = get_term_field( 'taxonomy', $menu_item->object_id ); - } - - if ( $path && $type ) { - $links['https://api.w.org/menu-item-object'][] = array( - 'href' => rest_url( $path ), - $key => $type, - 'embeddable' => true, - ); - } - - return $links; - } - - /** - * Retrieve Link Description Objects that should be added to the Schema for the posts collection. - * - * @return array - */ - protected function get_schema_links() { - $links = parent::get_schema_links(); - $href = rest_url( "{$this->namespace}/{$this->rest_base}/{id}" ); - $links[] = array( - 'rel' => 'https://api.w.org/object', - 'title' => __( 'Get linked object.', 'gutenberg' ), - 'href' => $href, - 'targetSchema' => array( - 'type' => 'object', - 'properties' => array( - 'object' => array( - 'type' => 'integer', - ), - ), - ), - ); - - return $links; - } - - /** - * Retrieves the term's schema, conforming to JSON Schema. - * - * @return array Item schema data. - */ - public function get_item_schema() { - $schema = array( - '$schema' => 'http://json-schema.org/draft-04/schema#', - 'title' => $this->post_type, - 'type' => 'object', - ); - - $schema['properties']['title'] = array( - 'description' => __( 'The title for the object.', 'gutenberg' ), - 'type' => 'object', - 'context' => array( 'view', 'edit', 'embed' ), - 'arg_options' => array( - // Note: sanitization implemented in self::prepare_item_for_database(). - 'sanitize_callback' => null, - // Note: validation implemented in self::prepare_item_for_database(). - 'validate_callback' => null, - ), - 'properties' => array( - 'raw' => array( - 'description' => __( 'Title for the object, as it exists in the database.', 'gutenberg' ), - 'type' => 'string', - 'context' => array( 'edit' ), - ), - 'rendered' => array( - 'description' => __( 'HTML title for the object, transformed for display.', 'gutenberg' ), - 'type' => 'string', - 'context' => array( 'view', 'edit', 'embed' ), - 'readonly' => true, - ), - ), - ); - - $schema['properties']['id'] = array( - 'description' => __( 'Unique identifier for the object.', 'gutenberg' ), - 'type' => 'integer', - 'default' => 0, - 'minimum' => 0, - 'context' => array( 'view', 'edit', 'embed' ), - 'readonly' => true, - ); - - $schema['properties']['type_label'] = array( - 'description' => __( 'Name of type.', 'gutenberg' ), - 'type' => 'string', - 'context' => array( 'view', 'edit', 'embed' ), - 'readonly' => true, - ); - - $schema['properties']['type'] = array( - 'description' => __( 'The family of objects originally represented, such as "post_type" or "taxonomy".', 'gutenberg' ), - 'type' => 'string', - 'enum' => array( 'taxonomy', 'post_type', 'post_type_archive', 'custom', 'block' ), - 'context' => array( 'view', 'edit', 'embed' ), - 'default' => 'custom', - ); - - $schema['properties']['status'] = array( - 'description' => __( 'A named status for the object.', 'gutenberg' ), - 'type' => 'string', - 'enum' => array_keys( get_post_stati( array( 'internal' => false ) ) ), - 'default' => 'publish', - 'context' => array( 'view', 'edit', 'embed' ), - ); - - $schema['properties']['parent'] = array( - 'description' => __( 'The ID for the parent of the object.', 'gutenberg' ), - 'type' => 'integer', - 'minimum' => 0, - 'default' => 0, - 'context' => array( 'view', 'edit', 'embed' ), - ); - - $schema['properties']['attr_title'] = array( - 'description' => __( 'Text for the title attribute of the link element for this menu item.', 'gutenberg' ), - 'type' => 'string', - 'context' => array( 'view', 'edit', 'embed' ), - 'arg_options' => array( - 'sanitize_callback' => 'sanitize_text_field', - ), - ); - - $schema['properties']['classes'] = array( - 'description' => __( 'Class names for the link element of this menu item.', 'gutenberg' ), - 'type' => 'array', - 'items' => array( - 'type' => 'string', - ), - 'context' => array( 'view', 'edit', 'embed' ), - 'arg_options' => array( - 'sanitize_callback' => function ( $value ) { - return array_map( 'sanitize_html_class', wp_parse_list( $value ) ); - }, - ), - ); - - $schema['properties']['description'] = array( - 'description' => __( 'The description of this menu item.', 'gutenberg' ), - 'type' => 'string', - 'context' => array( 'view', 'edit', 'embed' ), - 'arg_options' => array( - 'sanitize_callback' => 'sanitize_text_field', - ), - ); - - $schema['properties']['menu_order'] = array( - 'description' => __( 'The DB ID of the nav_menu_item that is this item\'s menu parent, if any, otherwise 0.', 'gutenberg' ), - 'context' => array( 'view', 'edit', 'embed' ), - 'type' => 'integer', - 'minimum' => 1, - 'default' => 1, - ); - - $schema['properties']['object'] = array( - 'description' => __( 'The type of object originally represented, such as "category," "post", or "attachment."', 'gutenberg' ), - 'context' => array( 'view', 'edit', 'embed' ), - 'type' => 'string', - ); - - $schema['properties']['object_id'] = array( - 'description' => __( 'The DB ID of the original object this menu item represents, e . g . ID for posts and term_id for categories.', 'gutenberg' ), - 'context' => array( 'view', 'edit', 'embed' ), - 'type' => 'integer', - 'minimum' => 0, - 'default' => 0, - ); - - $schema['properties']['content'] = array( - 'description' => __( 'HTML content to display for this block menu item.', 'gutenberg' ), - 'context' => array( 'view', 'edit', 'embed' ), - 'type' => 'object', - 'arg_options' => array( - 'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database(). - 'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database(). - ), - 'properties' => array( - 'raw' => array( - 'description' => __( 'HTML content, as it exists in the database.', 'gutenberg' ), - 'type' => 'string', - 'context' => array( 'edit' ), - ), - 'rendered' => array( - 'description' => __( 'HTML content, transformed for display.', 'gutenberg' ), - 'type' => 'string', - 'context' => array( 'view', 'edit' ), - 'readonly' => true, - ), - 'block_version' => array( - 'description' => __( 'Version of the block format used in the HTML content.', 'gutenberg' ), - 'type' => 'integer', - 'context' => array( 'edit' ), - 'readonly' => true, - ), - ), - ); - - $schema['properties']['target'] = array( - 'description' => __( 'The target attribute of the link element for this menu item.', 'gutenberg' ), - 'type' => 'string', - 'context' => array( 'view', 'edit', 'embed' ), - 'enum' => array( - '_blank', - '', - ), - ); - - $schema['properties']['type_label'] = array( - 'description' => __( 'The singular label used to describe this type of menu item.', 'gutenberg' ), - 'context' => array( 'view', 'edit', 'embed' ), - 'type' => 'string', - 'readonly' => true, - ); - - $schema['properties']['url'] = array( - 'description' => __( 'The URL to which this menu item points.', 'gutenberg' ), - 'type' => 'string', - 'format' => 'uri', - 'context' => array( 'view', 'edit', 'embed' ), - ); - - $schema['properties']['xfn'] = array( - 'description' => __( 'The XFN relationship expressed in the link of this menu item.', 'gutenberg' ), - 'type' => 'array', - 'items' => array( - 'type' => 'string', - ), - 'context' => array( 'view', 'edit', 'embed' ), - 'arg_options' => array( - 'sanitize_callback' => function ( $value ) { - return array_map( 'sanitize_html_class', wp_parse_list( $value ) ); - }, - ), - ); - - $schema['properties']['invalid'] = array( - 'description' => __( 'Whether the menu item represents an object that no longer exists.', 'gutenberg' ), - 'context' => array( 'view', 'edit', 'embed' ), - 'type' => 'boolean', - 'readonly' => true, - ); - - $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) ); - - foreach ( $taxonomies as $taxonomy ) { - $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name; - $schema['properties'][ $base ] = array( - /* translators: %s: taxonomy name */ - 'description' => sprintf( __( 'The terms assigned to the object in the %s taxonomy.', 'gutenberg' ), $taxonomy->name ), - 'type' => 'array', - 'items' => array( - 'type' => 'integer', - ), - 'context' => array( 'view', 'edit' ), - ); - - if ( 'nav_menu' === $taxonomy->name ) { - $schema['properties'][ $base ]['type'] = 'integer'; - unset( $schema['properties'][ $base ]['items'] ); - } - } - - $schema['properties']['meta'] = $this->meta->get_field_schema(); - - $schema_links = $this->get_schema_links(); - - if ( $schema_links ) { - $schema['links'] = $schema_links; - } - - return $this->add_additional_fields_schema( $schema ); - } - - /** - * Retrieves the query params for the posts collection. - * - * @return array Collection parameters. - */ - public function get_collection_params() { - $query_params = parent::get_collection_params(); - - $query_params['menu_order'] = array( - 'description' => __( 'Limit result set to posts with a specific menu_order value.', 'gutenberg' ), - 'type' => 'integer', - ); - - $query_params['order'] = array( - 'description' => __( 'Order sort attribute ascending or descending.', 'gutenberg' ), - 'type' => 'string', - 'default' => 'asc', - 'enum' => array( 'asc', 'desc' ), - ); - - $query_params['orderby'] = array( - 'description' => __( 'Sort collection by object attribute.', 'gutenberg' ), - 'type' => 'string', - 'default' => 'menu_order', - 'enum' => array( - 'author', - 'date', - 'id', - 'include', - 'modified', - 'parent', - 'relevance', - 'slug', - 'include_slugs', - 'title', - 'menu_order', - ), - ); - // Change default to 100 items. - $query_params['per_page']['default'] = 100; - - return $query_params; - } - - /** - * Determines the allowed query_vars for a get_items() response and prepares - * them for WP_Query. - * - * @param array $prepared_args Optional. Prepared WP_Query arguments. Default empty array. - * @param WP_REST_Request $request Optional. Full details about the request. - * - * @return array Items query arguments. - */ - protected function prepare_items_query( $prepared_args = array(), $request = null ) { - $query_args = parent::prepare_items_query( $prepared_args, $request ); - - // Map to proper WP_Query orderby param. - if ( isset( $query_args['orderby'] ) && isset( $request['orderby'] ) ) { - $orderby_mappings = array( - 'id' => 'ID', - 'include' => 'post__in', - 'slug' => 'post_name', - 'include_slugs' => 'post_name__in', - 'menu_order' => 'menu_order', - ); - - if ( isset( $orderby_mappings[ $request['orderby'] ] ) ) { - $query_args['orderby'] = $orderby_mappings[ $request['orderby'] ]; - } - } - - return $query_args; - } - - /** - * Checks whether current user can assign all terms sent with the current request. - * - * @param WP_REST_Request $request The request object with post and terms data. - * - * @return bool Whether the current user can assign the provided terms. - */ - protected function check_assign_terms_permission( $request ) { - $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) ); - foreach ( $taxonomies as $taxonomy ) { - $base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name; - - if ( ! isset( $request[ $base ] ) ) { - continue; - } - - foreach ( (array) $request[ $base ] as $term_id ) { - if ( ! $term_id ) { - continue; - } - - // Invalid terms will be rejected later. - if ( ! get_term( $term_id, $taxonomy->name ) ) { - continue; - } - - if ( ! current_user_can( 'assign_term', (int) $term_id ) ) { - return false; - } - } - } - - return true; - } - - /** - * Get menu id of current menu item. - * - * @param int $menu_item_id Menu item id. - * - * @return int - */ - protected function get_menu_id( $menu_item_id ) { - $menu_ids = wp_get_post_terms( $menu_item_id, 'nav_menu', array( 'fields' => 'ids' ) ); - $menu_id = 0; - if ( $menu_ids && ! is_wp_error( $menu_ids ) ) { - $menu_id = array_shift( $menu_ids ); - } - - return $menu_id; - } -} diff --git a/lib/compat/wordpress-5.9/class-wp-rest-menu-locations-controller.php b/lib/compat/wordpress-5.9/class-wp-rest-menu-locations-controller.php deleted file mode 100644 index 8fc9f699aeec9..0000000000000 --- a/lib/compat/wordpress-5.9/class-wp-rest-menu-locations-controller.php +++ /dev/null @@ -1,276 +0,0 @@ -namespace = 'wp/v2'; - $this->rest_base = 'menu-locations'; - } - - /** - * Registers the routes for the objects of the controller. - * - * @see register_rest_route() - */ - public function register_routes() { - register_rest_route( - $this->namespace, - '/' . $this->rest_base, - array( - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_items' ), - 'permission_callback' => array( $this, 'get_items_permissions_check' ), - 'args' => $this->get_collection_params(), - ), - 'schema' => array( $this, 'get_public_item_schema' ), - ) - ); - - register_rest_route( - $this->namespace, - '/' . $this->rest_base . '/(?P[\w-]+)', - array( - 'args' => array( - 'location' => array( - 'description' => __( 'An alphanumeric identifier for the menu location.', 'gutenberg' ), - 'type' => 'string', - ), - ), - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_item' ), - 'permission_callback' => array( $this, 'get_item_permissions_check' ), - 'args' => array( - 'context' => $this->get_context_param( array( 'default' => 'view' ) ), - ), - ), - 'schema' => array( $this, 'get_public_item_schema' ), - ) - ); - } - - /** - * Checks whether a given request has permission to read menu locations. - * - * @param WP_REST_Request $request Full details about the request. - * - * @return WP_Error|bool True if the request has read access, WP_Error object otherwise. - */ - public function get_items_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable - if ( ! current_user_can( 'edit_theme_options' ) ) { - return new WP_Error( 'rest_cannot_view', __( 'Sorry, you are not allowed to view menu locations.', 'gutenberg' ), array( 'status' => rest_authorization_required_code() ) ); - } - - return true; - } - - /** - * Retrieves all menu locations, depending on user context. - * - * @param WP_REST_Request $request Full details about the request. - * - * @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure. - */ - public function get_items( $request ) { - $data = array(); - - foreach ( get_registered_nav_menus() as $name => $description ) { - $location = new stdClass(); - $location->name = $name; - $location->description = $description; - - $location = $this->prepare_item_for_response( $location, $request ); - $data[ $name ] = $this->prepare_response_for_collection( $location ); - } - - return rest_ensure_response( $data ); - } - - /** - * Checks if a given request has access to read a menu location. - * - * @param WP_REST_Request $request Full details about the request. - * - * @return WP_Error|bool True if the request has read access for the item, WP_Error object otherwise. - */ - public function get_item_permissions_check( $request ) { - if ( ! current_user_can( 'edit_theme_options' ) ) { - return new WP_Error( 'rest_cannot_view', __( 'Sorry, you are not allowed to view menu locations.', 'gutenberg' ), array( 'status' => rest_authorization_required_code() ) ); - } - if ( ! array_key_exists( $request['location'], get_registered_nav_menus() ) ) { - return new WP_Error( 'rest_menu_location_invalid', __( 'Invalid menu location.', 'gutenberg' ), array( 'status' => 404 ) ); - } - - return true; - } - - /** - * Retrieves a specific menu location. - * - * @param WP_REST_Request $request Full details about the request. - * - * @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure. - */ - public function get_item( $request ) { - $registered_menus = get_registered_nav_menus(); - if ( ! array_key_exists( $request['location'], $registered_menus ) ) { - return new WP_Error( 'rest_menu_location_invalid', __( 'Invalid menu location.', 'gutenberg' ), array( 'status' => 404 ) ); - } - - $location = new stdClass(); - $location->name = $request['location']; - $location->description = $registered_menus[ $location->name ]; - - $data = $this->prepare_item_for_response( $location, $request ); - - return rest_ensure_response( $data ); - } - - /** - * Prepares a menu location object for serialization. - * - * @param stdClass $location Post status data. - * @param WP_REST_Request $request Full details about the request. - * - * @return WP_REST_Response Post status data. - */ - public function prepare_item_for_response( $location, $request ) { - $locations = get_nav_menu_locations(); - $menu = ( isset( $locations[ $location->name ] ) ) ? $locations[ $location->name ] : 0; - - $fields = $this->get_fields_for_response( $request ); - $data = array(); - - if ( rest_is_field_included( 'name', $fields ) ) { - $data['name'] = $location->name; - } - - if ( rest_is_field_included( 'description', $fields ) ) { - $data['description'] = $location->description; - } - - if ( rest_is_field_included( 'menu', $fields ) ) { - $data['menu'] = (int) $menu; - } - - $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; - $data = $this->add_additional_fields_to_object( $data, $request ); - $data = $this->filter_response_by_context( $data, $context ); - - $response = rest_ensure_response( $data ); - - $response->add_links( $this->prepare_links( $location ) ); - - /** - * Filters a menu location returned from the REST API. - * - * Allows modification of the menu location data right before it is - * returned. - * - * @param WP_REST_Response $response The response object. - * @param object $location The original status object. - * @param WP_REST_Request $request Request used to generate the response. - */ - return apply_filters( 'rest_prepare_menu_location', $response, $location, $request ); - } - - /** - * Retrieves the menu location's schema, conforming to JSON Schema. - * - * @return array Item schema data. - */ - public function get_item_schema() { - $schema = array( - '$schema' => 'http://json-schema.org/draft-04/schema#', - 'title' => 'menu-location', - 'type' => 'object', - 'properties' => array( - 'name' => array( - 'description' => __( 'The name of the menu location.', 'gutenberg' ), - 'type' => 'string', - 'context' => array( 'embed', 'view', 'edit' ), - 'readonly' => true, - ), - 'description' => array( - 'description' => __( 'The description of the menu location.', 'gutenberg' ), - 'type' => 'string', - 'context' => array( 'embed', 'view', 'edit' ), - 'readonly' => true, - ), - 'menu' => array( - 'description' => __( 'The ID of the assigned menu.', 'gutenberg' ), - 'type' => 'integer', - 'context' => array( 'embed', 'view', 'edit' ), - 'readonly' => true, - ), - ), - ); - - return $this->add_additional_fields_schema( $schema ); - } - - /** - * Retrieves the query params for collections. - * - * @return array Collection parameters. - */ - public function get_collection_params() { - return array( - 'context' => $this->get_context_param( array( 'default' => 'view' ) ), - ); - } - - /** - * Prepares links for the request. - * - * @param stdClass $location Menu location. - * - * @return array Links for the given menu location. - */ - protected function prepare_links( $location ) { - $base = sprintf( '%s/%s', $this->namespace, $this->rest_base ); - - // Entity meta. - $links = array( - 'self' => array( - 'href' => rest_url( trailingslashit( $base ) . $location->name ), - ), - 'collection' => array( - 'href' => rest_url( $base ), - ), - ); - - $locations = get_nav_menu_locations(); - $menu = ( isset( $locations[ $location->name ] ) ) ? $locations[ $location->name ] : 0; - if ( $menu ) { - $taxonomy_object = get_taxonomy( 'nav_menu' ); - if ( $taxonomy_object->show_in_rest ) { - $rest_base = ! empty( $taxonomy_object->rest_base ) ? $taxonomy_object->rest_base : $taxonomy_object->name; - $namespace = ! empty( $taxonomy_object->rest_namespace ) ? $taxonomy_object->rest_namespace : '__experimental'; - $url = rest_url( sprintf( '%s/%s/%d', $namespace, $rest_base, $menu ) ); - - $links['https://api.w.org/menu'][] = array( - 'href' => $url, - 'embeddable' => true, - ); - } - } - - return $links; - } -} diff --git a/lib/compat/wordpress-5.9/class-wp-rest-menus-controller.php b/lib/compat/wordpress-5.9/class-wp-rest-menus-controller.php deleted file mode 100644 index 3e839c2c958af..0000000000000 --- a/lib/compat/wordpress-5.9/class-wp-rest-menus-controller.php +++ /dev/null @@ -1,693 +0,0 @@ - true ); - - /** - * Overrides the route registration to support "allow_batch". - * - * @since 11.5.0 - * - * @see register_rest_route() - */ - public function register_routes() { - register_rest_route( - $this->namespace, - '/' . $this->rest_base, - array( - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_items' ), - 'permission_callback' => array( $this, 'get_items_permissions_check' ), - 'args' => $this->get_collection_params(), - ), - array( - 'methods' => WP_REST_Server::CREATABLE, - 'callback' => array( $this, 'create_item' ), - 'permission_callback' => array( $this, 'create_item_permissions_check' ), - 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), - ), - 'allow_batch' => $this->allow_batch, - 'schema' => array( $this, 'get_public_item_schema' ), - ) - ); - - register_rest_route( - $this->namespace, - '/' . $this->rest_base . '/(?P[\d]+)', - array( - 'args' => array( - 'id' => array( - 'description' => __( 'Unique identifier for the term.', 'default' ), - 'type' => 'integer', - ), - ), - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_item' ), - 'permission_callback' => array( $this, 'get_item_permissions_check' ), - 'args' => array( - 'context' => $this->get_context_param( array( 'default' => 'view' ) ), - ), - ), - array( - 'methods' => WP_REST_Server::EDITABLE, - 'callback' => array( $this, 'update_item' ), - 'permission_callback' => array( $this, 'update_item_permissions_check' ), - 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), - ), - array( - 'methods' => WP_REST_Server::DELETABLE, - 'callback' => array( $this, 'delete_item' ), - 'permission_callback' => array( $this, 'delete_item_permissions_check' ), - 'args' => array( - 'force' => array( - 'type' => 'boolean', - 'default' => false, - 'description' => __( 'Required to be true, as terms do not support trashing.', 'default' ), - ), - ), - ), - 'allow_batch' => $this->allow_batch, - 'schema' => array( $this, 'get_public_item_schema' ), - ) - ); - } - - - /** - * Checks if a request has access to read terms in the specified taxonomy. - * - * @param WP_REST_Request $request Full details about the request. - * @return bool|WP_Error True if the request has read access, otherwise false or WP_Error object. - */ - public function get_items_permissions_check( $request ) { - $tax_obj = get_taxonomy( $this->taxonomy ); - if ( ! $tax_obj || ! $this->check_is_taxonomy_allowed( $this->taxonomy ) ) { - return false; - } - if ( ! current_user_can( $tax_obj->cap->edit_terms ) ) { - if ( 'edit' === $request['context'] ) { - return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you are not allowed to edit terms in this taxonomy.', 'gutenberg' ), array( 'status' => rest_authorization_required_code() ) ); - } - return new WP_Error( 'rest_cannot_view', __( 'Sorry, you cannot view these menus, unless you have access to permission edit them. ', 'gutenberg' ), array( 'status' => rest_authorization_required_code() ) ); - } - return true; - } - - /** - * Checks if a request has access to read or edit the specified menu. - * - * @param WP_REST_Request $request Full details about the request. - * @return bool|WP_Error True if the request has read access for the item, otherwise false or WP_Error object. - */ - public function get_item_permissions_check( $request ) { - $term = $this->get_term( $request['id'] ); - if ( is_wp_error( $term ) ) { - return $term; - } - if ( ! current_user_can( 'edit_term', $term->term_id ) ) { - if ( 'edit' === $request['context'] ) { - return new WP_Error( 'rest_forbidden_context', __( 'Sorry, you are not allowed to edit this term.', 'gutenberg' ), array( 'status' => rest_authorization_required_code() ) ); - } - return new WP_Error( 'rest_cannot_view', __( 'Sorry, you cannot view this menu, unless you have access to permission edit it. ', 'gutenberg' ), array( 'status' => rest_authorization_required_code() ) ); - } - return true; - } - - /** - * Get the term, if the ID is valid. - * - * @param int $id Supplied ID. - * - * @return WP_Term|WP_Error Term object if ID is valid, WP_Error otherwise. - */ - protected function get_term( $id ) { - $term = parent::get_term( $id ); - - if ( is_wp_error( $term ) ) { - return $term; - } - - $nav_term = wp_get_nav_menu_object( $term ); - $nav_term->auto_add = $this->get_menu_auto_add( $nav_term->term_id ); - - return $nav_term; - } - - /** - * Checks if a request has access to create a term. - * Also check if request can assign menu locations. - * - * @param WP_REST_Request $request Full details about the request. - * - * @return bool|WP_Error True if the request has access to create items, false or WP_Error object otherwise. - */ - public function create_item_permissions_check( $request ) { - $check = $this->check_assign_locations_permission( $request ); - if ( is_wp_error( $check ) ) { - return $check; - } - $check = $this->check_set_auto_add_permission( $request ); - if ( is_wp_error( $check ) ) { - return $check; - } - - return parent::create_item_permissions_check( $request ); - } - - /** - * Checks if a request has access to update the specified term. - * - * @param WP_REST_Request $request Full details about the request. - * - * @return bool|WP_Error True if the request has access to update the item, false or WP_Error object otherwise. - */ - public function update_item_permissions_check( $request ) { - $check = $this->check_assign_locations_permission( $request ); - if ( is_wp_error( $check ) ) { - return $check; - } - $check = $this->check_set_auto_add_permission( $request ); - if ( is_wp_error( $check ) ) { - return $check; - } - - return parent::update_item_permissions_check( $request ); - } - - /** - * Checks whether current user can assign all locations sent with the current request. - * - * @param WP_REST_Request $request The request object with post and locations data. - * - * @return bool|WP_Error Whether the current user can assign the provided terms. - */ - protected function check_assign_locations_permission( $request ) { - if ( ! isset( $request['locations'] ) ) { - return true; - } - - if ( ! current_user_can( 'edit_theme_options' ) ) { - return new WP_Error( 'rest_cannot_assign_location', __( 'Sorry, you are not allowed to assign the provided locations.', 'gutenberg' ), array( 'status' => rest_authorization_required_code() ) ); - } - - foreach ( $request['locations'] as $location ) { - if ( ! array_key_exists( $location, get_registered_nav_menus() ) ) { - return new WP_Error( - 'rest_menu_location_invalid', - __( 'Invalid menu location.', 'gutenberg' ), - array( - 'status' => 400, - 'location' => $location, - ) - ); - } - } - - return true; - } - - /** - * Checks whether current user can set auto add pages. - * - * @param WP_REST_Request $request The request object with post and locations data. - * - * @return true|WP_Error Whether the current user can assign the provided terms. - */ - protected function check_set_auto_add_permission( $request ) { - if ( ! isset( $request['auto_add'] ) ) { - return true; - } - - if ( ! current_user_can( 'edit_theme_options' ) ) { - return new WP_Error( 'rest_cannot_set_auto_add', __( 'Sorry, you are not allowed to set auto add pages.', 'gutenberg' ), array( 'status' => rest_authorization_required_code() ) ); - } - - return true; - } - - /** - * Prepares a single term output for response. - * - * @param obj $term Term object. - * @param WP_REST_Request $request Request object. - * - * @return WP_REST_Response $response Response object. - */ - public function prepare_item_for_response( $term, $request ) { - $nav_menu = wp_get_nav_menu_object( $term ); - $response = parent::prepare_item_for_response( $nav_menu, $request ); - - $fields = $this->get_fields_for_response( $request ); - $data = $response->get_data(); - - if ( rest_is_field_included( 'locations', $fields ) ) { - $data['locations'] = $this->get_menu_locations( $nav_menu->term_id ); - } - - if ( rest_is_field_included( 'auto_add', $fields ) ) { - $auto_add = $this->get_menu_auto_add( $nav_menu->term_id ); - $data['auto_add'] = $auto_add; - } - - $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; - $data = $this->add_additional_fields_to_object( $data, $request ); - $data = $this->filter_response_by_context( $data, $context ); - - $response = rest_ensure_response( $data ); - $response->add_links( $this->prepare_links( $term ) ); - - /** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php */ - return apply_filters( "rest_prepare_{$this->taxonomy}", $response, $term, $request ); - } - - /** - * Prepares links for the request. - * - * @param object $term Term object. - * - * @return array Links for the given term. - */ - protected function prepare_links( $term ) { - $links = parent::prepare_links( $term ); - - $locations = $this->get_menu_locations( $term->term_id ); - $rest_base = 'menu-locations'; - foreach ( $locations as $location ) { - $url = rest_url( sprintf( 'wp/v2/%s/%s', $rest_base, $location ) ); - $links['https://api.w.org/menu-location'][] = array( - 'href' => $url, - 'embeddable' => true, - ); - } - - return $links; - } - - /** - * Prepares a single term for create or update. - * - * @param WP_REST_Request $request Request object. - * - * @return array $prepared_term Term object. - */ - public function prepare_item_for_database( $request ) { - $prepared_term = parent::prepare_item_for_database( $request ); - - $prepared_term = (array) $prepared_term; - $schema = $this->get_item_schema(); - if ( isset( $request['name'] ) && ! empty( $schema['properties']['name'] ) ) { - $prepared_term['menu-name'] = $request['name']; - } - - return $prepared_term; - } - - /** - * Creates a single term in a taxonomy. - * - * @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 create_item( $request ) { - if ( isset( $request['parent'] ) ) { - if ( ! is_taxonomy_hierarchical( $this->taxonomy ) ) { - return new WP_Error( 'rest_taxonomy_not_hierarchical', __( 'Cannot set parent term, taxonomy is not hierarchical.', 'gutenberg' ), array( 'status' => 400 ) ); - } - - $parent = wp_get_nav_menu_object( (int) $request['parent'] ); - - if ( ! $parent ) { - return new WP_Error( 'rest_term_invalid', __( 'Parent term does not exist.', 'gutenberg' ), array( 'status' => 400 ) ); - } - } - - $prepared_term = $this->prepare_item_for_database( $request ); - - $term = wp_update_nav_menu_object( 0, wp_slash( (array) $prepared_term ) ); - - if ( is_wp_error( $term ) ) { - /* - * If we're going to inform the client that the term already exists, - * give them the identifier for future use. - */ - - if ( in_array( 'menu_exists', $term->get_error_codes(), true ) ) { - $existing_term = get_term_by( 'name', $prepared_term['menu-name'], $this->taxonomy ); - $term->add_data( $existing_term->term_id, 'menu_exists' ); - $term->add_data( - array( - 'status' => 400, - 'term_id' => $existing_term->term_id, - ) - ); - } else { - $term->add_data( array( 'status' => 400 ) ); - } - - return $term; - } - - $term = $this->get_term( $term ); - - /** - * Fires after a single term is created or updated via the REST API. - * - * The dynamic portion of the hook name, `$this->taxonomy`, refers to the taxonomy slug. - * - * @param WP_Term $term Inserted or updated term object. - * @param WP_REST_Request $request Request object. - * @param bool $creating True when creating a term, false when updating. - */ - do_action( "rest_insert_{$this->taxonomy}", $term, $request, true ); - - $schema = $this->get_item_schema(); - if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) { - $meta_update = $this->meta->update_value( $request['meta'], $term->term_id ); - - if ( is_wp_error( $meta_update ) ) { - return $meta_update; - } - } - - $locations_update = $this->handle_locations( $term->term_id, $request ); - - if ( is_wp_error( $locations_update ) ) { - return $locations_update; - } - - $this->handle_auto_add( $term->term_id, $request ); - - $fields_update = $this->update_additional_fields_for_object( $term, $request ); - - if ( is_wp_error( $fields_update ) ) { - return $fields_update; - } - - $request->set_param( 'context', 'view' ); - - /** - * Fires after a single term is completely created or updated via the REST API. - * - * The dynamic portion of the hook name, `$this->taxonomy`, refers to the taxonomy slug. - * - * @param WP_Term $term Inserted or updated term object. - * @param WP_REST_Request $request Request object. - * @param bool $creating True when creating a term, false when updating. - * - * @since 5.0.0 - */ - do_action( "rest_after_insert_{$this->taxonomy}", $term, $request, true ); - - $response = $this->prepare_item_for_response( $term, $request ); - $response = rest_ensure_response( $response ); - - $response->set_status( 201 ); - $response->header( 'Location', rest_url( $this->namespace . '/' . $this->rest_base . '/' . $term->term_id ) ); - - return $response; - } - - /** - * Updates a single term from a taxonomy. - * - * @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 update_item( $request ) { - $term = $this->get_term( $request['id'] ); - if ( is_wp_error( $term ) ) { - return $term; - } - - if ( isset( $request['parent'] ) ) { - if ( ! is_taxonomy_hierarchical( $this->taxonomy ) ) { - return new WP_Error( 'rest_taxonomy_not_hierarchical', __( 'Cannot set parent term, taxonomy is not hierarchical.', 'gutenberg' ), array( 'status' => 400 ) ); - } - - $parent = get_term( (int) $request['parent'], $this->taxonomy ); - - if ( ! $parent ) { - return new WP_Error( 'rest_term_invalid', __( 'Parent term does not exist.', 'gutenberg' ), array( 'status' => 400 ) ); - } - } - - $prepared_term = $this->prepare_item_for_database( $request ); - - // Only update the term if we haz something to update. - if ( ! empty( $prepared_term ) ) { - $update = wp_update_nav_menu_object( $term->term_id, wp_slash( (array) $prepared_term ) ); - - if ( is_wp_error( $update ) ) { - return $update; - } - } - - $term = get_term( $term->term_id, $this->taxonomy ); - - /** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php */ - do_action( "rest_insert_{$this->taxonomy}", $term, $request, false ); - - $schema = $this->get_item_schema(); - if ( ! empty( $schema['properties']['meta'] ) && isset( $request['meta'] ) ) { - $meta_update = $this->meta->update_value( $request['meta'], $term->term_id ); - - if ( is_wp_error( $meta_update ) ) { - return $meta_update; - } - } - - $locations_update = $this->handle_locations( $term->term_id, $request ); - - if ( is_wp_error( $locations_update ) ) { - return $locations_update; - } - - $this->handle_auto_add( $term->term_id, $request ); - - $fields_update = $this->update_additional_fields_for_object( $term, $request ); - - if ( is_wp_error( $fields_update ) ) { - return $fields_update; - } - - $request->set_param( 'context', 'view' ); - - /** This action is documented in wp-includes/rest-api/endpoints/class-wp-rest-terms-controller.php */ - do_action( "rest_after_insert_{$this->taxonomy}", $term, $request, false ); - - $response = $this->prepare_item_for_response( $term, $request ); - - return rest_ensure_response( $response ); - } - - /** - * Deletes a single term from a taxonomy. - * - * @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 delete_item( $request ) { - $term = $this->get_term( $request['id'] ); - if ( is_wp_error( $term ) ) { - return $term; - } - - $force = isset( $request['force'] ) ? (bool) $request['force'] : false; - - // We don't support trashing for terms. - if ( ! $force ) { - /* translators: %s: force=true */ - return new WP_Error( 'rest_trash_not_supported', sprintf( __( "Terms do not support trashing. Set '%s' to delete.", 'gutenberg' ), 'force=true' ), array( 'status' => 501 ) ); - } - - $request->set_param( 'context', 'view' ); - - $previous = $this->prepare_item_for_response( $term, $request ); - - $retval = wp_delete_nav_menu( $term ); - - if ( ! $retval ) { - return new WP_Error( 'rest_cannot_delete', __( 'The term cannot be deleted.', 'gutenberg' ), array( 'status' => 500 ) ); - } - - $response = new WP_REST_Response(); - $response->set_data( - array( - 'deleted' => true, - 'previous' => $previous->get_data(), - ) - ); - - /** - * Fires after a single term is deleted via the REST API. - * - * The dynamic portion of the hook name, `$this->taxonomy`, refers to the taxonomy slug. - * - * @param WP_Term $term The deleted term. - * @param WP_REST_Response $response The response data. - * @param WP_REST_Request $request The request sent to the API. - */ - do_action( "rest_delete_{$this->taxonomy}", $term, $response, $request ); - - return $response; - } - - /** - * Returns the value of a menu's auto_add - * - * @param int $menu_id The menu id to update the location form. - * - * @return bool The value of auto_add. - */ - function get_menu_auto_add( $menu_id ) { - $nav_menu_option = (array) get_option( 'nav_menu_options', array( 'auto_add' => array() ) ); - $check = in_array( $menu_id, $nav_menu_option['auto_add'], true ); - - return $check; - } - - /** - * Updates the menu's auto add from a REST request. - * - * @param int $menu_id The menu id to update the location form. - * @param WP_REST_Request $request The request object with menu and locations data. - * - * @return bool True if the auto update was successfully updated. - */ - function handle_auto_add( $menu_id, $request ) { - if ( ! isset( $request['auto_add'] ) ) { - return true; - } - - $nav_menu_option = (array) get_option( 'nav_menu_options', array( 'auto_add' => array() ) ); - - if ( ! isset( $nav_menu_option['auto_add'] ) ) { - $nav_menu_option['auto_add'] = array(); - } - - $auto_add = $request['auto_add']; - - $i = array_search( $menu_id, $nav_menu_option['auto_add'], true ); - - if ( $auto_add && false === $i ) { - $nav_menu_option['auto_add'][] = $menu_id; - } elseif ( ! $auto_add && false !== $i ) { - array_splice( $nav_menu_option['auto_add'], $i, 1 ); - } - - $update = update_option( 'nav_menu_options', $nav_menu_option ); - - /** This action is documented in wp-includes/nav-menu.php */ - do_action( 'wp_update_nav_menu', $menu_id ); - - return $update; - } - - /** - * Returns names of the locations assigned to the menu. - * - * @since 5.8.0 - * - * @param int $menu_id The menu id. - * - * @return string[] $menu_locations The locations assigned to the menu. - */ - protected function get_menu_locations( $menu_id ) { - $locations = get_nav_menu_locations(); - $menu_locations = array(); - - foreach ( $locations as $location => $assigned_menu_id ) { - if ( $menu_id === $assigned_menu_id ) { - $menu_locations[] = $location; - } - } - - return $menu_locations; - } - - /** - * Updates the menu's locations from a REST request. - * - * @param int $menu_id The menu id to update the location form. - * @param WP_REST_Request $request The request object with menu and locations data. - * - * @return true|WP_Error WP_Error on an error assigning any of the locations, otherwise null. - */ - protected function handle_locations( $menu_id, $request ) { - if ( ! isset( $request['locations'] ) ) { - return true; - } - - $menu_locations = get_registered_nav_menus(); - $menu_locations = array_keys( $menu_locations ); - $new_locations = array(); - foreach ( $request['locations'] as $location ) { - if ( ! in_array( $location, $menu_locations, true ) ) { - return new WP_Error( 'invalid_menu_location', __( 'Menu location does not exist.', 'gutenberg' ), array( 'status' => 400 ) ); - } - $new_locations[ $location ] = $menu_id; - } - $assigned_menu = get_nav_menu_locations(); - foreach ( $assigned_menu as $location => $term_id ) { - if ( $term_id === $menu_id ) { - unset( $assigned_menu[ $location ] ); - } - } - $new_assignments = array_merge( $assigned_menu, $new_locations ); - set_theme_mod( 'nav_menu_locations', $new_assignments ); - - return true; - } - - /** - * Retrieves the term's schema, conforming to JSON Schema. - * - * @return array Item schema data. - */ - public function get_item_schema() { - $schema = parent::get_item_schema(); - unset( $schema['properties']['count'] ); - unset( $schema['properties']['link'] ); - unset( $schema['properties']['taxonomy'] ); - - $schema['properties']['locations'] = array( - 'description' => __( 'The locations assigned to the menu.', 'gutenberg' ), - 'type' => 'array', - 'items' => array( - 'type' => 'string', - ), - 'context' => array( 'view', 'edit' ), - ); - - $schema['properties']['auto_add'] = array( - 'description' => __( 'Whether to automatically add top level pages to this menu.', 'gutenberg' ), - 'context' => array( 'view', 'edit' ), - 'type' => 'boolean', - ); - - return $schema; - } -} diff --git a/lib/compat/wordpress-5.9/class-wp-rest-url-details-controller.php b/lib/compat/wordpress-5.9/class-wp-rest-url-details-controller.php deleted file mode 100644 index b9ff5982b4ecd..0000000000000 --- a/lib/compat/wordpress-5.9/class-wp-rest-url-details-controller.php +++ /dev/null @@ -1,591 +0,0 @@ -namespace = 'wp-block-editor/v1'; - $this->rest_base = 'url-details'; - } - - /** - * Registers the necessary REST API routes. - */ - public function register_routes() { - register_rest_route( - $this->namespace, - '/' . $this->rest_base, - array( - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'parse_url_details' ), - 'args' => array( - 'url' => array( - 'required' => true, - 'description' => __( 'The URL to process.', 'gutenberg' ), - 'validate_callback' => 'wp_http_validate_url', - 'sanitize_callback' => 'esc_url_raw', - 'type' => 'string', - 'format' => 'uri', - ), - ), - 'permission_callback' => array( $this, 'permissions_check' ), - 'schema' => array( $this, 'get_public_item_schema' ), - ), - ) - ); - } - - /** - * Get the schema for the endpoint. - * - * @return array the schema. - */ - public function get_item_schema() { - if ( $this->schema ) { - return $this->add_additional_fields_schema( $this->schema ); - } - - $schema = array( - '$schema' => 'http://json-schema.org/draft-04/schema#', - 'title' => 'url-details', - 'type' => 'object', - 'properties' => array( - 'title' => array( - 'description' => __( 'The contents of the element from the URL.', 'gutenberg' ), - 'type' => 'string', - 'context' => array( 'view', 'edit', 'embed' ), - 'readonly' => true, - ), - 'icon' => array( - 'description' => __( 'The favicon image link of the <link rel="icon"> element from the URL.', 'gutenberg' ), - 'type' => 'string', - 'format' => 'uri', - 'context' => array( 'view', 'edit', 'embed' ), - 'readonly' => true, - ), - 'description' => array( - 'description' => __( 'The content of the <meta name="description"> element from the URL.', 'gutenberg' ), - 'type' => 'string', - 'context' => array( 'view', 'edit', 'embed' ), - 'readonly' => true, - ), - 'image' => array( - 'description' => __( 'The OG image link of the <meta property="og:image"> or <meta property="og:image:url"> element from the URL.', 'gutenberg' ), - 'type' => 'string', - 'format' => 'uri', - 'context' => array( 'view', 'edit', 'embed' ), - 'readonly' => true, - ), - ), - ); - - $this->schema = $schema; - - return $this->add_additional_fields_schema( $this->schema ); - } - - /** - * Retrieves the contents of the <title> tag from the HTML - * response. - * - * @param WP_REST_REQUEST $request Full details about the request. - * @return WP_REST_Response|WP_Error The parsed details as a response object, or an error. - */ - public function parse_url_details( $request ) { - $url = untrailingslashit( $request['url'] ); - - if ( empty( $url ) ) { - return new WP_Error( 'rest_invalid_url', __( 'Invalid URL', 'gutenberg' ), array( 'status' => 404 ) ); - } - - // Transient per URL. - $cache_key = $this->build_cache_key_for_url( $url ); - - // Attempt to retrieve cached response. - $cached_response = $this->get_cache( $cache_key ); - - if ( ! empty( $cached_response ) ) { - $remote_url_response = $cached_response; - } else { - $remote_url_response = $this->get_remote_url( $url ); - - // Exit if we don't have a valid body or it's empty. - if ( is_wp_error( $remote_url_response ) || empty( $remote_url_response ) ) { - return $remote_url_response; - } - - // Cache the valid response. - $this->set_cache( $cache_key, $remote_url_response ); - } - - $html_head = $this->get_document_head( $remote_url_response ); - $meta_elements = $this->get_meta_with_content_elements( $html_head ); - - $data = $this->add_additional_fields_to_object( - array( - 'title' => $this->get_title( $html_head ), - 'icon' => $this->get_icon( $html_head, $url ), - 'description' => $this->get_description( $meta_elements ), - 'image' => $this->get_image( $meta_elements, $url ), - ), - $request - ); - - // Wrap the data in a response object. - $response = rest_ensure_response( $data ); - - /** - * Filters the URL data for the response. - * - * @param WP_REST_Response $response The response object. - * @param string $url The requested URL. - * @param WP_REST_Request $request Request object. - * @param array $remote_url_response HTTP response body from the remote URL. - */ - return apply_filters( 'rest_prepare_url_details', $response, $url, $request, $remote_url_response ); - } - - /** - * Checks whether a given request has permission to read remote urls. - * - * @return WP_Error|bool True if the request has access, or WP_Error object. - */ - public function permissions_check() { - if ( current_user_can( 'edit_posts' ) ) { - return true; - } - - foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) { - if ( current_user_can( $post_type->cap->edit_posts ) ) { - return true; - } - } - - return new WP_Error( - 'rest_cannot_view_url_details', - __( 'Sorry, you are not allowed to process remote urls.', 'gutenberg' ), - array( 'status' => rest_authorization_required_code() ) - ); - } - - /** - * Retrieves the document title from a remote URL. - * - * @param string $url The website url whose HTML we want to access. - * @return string|WP_Error The HTTP response from the remote URL, or an error. - */ - private function get_remote_url( $url ) { - - // Provide a modified UA string to workaround web properties which block WordPress "Pingbacks". - // Why? The UA string used for pingback requests contains `WordPress/` which is very similar - // to that used as the default UA string by the WP HTTP API. Therefore requests from this - // REST endpoint are being unintentionally blocked as they are misidentified as pingback requests. - // By slightly modifying the UA string, but still retaining the "WordPress" identification (via "WP") - // we are able to work around this issue. - // Example UA string: `WP-URLDetails/5.9-alpha-51389 (+http://localhost:8888)`. - $modified_user_agent = 'WP-URLDetails/' . get_bloginfo( 'version' ) . ' (+' . get_bloginfo( 'url' ) . ')'; - - $args = array( - 'limit_response_size' => 150 * KB_IN_BYTES, - 'user-agent' => $modified_user_agent, - ); - - /** - * Filters the HTTP request args for URL data retrieval. - * - * Can be used to adjust response size limit and other WP_Http::request args. - * - * @param array $args Arguments used for the HTTP request - * @param string $url The attempted URL. - */ - $args = apply_filters( 'rest_url_details_http_request_args', $args, $url ); - - $response = wp_safe_remote_get( - $url, - $args - ); - - if ( WP_Http::OK !== wp_remote_retrieve_response_code( $response ) ) { - // Not saving the error response to cache since the error might be temporary. - return new WP_Error( 'no_response', __( 'URL not found. Response returned a non-200 status code for this URL.', 'gutenberg' ), array( 'status' => WP_Http::NOT_FOUND ) ); - } - - $remote_body = wp_remote_retrieve_body( $response ); - - if ( empty( $remote_body ) ) { - return new WP_Error( 'no_content', __( 'Unable to retrieve body from response at this URL.', 'gutenberg' ), array( 'status' => WP_Http::NOT_FOUND ) ); - } - - return $remote_body; - } - - /** - * Parses the <title> contents from the provided HTML - * - * @param string $html The HTML from the remote website at URL. - * @return string The title tag contents on success, or an empty string. - */ - private function get_title( $html ) { - $pattern = '#<title[^>]*>(.*?)<\s*/\s*title>#is'; - preg_match( $pattern, $html, $match_title ); - - $title = ! empty( $match_title[1] ) && is_string( $match_title[1] ) ? trim( $match_title[1] ) : ''; - - if ( empty( $title ) ) { - return ''; - } - - return $this->prepare_metadata_for_output( $title ); - } - - /** - * Parses the site icon from the provided HTML - * - * @param string $html The HTML from the remote website at URL. - * @param string $url The target website URL. - * @return string The icon URI on success, or an empty string. - */ - private function get_icon( $html, $url ) { - // Grab the icon's link element. - $pattern = '#<link\s[^>]*rel=(?:[\"\']??)\s*(?:icon|shortcut icon|icon shortcut)\s*(?:[\"\']??)[^>]*\/?>#isU'; - preg_match( $pattern, $html, $element ); - $element = ! empty( $element[0] ) && is_string( $element[0] ) ? trim( $element[0] ) : ''; - if ( empty( $element ) ) { - return ''; - } - - // Get the icon's href value. - $pattern = '#href=([\"\']??)([^\" >]*?)\\1[^>]*#isU'; - preg_match( $pattern, $element, $icon ); - $icon = ! empty( $icon[2] ) && is_string( $icon[2] ) ? trim( $icon[2] ) : ''; - if ( empty( $icon ) ) { - return ''; - } - - // If the icon is a data URL, return it. - $parsed_icon = parse_url( $icon ); - if ( isset( $parsed_icon['schema'] ) && 'data' === $parsed_icon['scheme'] ) { - return $icon; - } - - // Attempt to convert relative URLs to absolute. - $parsed_url = parse_url( $url ); - $root_url = $parsed_url['scheme'] . '://' . $parsed_url['host'] . '/'; - $icon = WP_Http::make_absolute_url( $icon, $root_url ); - - return $icon; - } - - /** - * Parses the meta description from the provided HTML. - * - * @param array $meta_elements { - * A multi-dimensional indexed array on success, or empty array. - * - * @type string[] 0 Meta elements with a content attribute. - * @type string[] 1 Content attribute's opening quotation mark. - * @type string[] 2 Content attribute's value for each meta element. - * } - * @return string The meta description contents on success, or an empty string. - */ - private function get_description( $meta_elements ) { - // Bail out if there are no meta elements. - if ( empty( $meta_elements[0] ) ) { - return ''; - } - - $description = $this->get_metadata_from_meta_element( $meta_elements, 'name', '(?:description|og:description)' ); - - // Bail out if description not found. - if ( '' === $description ) { - return ''; - } - - return $this->prepare_metadata_for_output( $description ); - } - - /** - * Parses the Open Graph Image from the provided HTML. - * - * See: https://ogp.me/. - * - * @param array $meta_elements { - * A multi-dimensional indexed array on success, or empty array. - * - * @type string[] 0 Meta elements with a content attribute. - * @type string[] 1 Content attribute's opening quotation mark. - * @type string[] 2 Content attribute's value for each meta element. - * } - * @param string $url The target website URL. - * @return string The OG image on success, or empty string. - */ - private function get_image( $meta_elements, $url ) { - $image = $this->get_metadata_from_meta_element( $meta_elements, 'property', '(?:og:image|og:image:url)' ); - - // Bail out if image not found. - if ( '' === $image ) { - return ''; - } - - // Attempt to convert relative URLs to absolute. - $parsed_url = parse_url( $url ); - $root_url = $parsed_url['scheme'] . '://' . $parsed_url['host'] . '/'; - $image = WP_Http::make_absolute_url( $image, $root_url ); - - return $image; - } - - /** - * Prepare the metadata by: - * - * - stripping all HTML tags and tag entities - * - converting non-tag entities into characters. - * - * @param string $metadata The metadata content to prepare. - * @return string The prepared metadata. - */ - private function prepare_metadata_for_output( $metadata ) { - $metadata = html_entity_decode( $metadata, ENT_QUOTES, get_bloginfo( 'charset' ) ); - $metadata = wp_strip_all_tags( $metadata ); - return $metadata; - } - - /** - * Utility function to build cache key for a given URL. - * - * @param string $url The URL for which to build a cache key. - * @return string The cache key. - */ - private function build_cache_key_for_url( $url ) { - return 'g_url_details_response_' . md5( $url ); - } - - /** - * Utility function to retrieve a value from the cache at a given key. - * - * @param string $key The cache key. - * @return mixed The value from the cache. - */ - private function get_cache( $key ) { - return get_site_transient( $key ); - } - - /** - * Utility function to cache a given data set at a given cache key. - * - * @param string $key The cache key under which to store the value. - * @param string $data The data to be stored at the given cache key. - * @return bool True when transient set, or false. - */ - private function set_cache( $key, $data = '' ) { - $ttl = HOUR_IN_SECONDS; - - /** - * Filters the cache expiration. - * - * Can be used to adjust the time until expiration in seconds for the cache - * of the data retrieved for the given URL. - * - * @param int $ttl the time until cache expiration in seconds. - */ - $cache_expiration = apply_filters( 'rest_url_details_cache_expiration', $ttl ); - - return set_site_transient( $key, $data, $cache_expiration ); - } - - /** - * Retrieves the `<head>` section. - * - * @param string $html The string of HTML to parse. - * @return string The `<head>..</head>` section on success, or original HTML. - */ - private function get_document_head( $html ) { - $head_html = $html; - - // Find the opening `<head>` tag. - $head_start = strpos( $html, '<head' ); - if ( false === $head_start ) { - // Didn't find it. Return the original HTML. - return $html; - } - - // Find the closing `</head>` tag. - $head_end = strpos( $head_html, '</head>' ); - if ( false === $head_end ) { - // Didn't find it. Find the opening `<body>` tag. - $head_end = strpos( $head_html, '<body' ); - - // Didn't find it. Return the original HTML. - if ( false === $head_end ) { - return $html; - } - } - - // Extract the HTML from opening tag to the closing tag. Then add the closing tag. - $head_html = substr( $head_html, $head_start, $head_end ); - $head_html .= '</head>'; - - return $head_html; - } - - /** - * Gets all the <meta> elements that have a `content` attribute. - * - * @param string $html The string of HTML to be parsed. - * @return array { - * A multi-dimensional indexed array on success, or empty array. - * - * @type string[] 0 Meta elements with a content attribute. - * @type string[] 1 Content attribute's opening quotation mark. - * @type string[] 2 Content attribute's value for each meta element. - * } - */ - private function get_meta_with_content_elements( $html ) { - /* - * Parse all meta elements with a content attribute. - * - * Why first search for the content attribute rather than directly searching for name=description element? - * tl;dr The content attribute's value will be truncated when it contains a > symbol. - * - * The content attribute's value (i.e. the description to get) can have HTML in it and be well-formed as - * it's a string to the browser. Imagine what happens when attempting to match for the name=description - * first. Hmm, if a > or /> symbol is in the content attribute's value, then it terminates the match - * as the element's closing symbol. But wait, it's in the content attribute and is not the end of the - * element. This is a limitation of using regex. It can't determine "wait a minute this is inside of quotation". - * If this happens, what gets matched is not the entire element or all of the content. - * - * Why not search for the name=description and then content="(.*)"? - * The attribute order could be opposite. Plus, additional attributes may exist including being between - * the name and content attributes. - * - * Why not lookahead? - * Lookahead is not constrained to stay within the element. The first <meta it finds may not include - * the name or content, but rather could be from a different element downstream. - */ - $pattern = '#<meta\s' . - - /* - * Allows for additional attributes before the content attribute. - * Searches for anything other than > symbol. - */ - '[^>]*' . - - /* - * Find the content attribute. When found, capture its value (.*). - * - * Allows for (a) single or double quotes and (b) whitespace in the value. - * - * Why capture the opening quotation mark, i.e. (["\']), and then backreference, - * i.e \1, for the closing quotation mark? - * To ensure the closing quotation mark matches the opening one. Why? Attribute values - * can contain quotation marks, such as an apostrophe in the content. - */ - 'content=(["\']??)(.*)\1' . - - /* - * Allows for additional attributes after the content attribute. - * Searches for anything other than > symbol. - */ - '[^>]*' . - - /* - * \/?> searches for the closing > symbol, which can be in either /> or > format. - * # ends the pattern. - */ - '\/?>#' . - - /* - * These are the options: - * - i : case insensitive - * - s : allows newline characters for the . match (needed for multiline elements) - * - U means non-greedy matching - */ - 'isU'; - - preg_match_all( $pattern, $html, $elements ); - - return $elements; - } - - /** - * Gets the metadata from a target meta element. - * - * @param array $meta_elements { - * A multi-dimensional indexed array on success, or empty array. - * - * @type string[] 0 Meta elements with a content attribute. - * @type string[] 1 Content attribute's opening quotation mark. - * @type string[] 2 Content attribute's value for each meta element. - * } - * @param string $attr Attribute that identifies the element with the target metadata. - * @param string $attr_value The attribute's value that identifies the element with the target metadata. - * @return string The metadata on success, or an empty string. - */ - private function get_metadata_from_meta_element( $meta_elements, $attr, $attr_value ) { - // Bail out if there are no meta elements. - if ( empty( $meta_elements[0] ) ) { - return ''; - } - - $metadata = ''; - $pattern = '#' . - - /* - * Target this attribute and value to find the metadata element. - * - * Allows for (a) no, single, double quotes and (b) whitespace in the value. - * - * Why capture the opening quotation mark, i.e. (["\']), and then backreference, - * i.e \1, for the closing quotation mark? - * To ensure the closing quotation mark matches the opening one. Why? Attribute values - * can contain quotation marks, such as an apostrophe in the content. - */ - $attr . '=([\"\']??)\s*' . $attr_value . '\s*\1' . - - /* - * These are the options: - * - i : case insensitive - * - s : allows newline characters for the . match (needed for multiline elements) - * - U means non-greedy matching - */ - '#isU'; - - // Find the metdata element. - foreach ( $meta_elements[0] as $index => $element ) { - preg_match( $pattern, $element, $match ); - - // This is not the metadata element. Skip it. - if ( empty( $match ) ) { - continue; - } - - /* - * Found the metadata element. - * Get the metadata from its matching content array. - */ - if ( isset( $meta_elements[2][ $index ] ) && is_string( $meta_elements[2][ $index ] ) ) { - $metadata = trim( $meta_elements[2][ $index ] ); - } - - break; - } - - return $metadata; - } -} diff --git a/lib/compat/wordpress-5.9/class-wp-theme-json-5-9.php b/lib/compat/wordpress-5.9/class-wp-theme-json-5-9.php deleted file mode 100644 index 19b8611df0a4d..0000000000000 --- a/lib/compat/wordpress-5.9/class-wp-theme-json-5-9.php +++ /dev/null @@ -1,1944 +0,0 @@ -<?php -/** - * WP_Theme_JSON_5_9 class - * - * @package gutenberg - */ - -/** - * Class that encapsulates the processing of structures that adhere to the theme.json spec. - * - * This class is for internal core usage and is not supposed to be used by extenders (plugins and/or themes). - * This is a low-level API that may need to do breaking changes. Please, - * use get_global_settings, get_global_styles, and get_global_stylesheet instead. - * - * @access private - */ -class WP_Theme_JSON_5_9 { - - /** - * Container of data in theme.json format. - * - * @var array - */ - protected $theme_json = null; - - /** - * Holds block metadata extracted from block.json - * to be shared among all instances so we don't - * process it twice. - * - * @var array - */ - protected static $blocks_metadata = null; - - /** - * The CSS selector for the top-level styles. - * - * @var string - */ - const ROOT_BLOCK_SELECTOR = 'body'; - - /** - * The sources of data this object can represent. - * - * @since 5.8.0 - * @var string[] - */ - const VALID_ORIGINS = array( - 'default', - 'theme', - 'custom', - ); - - /** - * Presets are a set of values that serve - * to bootstrap some styles: colors, font sizes, etc. - * - * They are a unkeyed array of values such as: - * - * ```php - * array( - * array( - * 'slug' => 'unique-name-within-the-set', - * 'name' => 'Name for the UI', - * <value_key> => 'value' - * ), - * ) - * ``` - * - * This contains the necessary metadata to process them: - * - * - path => where to find the preset within the settings section - * - override => whether a theme preset with the same slug as a default preset - * can override it - * - use_default_names => whether to use the default names - * - value_key => the key that represents the value - * - value_func => optionally, instead of value_key, a function to generate - * the value that takes a preset as an argument - * (either value_key or value_func should be present) - * - css_vars => template string to use in generating the CSS Custom Property. - * Example output: "--wp--preset--duotone--blue: <value>" will generate as many CSS Custom Properties as presets defined - * substituting the $slug for the slug's value for each preset value. - * - classes => array containing a structure with the classes to - * generate for the presets, where for each array item - * the key is the class name and the value the property name. - * The "$slug" substring will be replaced by the slug of each preset. - * For example: - * 'classes' => array( - * '.has-$slug-color' => 'color', - * '.has-$slug-background-color' => 'background-color', - * '.has-$slug-border-color' => 'border-color', - * ) - * - properties => array of CSS properties to be used by kses to - * validate the content of each preset - * by means of the remove_insecure_properties method. - */ - const PRESETS_METADATA = array( - array( - 'path' => array( 'color', 'palette' ), - 'override' => array( 'color', 'defaultPalette' ), - 'use_default_names' => false, - 'value_key' => 'color', - 'css_vars' => '--wp--preset--color--$slug', - 'classes' => array( - '.has-$slug-color' => 'color', - '.has-$slug-background-color' => 'background-color', - '.has-$slug-border-color' => 'border-color', - ), - 'properties' => array( 'color', 'background-color', 'border-color' ), - ), - array( - 'path' => array( 'color', 'gradients' ), - 'override' => array( 'color', 'defaultGradients' ), - 'use_default_names' => false, - 'value_key' => 'gradient', - 'css_vars' => '--wp--preset--gradient--$slug', - 'classes' => array( '.has-$slug-gradient-background' => 'background' ), - 'properties' => array( 'background' ), - ), - array( - 'path' => array( 'color', 'duotone' ), - 'override' => true, - 'use_default_names' => false, - 'value_func' => 'gutenberg_get_duotone_filter_property', - 'css_vars' => '--wp--preset--duotone--$slug', - 'classes' => array(), - 'properties' => array( 'filter' ), - ), - array( - 'path' => array( 'typography', 'fontSizes' ), - 'override' => true, - 'use_default_names' => true, - 'value_key' => 'size', - 'css_vars' => '--wp--preset--font-size--$slug', - 'classes' => array( '.has-$slug-font-size' => 'font-size' ), - 'properties' => array( 'font-size' ), - ), - array( - 'path' => array( 'typography', 'fontFamilies' ), - 'override' => true, - 'use_default_names' => false, - 'value_key' => 'fontFamily', - 'css_vars' => '--wp--preset--font-family--$slug', - 'classes' => array( '.has-$slug-font-family' => 'font-family' ), - 'properties' => array( 'font-family' ), - ), - ); - - /** - * Metadata for style properties. - * - * Each element is a direct mapping from the CSS property name to the - * path to the value in theme.json & block attributes. - */ - const PROPERTIES_METADATA = array( - 'background' => array( 'color', 'gradient' ), - 'background-color' => array( 'color', 'background' ), - 'border-radius' => array( 'border', 'radius' ), - 'border-top-left-radius' => array( 'border', 'radius', 'topLeft' ), - 'border-top-right-radius' => array( 'border', 'radius', 'topRight' ), - 'border-bottom-left-radius' => array( 'border', 'radius', 'bottomLeft' ), - 'border-bottom-right-radius' => array( 'border', 'radius', 'bottomRight' ), - 'border-color' => array( 'border', 'color' ), - 'border-width' => array( 'border', 'width' ), - 'border-style' => array( 'border', 'style' ), - 'color' => array( 'color', 'text' ), - 'font-family' => array( 'typography', 'fontFamily' ), - 'font-size' => array( 'typography', 'fontSize' ), - 'font-style' => array( 'typography', 'fontStyle' ), - 'font-weight' => array( 'typography', 'fontWeight' ), - 'letter-spacing' => array( 'typography', 'letterSpacing' ), - 'line-height' => array( 'typography', 'lineHeight' ), - 'margin' => array( 'spacing', 'margin' ), - 'margin-top' => array( 'spacing', 'margin', 'top' ), - 'margin-right' => array( 'spacing', 'margin', 'right' ), - 'margin-bottom' => array( 'spacing', 'margin', 'bottom' ), - 'margin-left' => array( 'spacing', 'margin', 'left' ), - 'padding' => array( 'spacing', 'padding' ), - 'padding-top' => array( 'spacing', 'padding', 'top' ), - 'padding-right' => array( 'spacing', 'padding', 'right' ), - 'padding-bottom' => array( 'spacing', 'padding', 'bottom' ), - 'padding-left' => array( 'spacing', 'padding', 'left' ), - '--wp--style--block-gap' => array( 'spacing', 'blockGap' ), - 'text-decoration' => array( 'typography', 'textDecoration' ), - 'text-transform' => array( 'typography', 'textTransform' ), - 'filter' => array( 'filter', 'duotone' ), - ); - - /** - * Protected style properties. - * - * These style properties are only rendered if a setting enables it - * via a value other than `null`. - * - * Each element maps the style property to the corresponding theme.json - * setting key. - */ - const PROTECTED_PROPERTIES = array( - 'spacing.blockGap' => array( 'spacing', 'blockGap' ), - ); - - /** - * The top-level keys a theme.json can have. - * - * @var string[] - */ - const VALID_TOP_LEVEL_KEYS = array( - 'customTemplates', - 'settings', - 'styles', - 'templateParts', - 'version', - ); - - /** - * The valid properties under the settings key. - * - * @var array - */ - const VALID_SETTINGS = array( - 'appearanceTools' => null, - 'border' => array( - 'color' => null, - 'radius' => null, - 'style' => null, - 'width' => null, - ), - 'color' => array( - 'background' => null, - 'custom' => null, - 'customDuotone' => null, - 'customGradient' => null, - 'defaultGradients' => null, - 'defaultPalette' => null, - 'duotone' => null, - 'gradients' => null, - 'link' => null, - 'palette' => null, - 'text' => null, - ), - 'custom' => null, - 'layout' => array( - 'contentSize' => null, - 'wideSize' => null, - ), - 'spacing' => array( - 'blockGap' => null, - 'margin' => null, - 'padding' => null, - 'units' => null, - ), - 'typography' => array( - 'customFontSize' => null, - 'dropCap' => null, - 'fontFamilies' => null, - 'fontSizes' => null, - 'fontStyle' => null, - 'fontWeight' => null, - 'letterSpacing' => null, - 'lineHeight' => null, - 'textDecoration' => null, - 'textTransform' => null, - ), - ); - - /** - * The valid properties under the styles key. - * - * @var array - */ - const VALID_STYLES = array( - 'border' => array( - 'color' => null, - 'radius' => null, - 'style' => null, - 'width' => null, - ), - 'color' => array( - 'background' => null, - 'gradient' => null, - 'text' => null, - ), - 'filter' => array( - 'duotone' => null, - ), - 'spacing' => array( - 'margin' => null, - 'padding' => null, - 'blockGap' => 'top', - ), - 'typography' => array( - 'fontFamily' => null, - 'fontSize' => null, - 'fontStyle' => null, - 'fontWeight' => null, - 'letterSpacing' => null, - 'lineHeight' => null, - 'textDecoration' => null, - 'textTransform' => null, - ), - ); - - /** - * The valid elements that can be found under styles. - * - * @var string[] - */ - const ELEMENTS = array( - 'link' => 'a', - 'h1' => 'h1', - 'h2' => 'h2', - 'h3' => 'h3', - 'h4' => 'h4', - 'h5' => 'h5', - 'h6' => 'h6', - ); - - /** - * The latest version of the schema in use. - * - * @var int - */ - const LATEST_SCHEMA = 2; - - /** - * Constructor. - * - * @param array $theme_json A structure that follows the theme.json schema. - * @param string $origin Optional. What source of data this object represents. - * One of 'default', 'theme', or 'custom'. Default 'theme'. - */ - public function __construct( $theme_json = array(), $origin = 'theme' ) { - if ( ! in_array( $origin, static::VALID_ORIGINS, true ) ) { - $origin = 'theme'; - } - - $this->theme_json = WP_Theme_JSON_Schema_Gutenberg::migrate( $theme_json ); - $valid_block_names = array_keys( static::get_blocks_metadata() ); - $valid_element_names = array_keys( static::ELEMENTS ); - $theme_json = static::sanitize( $this->theme_json, $valid_block_names, $valid_element_names ); - $this->theme_json = static::maybe_opt_in_into_settings( $theme_json ); - - // Internally, presets are keyed by origin. - $nodes = static::get_setting_nodes( $this->theme_json ); - foreach ( $nodes as $node ) { - foreach ( static::PRESETS_METADATA as $preset_metadata ) { - $path = array_merge( $node['path'], $preset_metadata['path'] ); - $preset = _wp_array_get( $this->theme_json, $path, null ); - if ( null !== $preset ) { - // If the preset is not already keyed by origin. - if ( isset( $preset[0] ) || empty( $preset ) ) { - _wp_array_set( $this->theme_json, $path, array( $origin => $preset ) ); - } - } - } - } - } - - /** - * Enables some opt-in settings if theme declared support. - * - * @param array $theme_json A theme.json structure to modify. - * @return array The modified theme.json structure. - */ - protected static function maybe_opt_in_into_settings( $theme_json ) { - $new_theme_json = $theme_json; - - if ( - isset( $new_theme_json['settings']['appearanceTools'] ) && - true === $new_theme_json['settings']['appearanceTools'] - ) { - static::do_opt_in_into_settings( $new_theme_json['settings'] ); - } - - if ( isset( $new_theme_json['settings']['blocks'] ) && is_array( $new_theme_json['settings']['blocks'] ) ) { - foreach ( $new_theme_json['settings']['blocks'] as &$block ) { - if ( isset( $block['appearanceTools'] ) && ( true === $block['appearanceTools'] ) ) { - static::do_opt_in_into_settings( $block ); - } - } - } - - return $new_theme_json; - } - - /** - * Enables some settings. - * - * @param array $context The context to which the settings belong. - */ - protected static function do_opt_in_into_settings( &$context ) { - $to_opt_in = array( - array( 'border', 'color' ), - array( 'border', 'radius' ), - array( 'border', 'style' ), - array( 'border', 'width' ), - array( 'color', 'link' ), - array( 'spacing', 'blockGap' ), - array( 'spacing', 'margin' ), - array( 'spacing', 'padding' ), - array( 'typography', 'lineHeight' ), - ); - - foreach ( $to_opt_in as $path ) { - // Use "unset prop" as a marker instead of "null" because - // "null" can be a valid value for some props (e.g. blockGap). - if ( 'unset prop' === _wp_array_get( $context, $path, 'unset prop' ) ) { - _wp_array_set( $context, $path, true ); - } - } - - unset( $context['appearanceTools'] ); - } - - /** - * Sanitizes the input according to the schemas. - * - * @param array $input Structure to sanitize. - * @param array $valid_block_names List of valid block names. - * @param array $valid_element_names List of valid element names. - * @return array The sanitized output. - */ - protected static function sanitize( $input, $valid_block_names, $valid_element_names ) { - $output = array(); - - if ( ! is_array( $input ) ) { - return $output; - } - - $output = array_intersect_key( $input, array_flip( static::VALID_TOP_LEVEL_KEYS ) ); - - // Some styles are only meant to be available at the top-level (e.g.: blockGap), - // hence, the schema for blocks & elements should not have them. - $styles_non_top_level = static::VALID_STYLES; - foreach ( array_keys( $styles_non_top_level ) as $section ) { - foreach ( array_keys( $styles_non_top_level[ $section ] ) as $prop ) { - if ( 'top' === $styles_non_top_level[ $section ][ $prop ] ) { - unset( $styles_non_top_level[ $section ][ $prop ] ); - } - } - } - - // Build the schema based on valid block & element names. - $schema = array(); - $schema_styles_elements = array(); - foreach ( $valid_element_names as $element ) { - $schema_styles_elements[ $element ] = $styles_non_top_level; - } - $schema_styles_blocks = array(); - $schema_settings_blocks = array(); - foreach ( $valid_block_names as $block ) { - $schema_settings_blocks[ $block ] = static::VALID_SETTINGS; - $schema_styles_blocks[ $block ] = $styles_non_top_level; - $schema_styles_blocks[ $block ]['elements'] = $schema_styles_elements; - } - $schema['styles'] = static::VALID_STYLES; - $schema['styles']['blocks'] = $schema_styles_blocks; - $schema['styles']['elements'] = $schema_styles_elements; - $schema['settings'] = static::VALID_SETTINGS; - $schema['settings']['blocks'] = $schema_settings_blocks; - - // Remove anything that's not present in the schema. - foreach ( array( 'styles', 'settings' ) as $subtree ) { - if ( ! isset( $input[ $subtree ] ) ) { - continue; - } - - if ( ! is_array( $input[ $subtree ] ) ) { - unset( $output[ $subtree ] ); - continue; - } - - $result = static::remove_keys_not_in_schema( $input[ $subtree ], $schema[ $subtree ] ); - - if ( empty( $result ) ) { - unset( $output[ $subtree ] ); - } else { - $output[ $subtree ] = $result; - } - } - - return $output; - } - - /** - * Returns the metadata for each block. - * - * Example: - * - * { - * 'core/paragraph': { - * 'selector': 'p', - * 'elements': { - * 'link' => 'link selector', - * 'etc' => 'element selector' - * } - * }, - * 'core/heading': { - * 'selector': 'h1', - * 'elements': {} - * }, - * 'core/image': { - * 'selector': '.wp-block-image', - * 'duotone': 'img', - * 'elements': {} - * } - * } - * - * @return array Block metadata. - */ - protected static function get_blocks_metadata() { - if ( null !== static::$blocks_metadata ) { - return static::$blocks_metadata; - } - - static::$blocks_metadata = array(); - - $registry = WP_Block_Type_Registry::get_instance(); - $blocks = $registry->get_all_registered(); - foreach ( $blocks as $block_name => $block_type ) { - if ( - isset( $block_type->supports['__experimentalSelector'] ) && - is_string( $block_type->supports['__experimentalSelector'] ) - ) { - static::$blocks_metadata[ $block_name ]['selector'] = $block_type->supports['__experimentalSelector']; - } else { - static::$blocks_metadata[ $block_name ]['selector'] = '.wp-block-' . str_replace( '/', '-', str_replace( 'core/', '', $block_name ) ); - } - - if ( - isset( $block_type->supports['color']['__experimentalDuotone'] ) && - is_string( $block_type->supports['color']['__experimentalDuotone'] ) - ) { - static::$blocks_metadata[ $block_name ]['duotone'] = $block_type->supports['color']['__experimentalDuotone']; - } - - // Assign defaults, then overwrite those that the block sets by itself. - // If the block selector is compounded, will append the element to each - // individual block selector. - $block_selectors = explode( ',', static::$blocks_metadata[ $block_name ]['selector'] ); - foreach ( static::ELEMENTS as $el_name => $el_selector ) { - $element_selector = array(); - foreach ( $block_selectors as $selector ) { - if ( $selector === $el_selector ) { - $element_selector = array( $el_selector ); - break; - } - - $element_selector[] = $selector . ' ' . $el_selector; - } - static::$blocks_metadata[ $block_name ]['elements'][ $el_name ] = implode( ',', $element_selector ); - } - } - - return static::$blocks_metadata; - } - - /** - * Given a tree, removes the keys that are not present in the schema. - * - * It is recursive and modifies the input in-place. - * - * @param array $tree Input to process. - * @param array $schema Schema to adhere to. - * @return array Returns the modified $tree. - */ - protected static function remove_keys_not_in_schema( $tree, $schema ) { - $tree = array_intersect_key( $tree, $schema ); - - foreach ( $schema as $key => $data ) { - if ( ! isset( $tree[ $key ] ) ) { - continue; - } - - if ( is_array( $schema[ $key ] ) && is_array( $tree[ $key ] ) ) { - $tree[ $key ] = static::remove_keys_not_in_schema( $tree[ $key ], $schema[ $key ] ); - - if ( empty( $tree[ $key ] ) ) { - unset( $tree[ $key ] ); - } - } elseif ( is_array( $schema[ $key ] ) && ! is_array( $tree[ $key ] ) ) { - unset( $tree[ $key ] ); - } - } - - return $tree; - } - - /** - * Returns the existing settings for each block. - * - * Example: - * - * { - * 'root': { - * 'color': { - * 'custom': true - * } - * }, - * 'core/paragraph': { - * 'spacing': { - * 'customPadding': true - * } - * } - * } - * - * @return array Settings per block. - */ - public function get_settings() { - if ( ! isset( $this->theme_json['settings'] ) ) { - return array(); - } else { - return $this->theme_json['settings']; - } - } - - /** - * Returns the stylesheet that results of processing - * the theme.json structure this object represents. - * - * @param array $types Types of styles to load. Will load all by default. It accepts: - * 'variables': only the CSS Custom Properties for presets & custom ones. - * 'styles': only the styles section in theme.json. - * 'presets': only the classes for the presets. - * @param array $origins A list of origins to include. By default it includes VALID_ORIGINS. - * @return string Stylesheet. - */ - public function get_stylesheet( $types = array( 'variables', 'styles', 'presets' ), $origins = null ) { - if ( null === $origins ) { - $origins = static::VALID_ORIGINS; - } - - if ( is_string( $types ) ) { - // Dispatch error and map old arguments to new ones. - _deprecated_argument( __FUNCTION__, '5.9' ); - if ( 'block_styles' === $types ) { - $types = array( 'styles', 'presets' ); - } elseif ( 'css_variables' === $types ) { - $types = array( 'variables' ); - } else { - $types = array( 'variables', 'styles', 'presets' ); - } - } - - $blocks_metadata = static::get_blocks_metadata(); - $style_nodes = static::get_style_nodes( $this->theme_json, $blocks_metadata ); - $setting_nodes = static::get_setting_nodes( $this->theme_json, $blocks_metadata ); - - $stylesheet = ''; - - if ( in_array( 'variables', $types, true ) ) { - $stylesheet .= $this->get_css_variables( $setting_nodes, $origins ); - } - - if ( in_array( 'styles', $types, true ) ) { - $stylesheet .= $this->get_block_classes( $style_nodes ); - } - - if ( in_array( 'presets', $types, true ) ) { - $stylesheet .= $this->get_preset_classes( $setting_nodes, $origins ); - } - - return $stylesheet; - } - - /** - * Returns the page templates of the current theme. - * - * @return array - */ - public function get_custom_templates() { - $custom_templates = array(); - if ( ! isset( $this->theme_json['customTemplates'] ) || ! is_array( $this->theme_json['customTemplates'] ) ) { - return $custom_templates; - } - - foreach ( $this->theme_json['customTemplates'] as $item ) { - if ( isset( $item['name'] ) ) { - $custom_templates[ $item['name'] ] = array( - 'title' => isset( $item['title'] ) ? $item['title'] : '', - 'postTypes' => isset( $item['postTypes'] ) ? $item['postTypes'] : array( 'page' ), - ); - } - } - return $custom_templates; - } - - /** - * Returns the template part data of current theme. - * - * @return array - */ - public function get_template_parts() { - $template_parts = array(); - if ( ! isset( $this->theme_json['templateParts'] ) || ! is_array( $this->theme_json['templateParts'] ) ) { - return $template_parts; - } - - foreach ( $this->theme_json['templateParts'] as $item ) { - if ( isset( $item['name'] ) ) { - $template_parts[ $item['name'] ] = array( - 'title' => isset( $item['title'] ) ? $item['title'] : '', - 'area' => isset( $item['area'] ) ? $item['area'] : '', - ); - } - } - return $template_parts; - } - - /** - * Converts each style section into a list of rulesets - * containing the block styles to be appended to the stylesheet. - * - * See glossary at https://developer.mozilla.org/en-US/docs/Web/CSS/Syntax - * - * For each section this creates a new ruleset such as: - * - * block-selector { - * style-property-one: value; - * } - * - * @param array $style_nodes Nodes with styles. - * @return string The new stylesheet. - */ - protected function get_block_classes( $style_nodes ) { - $block_rules = ''; - - foreach ( $style_nodes as $metadata ) { - if ( null === $metadata['selector'] ) { - continue; - } - - $node = _wp_array_get( $this->theme_json, $metadata['path'], array() ); - $selector = $metadata['selector']; - $settings = _wp_array_get( $this->theme_json, array( 'settings' ) ); - $declarations = static::compute_style_properties( $node, $settings ); - - // 1. Separate the ones who use the general selector - // and the ones who use the duotone selector. - $declarations_duotone = array(); - foreach ( $declarations as $index => $declaration ) { - if ( 'filter' === $declaration['name'] ) { - unset( $declarations[ $index ] ); - $declarations_duotone[] = $declaration; - } - } - - /* - * Reset default browser margin on the root body element. - * This is set on the root selector **before** generating the ruleset - * from the `theme.json`. This is to ensure that if the `theme.json` declares - * `margin` in its `spacing` declaration for the `body` element then these - * user-generated values take precedence in the CSS cascade. - * @link https://github.com/WordPress/gutenberg/issues/36147. - */ - if ( static::ROOT_BLOCK_SELECTOR === $selector ) { - $block_rules .= 'body { margin: 0; }'; - } - - // 2. Generate the rules that use the general selector. - $block_rules .= static::to_ruleset( $selector, $declarations ); - - // 3. Generate the rules that use the duotone selector. - if ( isset( $metadata['duotone'] ) && ! empty( $declarations_duotone ) ) { - $selector_duotone = static::scope_selector( $metadata['selector'], $metadata['duotone'] ); - $block_rules .= static::to_ruleset( $selector_duotone, $declarations_duotone ); - } - - if ( static::ROOT_BLOCK_SELECTOR === $selector ) { - $block_rules .= '.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }'; - $block_rules .= '.wp-site-blocks > .alignright { float: right; margin-left: 2em; }'; - $block_rules .= '.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }'; - - $has_block_gap_support = _wp_array_get( $this->theme_json, array( 'settings', 'spacing', 'blockGap' ) ) !== null; - if ( $has_block_gap_support ) { - $block_rules .= '.wp-site-blocks > * { margin-top: 0; margin-bottom: 0; }'; - $block_rules .= '.wp-site-blocks > * + * { margin-top: var( --wp--style--block-gap ); }'; - } - } - } - - return $block_rules; - } - - /** - * Creates new rulesets as classes for each preset value such as: - * - * .has-value-color { - * color: value; - * } - * - * .has-value-background-color { - * background-color: value; - * } - * - * .has-value-font-size { - * font-size: value; - * } - * - * .has-value-gradient-background { - * background: value; - * } - * - * p.has-value-gradient-background { - * background: value; - * } - * - * @param array $setting_nodes Nodes with settings. - * @param array $origins List of origins to process presets from. - * @return string The new stylesheet. - */ - protected function get_preset_classes( $setting_nodes, $origins ) { - $preset_rules = ''; - - foreach ( $setting_nodes as $metadata ) { - if ( null === $metadata['selector'] ) { - continue; - } - - $selector = $metadata['selector']; - $node = _wp_array_get( $this->theme_json, $metadata['path'], array() ); - $preset_rules .= static::compute_preset_classes( $node, $selector, $origins ); - } - - return $preset_rules; - } - - /** - * Converts each styles section into a list of rulesets - * to be appended to the stylesheet. - * These rulesets contain all the css variables (custom variables and preset variables). - * - * See glossary at https://developer.mozilla.org/en-US/docs/Web/CSS/Syntax - * - * For each section this creates a new ruleset such as: - * - * block-selector { - * --wp--preset--category--slug: value; - * --wp--custom--variable: value; - * } - * - * @param array $nodes Nodes with settings. - * @param array $origins List of origins to process. - * @return string The new stylesheet. - */ - protected function get_css_variables( $nodes, $origins ) { - $stylesheet = ''; - foreach ( $nodes as $metadata ) { - if ( null === $metadata['selector'] ) { - continue; - } - - $selector = $metadata['selector']; - - $node = _wp_array_get( $this->theme_json, $metadata['path'], array() ); - $declarations = array_merge( static::compute_preset_vars( $node, $origins ), static::compute_theme_vars( $node ) ); - - $stylesheet .= static::to_ruleset( $selector, $declarations ); - } - - return $stylesheet; - } - - /** - * Given a selector and a declaration list, - * creates the corresponding ruleset. - * - * To help debugging, will add some space - * if SCRIPT_DEBUG is defined and true. - * - * @param string $selector CSS selector. - * @param array $declarations List of declarations. - * - * @return string CSS ruleset. - */ - protected static function to_ruleset( $selector, $declarations ) { - if ( empty( $declarations ) ) { - return ''; - } - $ruleset = ''; - - if ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) { - $declaration_block = array_reduce( - $declarations, - function ( $carry, $element ) { - return $carry .= "\t" . $element['name'] . ': ' . $element['value'] . ";\n"; }, - '' - ); - $ruleset .= $selector . " {\n" . $declaration_block . "}\n"; - } else { - $declaration_block = array_reduce( - $declarations, - function ( $carry, $element ) { - return $carry .= $element['name'] . ': ' . $element['value'] . ';'; }, - '' - ); - $ruleset .= $selector . '{' . $declaration_block . '}'; - } - - return $ruleset; - } - - /** - * Function that appends a sub-selector to a existing one. - * - * Given the compounded $selector "h1, h2, h3" - * and the $to_append selector ".some-class" the result will be - * "h1.some-class, h2.some-class, h3.some-class". - * - * @param string $selector Original selector. - * @param string $to_append Selector to append. - * @return string - */ - protected static function append_to_selector( $selector, $to_append ) { - $new_selectors = array(); - $selectors = explode( ',', $selector ); - foreach ( $selectors as $sel ) { - $new_selectors[] = $sel . $to_append; - } - - return implode( ',', $new_selectors ); - } - - /** - * Given a settings array, it returns the generated rulesets - * for the preset classes. - * - * @param array $settings Settings to process. - * @param string $selector Selector wrapping the classes. - * @param array $origins List of origins to process. - * @return string The result of processing the presets. - */ - protected static function compute_preset_classes( $settings, $selector, $origins ) { - if ( static::ROOT_BLOCK_SELECTOR === $selector ) { - // Classes at the global level do not need any CSS prefixed, - // and we don't want to increase its specificity. - $selector = ''; - } - - $stylesheet = ''; - foreach ( static::PRESETS_METADATA as $preset_metadata ) { - $slugs = static::get_settings_slugs( $settings, $preset_metadata, $origins ); - foreach ( $preset_metadata['classes'] as $class => $property ) { - foreach ( $slugs as $slug ) { - $css_var = static::replace_slug_in_string( $preset_metadata['css_vars'], $slug ); - $class_name = static::replace_slug_in_string( $class, $slug ); - $stylesheet .= static::to_ruleset( - static::append_to_selector( $selector, $class_name ), - array( - array( - 'name' => $property, - 'value' => 'var(' . $css_var . ') !important', - ), - ) - ); - } - } - } - - return $stylesheet; - } - - /** - * Function that scopes a selector with another one. This works a bit like - * SCSS nesting except the `&` operator isn't supported. - * - * <code> - * $scope = '.a, .b .c'; - * $selector = '> .x, .y'; - * $merged = scope_selector( $scope, $selector ); - * // $merged is '.a > .x, .a .y, .b .c > .x, .b .c .y' - * </code> - * - * @param string $scope Selector to scope to. - * @param string $selector Original selector. - * - * @return string Scoped selector. - */ - protected static function scope_selector( $scope, $selector ) { - $scopes = explode( ',', $scope ); - $selectors = explode( ',', $selector ); - - $selectors_scoped = array(); - foreach ( $scopes as $outer ) { - foreach ( $selectors as $inner ) { - $selectors_scoped[] = trim( $outer ) . ' ' . trim( $inner ); - } - } - - return implode( ', ', $selectors_scoped ); - } - - /** - * Gets preset values keyed by slugs based on settings and metadata. - * - * <code> - * $settings = array( - * 'typography' => array( - * 'fontFamilies' => array( - * array( - * 'slug' => 'sansSerif', - * 'fontFamily' => '"Helvetica Neue", sans-serif', - * ), - * array( - * 'slug' => 'serif', - * 'colors' => 'Georgia, serif', - * ) - * ), - * ), - * ); - * $meta = array( - * 'path' => array( 'typography', 'fontFamilies' ), - * 'value_key' => 'fontFamily', - * ); - * $values_by_slug = get_settings_values_by_slug(); - * // $values_by_slug === array( - * // 'sans-serif' => '"Helvetica Neue", sans-serif', - * // 'serif' => 'Georgia, serif', - * // ); - * </code> - * - * @param array $settings Settings to process. - * @param array $preset_metadata One of the PRESETS_METADATA values. - * @param array $origins List of origins to process. - * @return array Array of presets where each key is a slug and each value is the preset value. - */ - protected static function get_settings_values_by_slug( $settings, $preset_metadata, $origins ) { - $preset_per_origin = _wp_array_get( $settings, $preset_metadata['path'], array() ); - - $result = array(); - foreach ( $origins as $origin ) { - if ( ! isset( $preset_per_origin[ $origin ] ) ) { - continue; - } - foreach ( $preset_per_origin[ $origin ] as $preset ) { - $slug = _wp_to_kebab_case( $preset['slug'] ); - - $value = ''; - if ( isset( $preset_metadata['value_key'], $preset[ $preset_metadata['value_key'] ] ) ) { - $value_key = $preset_metadata['value_key']; - $value = $preset[ $value_key ]; - } elseif ( - isset( $preset_metadata['value_func'] ) && - is_callable( $preset_metadata['value_func'] ) - ) { - $value_func = $preset_metadata['value_func']; - $value = call_user_func( $value_func, $preset ); - } else { - // If we don't have a value, then don't add it to the result. - continue; - } - - $result[ $slug ] = $value; - } - } - return $result; - } - - /** - * Similar to get_settings_values_by_slug, but doesn't compute the value. - * - * @param array $settings Settings to process. - * @param array $preset_metadata One of the PRESETS_METADATA values. - * @param array $origins List of origins to process. - * @return array Array of presets where the key and value are both the slug. - */ - protected static function get_settings_slugs( $settings, $preset_metadata, $origins = null ) { - if ( null === $origins ) { - $origins = static::VALID_ORIGINS; - } - - $preset_per_origin = _wp_array_get( $settings, $preset_metadata['path'], array() ); - - $result = array(); - foreach ( $origins as $origin ) { - if ( ! isset( $preset_per_origin[ $origin ] ) ) { - continue; - } - foreach ( $preset_per_origin[ $origin ] as $preset ) { - $slug = _wp_to_kebab_case( $preset['slug'] ); - - // Use the array as a set so we don't get duplicates. - $result[ $slug ] = $slug; - } - } - return $result; - } - - /** - * Transform a slug into a CSS Custom Property. - * - * @param string $input String to replace. - * @param string $slug The slug value to use to generate the custom property. - * @return string The CSS Custom Property. Something along the lines of --wp--preset--color--black. - */ - protected static function replace_slug_in_string( $input, $slug ) { - return strtr( $input, array( '$slug' => $slug ) ); - } - - /** - * Given the block settings, it extracts the CSS Custom Properties - * for the presets and adds them to the $declarations array - * following the format: - * - * ```php - * array( - * 'name' => 'property_name', - * 'value' => 'property_value, - * ) - * ``` - * - * @param array $settings Settings to process. - * @param array $origins List of origins to process. - * @return array Returns the modified $declarations. - */ - protected static function compute_preset_vars( $settings, $origins ) { - $declarations = array(); - foreach ( static::PRESETS_METADATA as $preset_metadata ) { - $values_by_slug = static::get_settings_values_by_slug( $settings, $preset_metadata, $origins ); - foreach ( $values_by_slug as $slug => $value ) { - $declarations[] = array( - 'name' => static::replace_slug_in_string( $preset_metadata['css_vars'], $slug ), - 'value' => $value, - ); - } - } - - return $declarations; - } - - /** - * Given an array of settings, it extracts the CSS Custom Properties - * for the custom values and adds them to the $declarations - * array following the format: - * - * ```php - * array( - * 'name' => 'property_name', - * 'value' => 'property_value, - * ) - * ``` - * - * @param array $settings Settings to process. - * @return array Returns the modified $declarations. - */ - protected static function compute_theme_vars( $settings ) { - $declarations = array(); - $custom_values = _wp_array_get( $settings, array( 'custom' ), array() ); - $css_vars = static::flatten_tree( $custom_values ); - foreach ( $css_vars as $key => $value ) { - $declarations[] = array( - 'name' => '--wp--custom--' . $key, - 'value' => $value, - ); - } - - return $declarations; - } - - /** - * Given a tree, it creates a flattened one - * by merging the keys and binding the leaf values - * to the new keys. - * - * It also transforms camelCase names into kebab-case - * and substitutes '/' by '-'. - * - * This is thought to be useful to generate - * CSS Custom Properties from a tree, - * although there's nothing in the implementation - * of this function that requires that format. - * - * For example, assuming the given prefix is '--wp' - * and the token is '--', for this input tree: - * - * { - * 'some/property': 'value', - * 'nestedProperty': { - * 'sub-property': 'value' - * } - * } - * - * it'll return this output: - * - * { - * '--wp--some-property': 'value', - * '--wp--nested-property--sub-property': 'value' - * } - * - * @param array $tree Input tree to process. - * @param string $prefix Optional. Prefix to prepend to each variable. Default empty string. - * @param string $token Optional. Token to use between levels. Default '--'. - * @return array The flattened tree. - */ - protected static function flatten_tree( $tree, $prefix = '', $token = '--' ) { - $result = array(); - foreach ( $tree as $property => $value ) { - $new_key = $prefix . str_replace( - '/', - '-', - strtolower( _wp_to_kebab_case( $property ) ) - ); - - if ( is_array( $value ) ) { - $new_prefix = $new_key . $token; - $result = array_merge( - $result, - static::flatten_tree( $value, $new_prefix, $token ) - ); - } else { - $result[ $new_key ] = $value; - } - } - return $result; - } - - /** - * Given a styles array, it extracts the style properties - * and adds them to the $declarations array following the format: - * - * ```php - * array( - * 'name' => 'property_name', - * 'value' => 'property_value, - * ) - * ``` - * - * @param array $styles Styles to process. - * @param array $settings Theme settings. - * @param array $properties Properties metadata. - * @return array Returns the modified $declarations. - */ - protected static function compute_style_properties( $styles, $settings = array(), $properties = null ) { - if ( null === $properties ) { - $properties = static::PROPERTIES_METADATA; - } - - $declarations = array(); - if ( empty( $styles ) ) { - return $declarations; - } - - foreach ( $properties as $css_property => $value_path ) { - $value = static::get_property_value( $styles, $value_path ); - - // Look up protected properties, keyed by value path. - // Skip protected properties that are explicitly set to `null`. - if ( is_array( $value_path ) ) { - $path_string = implode( '.', $value_path ); - if ( - array_key_exists( $path_string, static::PROTECTED_PROPERTIES ) && - _wp_array_get( $settings, static::PROTECTED_PROPERTIES[ $path_string ], null ) === null - ) { - continue; - } - } - - // Skip if empty and not "0" or value represents array of longhand values. - $has_missing_value = empty( $value ) && ! is_numeric( $value ); - if ( $has_missing_value || is_array( $value ) ) { - continue; - } - - $declarations[] = array( - 'name' => $css_property, - 'value' => $value, - ); - } - - return $declarations; - } - - /** - * Returns the style property for the given path. - * - * It also converts CSS Custom Property stored as - * "var:preset|color|secondary" to the form - * "--wp--preset--color--secondary". - * - * @param array $styles Styles subtree. - * @param array $path Which property to process. - * @return string Style property value. - */ - protected static function get_property_value( $styles, $path ) { - $value = _wp_array_get( $styles, $path, '' ); - - if ( '' === $value || is_array( $value ) ) { - return $value; - } - - $prefix = 'var:'; - $prefix_len = strlen( $prefix ); - $token_in = '|'; - $token_out = '--'; - if ( 0 === strncmp( $value, $prefix, $prefix_len ) ) { - $unwrapped_name = str_replace( - $token_in, - $token_out, - substr( $value, $prefix_len ) - ); - $value = "var(--wp--$unwrapped_name)"; - } - - return $value; - } - - /** - * Builds metadata for the setting nodes, which returns in the form of: - * - * [ - * [ - * 'path' => ['path', 'to', 'some', 'node' ], - * 'selector' => 'CSS selector for some node' - * ], - * [ - * 'path' => [ 'path', 'to', 'other', 'node' ], - * 'selector' => 'CSS selector for other node' - * ], - * ] - * - * @param array $theme_json The tree to extract setting nodes from. - * @param array $selectors List of selectors per block. - * @return array - */ - protected static function get_setting_nodes( $theme_json, $selectors = array() ) { - $nodes = array(); - if ( ! isset( $theme_json['settings'] ) ) { - return $nodes; - } - - // Top-level. - $nodes[] = array( - 'path' => array( 'settings' ), - 'selector' => static::ROOT_BLOCK_SELECTOR, - ); - - // Calculate paths for blocks. - if ( ! isset( $theme_json['settings']['blocks'] ) ) { - return $nodes; - } - - foreach ( $theme_json['settings']['blocks'] as $name => $node ) { - $selector = null; - if ( isset( $selectors[ $name ]['selector'] ) ) { - $selector = $selectors[ $name ]['selector']; - } - - $nodes[] = array( - 'path' => array( 'settings', 'blocks', $name ), - 'selector' => $selector, - ); - } - - return $nodes; - } - - /** - * Builds metadata for the style nodes, which returns in the form of: - * - * [ - * [ - * 'path' => [ 'path', 'to', 'some', 'node' ], - * 'selector' => 'CSS selector for some node', - * 'duotone' => 'CSS selector for duotone for some node' - * ], - * [ - * 'path' => ['path', 'to', 'other', 'node' ], - * 'selector' => 'CSS selector for other node', - * 'duotone' => null - * ], - * ] - * - * @param array $theme_json The tree to extract style nodes from. - * @param array $selectors List of selectors per block. - * @return array - */ - protected static function get_style_nodes( $theme_json, $selectors = array() ) { - $nodes = array(); - if ( ! isset( $theme_json['styles'] ) ) { - return $nodes; - } - - // Top-level. - $nodes[] = array( - 'path' => array( 'styles' ), - 'selector' => static::ROOT_BLOCK_SELECTOR, - ); - - if ( isset( $theme_json['styles']['elements'] ) ) { - foreach ( $theme_json['styles']['elements'] as $element => $node ) { - $nodes[] = array( - 'path' => array( 'styles', 'elements', $element ), - 'selector' => static::ELEMENTS[ $element ], - ); - } - } - - // Blocks. - if ( ! isset( $theme_json['styles']['blocks'] ) ) { - return $nodes; - } - - foreach ( $theme_json['styles']['blocks'] as $name => $node ) { - $selector = null; - if ( isset( $selectors[ $name ]['selector'] ) ) { - $selector = $selectors[ $name ]['selector']; - } - - $duotone_selector = null; - if ( isset( $selectors[ $name ]['duotone'] ) ) { - $duotone_selector = $selectors[ $name ]['duotone']; - } - - $nodes[] = array( - 'path' => array( 'styles', 'blocks', $name ), - 'selector' => $selector, - 'duotone' => $duotone_selector, - ); - - if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'] ) ) { - foreach ( $theme_json['styles']['blocks'][ $name ]['elements'] as $element => $node ) { - $nodes[] = array( - 'path' => array( 'styles', 'blocks', $name, 'elements', $element ), - 'selector' => $selectors[ $name ]['elements'][ $element ], - ); - } - } - } - - return $nodes; - } - - /** - * Merge new incoming data. - * - * @param WP_Theme_JSON $incoming Data to merge. - */ - public function merge( $incoming ) { - $incoming_data = $incoming->get_raw_data(); - $this->theme_json = array_replace_recursive( $this->theme_json, $incoming_data ); - - /* - * The array_replace_recursive algorithm merges at the leaf level, - * but we don't want leaf arrays to be merged, so we overwrite it. - * - * For leaf values that are sequential arrays it will use the numeric indexes for replacement. - * We rather replace the existing with the incoming value, if it exists. - * This is the case of spacing.units. - * - * For leaf values that are associative arrays it will merge them as expected. - * This is also not the behavior we want for the current associative arrays (presets). - * We rather replace the existing with the incoming value, if it exists. - * This happens, for example, when we merge data from theme.json upon existing - * theme supports or when we merge anything coming from the same source twice. - * This is the case of color.palette, color.gradients, color.duotone, - * typography.fontSizes, or typography.fontFamilies. - * - * Additionally, for some preset types, we also want to make sure the - * values they introduce don't conflict with default values. We do so - * by checking the incoming slugs for theme presets and compare them - * with the equivalent default presets: if a slug is present as a default - * we remove it from the theme presets. - */ - $nodes = static::get_setting_nodes( $incoming_data ); - $slugs_global = static::get_default_slugs( $this->theme_json, array( 'settings' ) ); - foreach ( $nodes as $node ) { - $slugs_node = static::get_default_slugs( $this->theme_json, $node['path'] ); - $slugs = array_merge_recursive( $slugs_global, $slugs_node ); - - // Replace the spacing.units. - $path = array_merge( $node['path'], array( 'spacing', 'units' ) ); - $content = _wp_array_get( $incoming_data, $path, null ); - if ( isset( $content ) ) { - _wp_array_set( $this->theme_json, $path, $content ); - } - - // Replace the presets. - foreach ( static::PRESETS_METADATA as $preset ) { - $override_preset = static::should_override_preset( $this->theme_json, $node['path'], $preset['override'] ); - - foreach ( static::VALID_ORIGINS as $origin ) { - $base_path = array_merge( $node['path'], $preset['path'] ); - $path = array_merge( $base_path, array( $origin ) ); - $content = _wp_array_get( $incoming_data, $path, null ); - if ( ! isset( $content ) ) { - continue; - } - - if ( 'theme' === $origin && $preset['use_default_names'] ) { - foreach ( $content as &$item ) { - if ( ! array_key_exists( 'name', $item ) ) { - $name = static::get_name_from_defaults( $item['slug'], $base_path ); - if ( null !== $name ) { - $item['name'] = $name; - } - } - } - } - - if ( - ( 'theme' !== $origin ) || - ( 'theme' === $origin && $override_preset ) - ) { - _wp_array_set( $this->theme_json, $path, $content ); - } else { - $slugs_for_preset = _wp_array_get( $slugs, $preset['path'], array() ); - $content = static::filter_slugs( $content, $slugs_for_preset ); - _wp_array_set( $this->theme_json, $path, $content ); - } - } - } - } - } - - /** - * Converts all filter (duotone) presets into SVGs. - * - * @param array $origins List of origins to process. - * - * @return string SVG filters. - */ - public function get_svg_filters( $origins ) { - $blocks_metadata = static::get_blocks_metadata(); - $setting_nodes = static::get_setting_nodes( $this->theme_json, $blocks_metadata ); - - $filters = ''; - foreach ( $setting_nodes as $metadata ) { - $node = _wp_array_get( $this->theme_json, $metadata['path'], array() ); - if ( empty( $node['color']['duotone'] ) ) { - continue; - } - - $duotone_presets = $node['color']['duotone']; - - foreach ( $origins as $origin ) { - if ( ! isset( $duotone_presets[ $origin ] ) ) { - continue; - } - foreach ( $duotone_presets[ $origin ] as $duotone_preset ) { - $filters .= gutenberg_get_duotone_filter_svg( $duotone_preset ); - } - } - } - - return $filters; - } - - /** - * Returns whether a presets should be overriden or not. - * - * @param array $theme_json The theme.json like structure to inspect. - * @param array $path Path to inspect. - * @param bool|array $override Data to compute whether to override the preset. - * @return boolean - */ - protected static function should_override_preset( $theme_json, $path, $override ) { - if ( is_bool( $override ) ) { - return $override; - } - - // The relationship between whether to override the defaults - // and whether the defaults are enabled is inverse: - // - // - If defaults are enabled => theme presets should not be overriden - // - If defaults are disabled => theme presets should be overriden - // - // For example, a theme sets defaultPalette to false, - // making the default palette hidden from the user. - // In that case, we want all the theme presets to be present, - // so they should override the defaults. - if ( is_array( $override ) ) { - $value = _wp_array_get( $theme_json, array_merge( $path, $override ) ); - if ( isset( $value ) ) { - return ! $value; - } - - // Search the top-level key if none was found for this node. - $value = _wp_array_get( $theme_json, array_merge( array( 'settings' ), $override ) ); - if ( isset( $value ) ) { - return ! $value; - } - - return true; - } - } - - /** - * Returns the default slugs for all the presets in an associative array - * whose keys are the preset paths and the leafs is the list of slugs. - * - * For example: - * - * array( - * 'color' => array( - * 'palette' => array( 'slug-1', 'slug-2' ), - * 'gradients' => array( 'slug-3', 'slug-4' ), - * ), - * ) - * - * @param array $data A theme.json like structure. - * @param array $node_path The path to inspect. It's 'settings' by default. - * - * @return array - */ - protected static function get_default_slugs( $data, $node_path ) { - $slugs = array(); - - foreach ( static::PRESETS_METADATA as $metadata ) { - $path = array_merge( $node_path, $metadata['path'], array( 'default' ) ); - $preset = _wp_array_get( $data, $path, null ); - if ( ! isset( $preset ) ) { - continue; - } - - $slugs_for_preset = array(); - $slugs_for_preset = array_map( - function( $value ) { - return isset( $value['slug'] ) ? $value['slug'] : null; - }, - $preset - ); - _wp_array_set( $slugs, $metadata['path'], $slugs_for_preset ); - } - - return $slugs; - } - - /** - * Get a `default`'s preset name by a provided slug. - * - * @param string $slug The slug we want to find a match from default presets. - * @param array $base_path The path to inspect. It's 'settings' by default. - * - * @return string|null - */ - protected function get_name_from_defaults( $slug, $base_path ) { - $path = array_merge( $base_path, array( 'default' ) ); - $default_content = _wp_array_get( $this->theme_json, $path, null ); - if ( ! $default_content ) { - return null; - } - foreach ( $default_content as $item ) { - if ( $slug === $item['slug'] ) { - return $item['name']; - } - } - return null; - } - - /** - * Removes the preset values whose slug is equal to any of given slugs. - * - * @param array $node The node with the presets to validate. - * @param array $slugs The slugs that should not be overriden. - * - * @return array The new node - */ - protected static function filter_slugs( $node, $slugs ) { - if ( empty( $slugs ) ) { - return $node; - } - - $new_node = array(); - foreach ( $node as $value ) { - if ( isset( $value['slug'] ) && ! in_array( $value['slug'], $slugs, true ) ) { - $new_node[] = $value; - } - } - - return $new_node; - } - - /** - * Removes insecure data from theme.json. - * - * @param array $theme_json Structure to sanitize. - * @return array Sanitized structure. - */ - public static function remove_insecure_properties( $theme_json ) { - $sanitized = array(); - - $theme_json = WP_Theme_JSON_Schema_Gutenberg::migrate( $theme_json ); - - $valid_block_names = array_keys( static::get_blocks_metadata() ); - $valid_element_names = array_keys( static::ELEMENTS ); - $theme_json = static::sanitize( $theme_json, $valid_block_names, $valid_element_names ); - - $blocks_metadata = static::get_blocks_metadata(); - $style_nodes = static::get_style_nodes( $theme_json, $blocks_metadata ); - foreach ( $style_nodes as $metadata ) { - $input = _wp_array_get( $theme_json, $metadata['path'], array() ); - if ( empty( $input ) ) { - continue; - } - - $output = static::remove_insecure_styles( $input ); - if ( ! empty( $output ) ) { - _wp_array_set( $sanitized, $metadata['path'], $output ); - } - } - - $setting_nodes = static::get_setting_nodes( $theme_json ); - foreach ( $setting_nodes as $metadata ) { - $input = _wp_array_get( $theme_json, $metadata['path'], array() ); - if ( empty( $input ) ) { - continue; - } - - $output = static::remove_insecure_settings( $input ); - if ( ! empty( $output ) ) { - _wp_array_set( $sanitized, $metadata['path'], $output ); - } - } - - if ( empty( $sanitized['styles'] ) ) { - unset( $theme_json['styles'] ); - } else { - $theme_json['styles'] = $sanitized['styles']; - } - - if ( empty( $sanitized['settings'] ) ) { - unset( $theme_json['settings'] ); - } else { - $theme_json['settings'] = $sanitized['settings']; - } - - return $theme_json; - } - - /** - * Processes a setting node and returns the same node - * without the insecure settings. - * - * @param array $input Node to process. - * @return array - */ - protected static function remove_insecure_settings( $input ) { - $output = array(); - foreach ( static::PRESETS_METADATA as $preset_metadata ) { - foreach ( static::VALID_ORIGINS as $origin ) { - $path_with_origin = array_merge( $preset_metadata['path'], array( $origin ) ); - $presets = _wp_array_get( $input, $path_with_origin, null ); - if ( null === $presets ) { - continue; - } - - $escaped_preset = array(); - foreach ( $presets as $preset ) { - if ( - esc_attr( esc_html( $preset['name'] ) ) === $preset['name'] && - sanitize_html_class( $preset['slug'] ) === $preset['slug'] - ) { - $value = null; - if ( isset( $preset_metadata['value_key'], $preset[ $preset_metadata['value_key'] ] ) ) { - $value = $preset[ $preset_metadata['value_key'] ]; - } elseif ( - isset( $preset_metadata['value_func'] ) && - is_callable( $preset_metadata['value_func'] ) - ) { - $value = call_user_func( $preset_metadata['value_func'], $preset ); - } - - $preset_is_valid = true; - foreach ( $preset_metadata['properties'] as $property ) { - if ( ! static::is_safe_css_declaration( $property, $value ) ) { - $preset_is_valid = false; - break; - } - } - - if ( $preset_is_valid ) { - $escaped_preset[] = $preset; - } - } - } - - if ( ! empty( $escaped_preset ) ) { - _wp_array_set( $output, $path_with_origin, $escaped_preset ); - } - } - } - return $output; - } - - /** - * Processes a style node and returns the same node - * without the insecure styles. - * - * @param array $input Node to process. - * @return array - */ - protected static function remove_insecure_styles( $input ) { - $output = array(); - $declarations = static::compute_style_properties( $input ); - - foreach ( $declarations as $declaration ) { - if ( static::is_safe_css_declaration( $declaration['name'], $declaration['value'] ) ) { - $path = static::PROPERTIES_METADATA[ $declaration['name'] ]; - - // Check the value isn't an array before adding so as to not - // double up shorthand and longhand styles. - $value = _wp_array_get( $input, $path, array() ); - if ( ! is_array( $value ) ) { - _wp_array_set( $output, $path, $value ); - } - } - } - return $output; - } - - /** - * Checks that a declaration provided by the user is safe. - * - * @param string $property_name Property name in a CSS declaration, i.e. the `color` in `color: red`. - * @param string $property_value Value in a CSS declaration, i.e. the `red` in `color: red`. - * @return boolean - */ - protected static function is_safe_css_declaration( $property_name, $property_value ) { - $style_to_validate = $property_name . ': ' . $property_value; - $filtered = esc_html( safecss_filter_attr( $style_to_validate ) ); - return ! empty( trim( $filtered ) ); - } - - /** - * Returns the raw data. - * - * @return array Raw data. - */ - public function get_raw_data() { - return $this->theme_json; - } - - /** - * Transforms the given editor settings according the - * add_theme_support format to the theme.json format. - * - * @param array $settings Existing editor settings. - * @return array Config that adheres to the theme.json schema. - */ - public static function get_from_editor_settings( $settings ) { - $theme_settings = array( - 'version' => static::LATEST_SCHEMA, - 'settings' => array(), - ); - - // Deprecated theme supports. - if ( isset( $settings['disableCustomColors'] ) ) { - if ( ! isset( $theme_settings['settings']['color'] ) ) { - $theme_settings['settings']['color'] = array(); - } - $theme_settings['settings']['color']['custom'] = ! $settings['disableCustomColors']; - } - - if ( isset( $settings['disableCustomGradients'] ) ) { - if ( ! isset( $theme_settings['settings']['color'] ) ) { - $theme_settings['settings']['color'] = array(); - } - $theme_settings['settings']['color']['customGradient'] = ! $settings['disableCustomGradients']; - } - - if ( isset( $settings['disableCustomFontSizes'] ) ) { - if ( ! isset( $theme_settings['settings']['typography'] ) ) { - $theme_settings['settings']['typography'] = array(); - } - $theme_settings['settings']['typography']['customFontSize'] = ! $settings['disableCustomFontSizes']; - } - - if ( isset( $settings['enableCustomLineHeight'] ) ) { - if ( ! isset( $theme_settings['settings']['typography'] ) ) { - $theme_settings['settings']['typography'] = array(); - } - $theme_settings['settings']['typography']['lineHeight'] = $settings['enableCustomLineHeight']; - } - - if ( isset( $settings['enableCustomUnits'] ) ) { - if ( ! isset( $theme_settings['settings']['spacing'] ) ) { - $theme_settings['settings']['spacing'] = array(); - } - $theme_settings['settings']['spacing']['units'] = ( true === $settings['enableCustomUnits'] ) ? - array( 'px', 'em', 'rem', 'vh', 'vw', '%' ) : - $settings['enableCustomUnits']; - } - - if ( isset( $settings['colors'] ) ) { - if ( ! isset( $theme_settings['settings']['color'] ) ) { - $theme_settings['settings']['color'] = array(); - } - $theme_settings['settings']['color']['palette'] = $settings['colors']; - } - - if ( isset( $settings['gradients'] ) ) { - if ( ! isset( $theme_settings['settings']['color'] ) ) { - $theme_settings['settings']['color'] = array(); - } - $theme_settings['settings']['color']['gradients'] = $settings['gradients']; - } - - if ( isset( $settings['fontSizes'] ) ) { - $font_sizes = $settings['fontSizes']; - // Back-compatibility for presets without units. - foreach ( $font_sizes as $key => $font_size ) { - if ( is_numeric( $font_size['size'] ) ) { - $font_sizes[ $key ]['size'] = $font_size['size'] . 'px'; - } - } - if ( ! isset( $theme_settings['settings']['typography'] ) ) { - $theme_settings['settings']['typography'] = array(); - } - $theme_settings['settings']['typography']['fontSizes'] = $font_sizes; - } - - // This allows to make the plugin work with WordPress 5.7 beta - // as well as lower versions. The second check can be removed - // as soon as the minimum WordPress version for the plugin - // is bumped to 5.7. - if ( isset( $settings['enableCustomSpacing'] ) ) { - if ( ! isset( $theme_settings['settings']['spacing'] ) ) { - $theme_settings['settings']['spacing'] = array(); - } - $theme_settings['settings']['spacing']['padding'] = $settings['enableCustomSpacing']; - } - - // Things that didn't land in core yet, so didn't have a setting assigned. - // This should be removed when the plugin minimum WordPress version - // is bumped to 5.9. - // - // Since WordPress 5.9, the CSS Custom Properties are enqueued for all themes, - // so we no longer need to detect theme support for this. - // Removing this code will have the effect of making link color - // not visible for classic themes that opted-in and depended on the plugin - // to show the UI control for link color to users. - // - // Do not port this to WordPress core. - if ( current( (array) get_theme_support( 'experimental-link-color' ) ) ) { - if ( ! isset( $theme_settings['settings']['color'] ) ) { - $theme_settings['settings']['color'] = array(); - } - $theme_settings['settings']['color']['link'] = true; - } - - return $theme_settings; - } - -} diff --git a/lib/compat/wordpress-5.9/class-wp-theme-json-resolver-5-9.php b/lib/compat/wordpress-5.9/class-wp-theme-json-resolver-5-9.php deleted file mode 100644 index b00c6e708c763..0000000000000 --- a/lib/compat/wordpress-5.9/class-wp-theme-json-resolver-5-9.php +++ /dev/null @@ -1,416 +0,0 @@ -<?php -/** - * WP_Theme_JSON_Resolver_Gutenberg class - * - * @package gutenberg - */ - -/** - * Class that abstracts the processing of the different data sources - * for site-level config and offers an API to work with them. - * - * This class is for internal core usage and is not supposed to be used by extenders (plugins and/or themes). - * This is a low-level API that may need to do breaking changes. Please, - * use get_global_settings, get_global_styles, and get_global_stylesheet instead. - * - * @access private - */ -class WP_Theme_JSON_Resolver_5_9 { - - /** - * Container for data coming from core. - * - * @var WP_Theme_JSON_Gutenberg - */ - protected static $core = null; - - /** - * Container for data coming from the theme. - * - * @var WP_Theme_JSON_Gutenberg - */ - protected static $theme = null; - - /** - * Whether or not the theme supports theme.json. - * - * @var bool - */ - protected static $theme_has_support = null; - - /** - * Container for data coming from the user. - * - * @var WP_Theme_JSON_Gutenberg - */ - protected static $user = null; - - /** - * Stores the ID of the custom post type - * that holds the user data. - * - * @var integer - */ - protected static $user_custom_post_type_id = null; - - /** - * Container to keep loaded i18n schema for `theme.json`. - * - * @var array - */ - protected static $i18n_schema = null; - - /** - * Processes a file that adheres to the theme.json - * schema and returns an array with its contents, - * or a void array if none found. - * - * @param string $file_path Path to file. Empty if no file. - * @return array Contents that adhere to the theme.json schema. - */ - protected static function read_json_file( $file_path ) { - $config = array(); - if ( $file_path ) { - $decoded_file = wp_json_file_decode( $file_path, array( 'associative' => true ) ); - if ( is_array( $decoded_file ) ) { - $config = $decoded_file; - } - } - return $config; - } - - /** - * Returns a data structure used in theme.json translation. - * - * @deprecated - * @return array An array of theme.json fields that are translatable and the keys that are translatable - */ - public static function get_fields_to_translate() { - _deprecated_function( __METHOD__, '5.9.0' ); - return array(); - } - - /** - * Given a theme.json structure modifies it in place - * to update certain values by its translated strings - * according to the language set by the user. - * - * @param array $theme_json The theme.json to translate. - * @param string $domain Optional. Text domain. Unique identifier for retrieving translated strings. - * Default 'default'. - * @return array Returns the modified $theme_json_structure. - */ - protected static function translate( $theme_json, $domain = 'default' ) { - if ( null === static::$i18n_schema ) { - $i18n_schema = wp_json_file_decode( __DIR__ . '/theme-i18n.json' ); - static::$i18n_schema = null === $i18n_schema ? array() : $i18n_schema; - } - - return translate_settings_using_i18n_schema( static::$i18n_schema, $theme_json, $domain ); - } - - /** - * Return core's origin config. - * - * @return WP_Theme_JSON_Gutenberg Entity that holds core data. - */ - public static function get_core_data() { - if ( null !== static::$core ) { - return static::$core; - } - - $config = static::read_json_file( __DIR__ . '/theme.json' ); - $config = static::translate( $config ); - static::$core = new WP_Theme_JSON_Gutenberg( $config, 'default' ); - - return static::$core; - } - - /** - * Returns the theme's data. - * - * Data from theme.json will be backfilled from existing - * theme supports, if any. Note that if the same data - * is present in theme.json and in theme supports, - * the theme.json takes precedence. - * - * @param array $deprecated Deprecated argument. - * @return WP_Theme_JSON_Gutenberg Entity that holds theme data. - */ - public static function get_theme_data( $deprecated = array() ) { - if ( ! empty( $deprecated ) ) { - _deprecated_argument( __METHOD__, '5.9' ); - } - if ( null === static::$theme ) { - $theme_json_data = static::read_json_file( static::get_file_path_from_theme( 'theme.json' ) ); - $theme_json_data = static::translate( $theme_json_data, wp_get_theme()->get( 'TextDomain' ) ); - static::$theme = new WP_Theme_JSON_Gutenberg( $theme_json_data ); - - if ( wp_get_theme()->parent() ) { - // Get parent theme.json. - $parent_theme_json_data = static::read_json_file( static::get_file_path_from_theme( 'theme.json', true ) ); - $parent_theme_json_data = static::translate( $parent_theme_json_data, wp_get_theme()->parent()->get( 'TextDomain' ) ); - $parent_theme = new WP_Theme_JSON_Gutenberg( $parent_theme_json_data ); - - // Merge the child theme.json into the parent theme.json. - // The child theme takes precedence over the parent. - $parent_theme->merge( static::$theme ); - static::$theme = $parent_theme; - } - } - - /* - * We want the presets and settings declared in theme.json - * to override the ones declared via theme supports. - * So we take theme supports, transform it to theme.json shape - * and merge the static::$theme upon that. - */ - $theme_support_data = WP_Theme_JSON_Gutenberg::get_from_editor_settings( get_default_block_editor_settings() ); - if ( ! static::theme_has_support() ) { - if ( ! isset( $theme_support_data['settings']['color'] ) ) { - $theme_support_data['settings']['color'] = array(); - } - - $default_palette = false; - if ( current_theme_supports( 'default-color-palette' ) ) { - $default_palette = true; - } - if ( ! isset( $theme_support_data['settings']['color']['palette'] ) ) { - // If the theme does not have any palette, we still want to show the core one. - $default_palette = true; - } - $theme_support_data['settings']['color']['defaultPalette'] = $default_palette; - - $default_gradients = false; - if ( current_theme_supports( 'default-gradient-presets' ) ) { - $default_gradients = true; - } - if ( ! isset( $theme_support_data['settings']['color']['gradients'] ) ) { - // If the theme does not have any gradients, we still want to show the core ones. - $default_gradients = true; - } - $theme_support_data['settings']['color']['defaultGradients'] = $default_gradients; - } - $with_theme_supports = new WP_Theme_JSON_Gutenberg( $theme_support_data ); - $with_theme_supports->merge( static::$theme ); - - return $with_theme_supports; - } - - /** - * Returns the custom post type that contains the user's origin config - * for the current theme or a void array if none are found. - * - * This can also create and return a new draft custom post type. - * - * @param WP_Theme $theme The theme object. If empty, it - * defaults to the current theme. - * @param bool $create_post Optional. Whether a new custom post - * type should be created if none are - * found. False by default. - * @param array $post_status_filter Filter Optional. custom post type by - * post status. ['publish'] by default, - * so it only fetches published posts. - * @return array Custom Post Type for the user's origin config. - */ - public static function get_user_data_from_wp_global_styles( $theme, $create_post = false, $post_status_filter = array( 'publish' ) ) { - if ( ! $theme instanceof WP_Theme ) { - $theme = wp_get_theme(); - } - $user_cpt = array(); - $post_type_filter = 'wp_global_styles'; - $args = array( - 'numberposts' => 1, - 'orderby' => 'date', - 'order' => 'desc', - 'post_type' => $post_type_filter, - 'post_status' => $post_status_filter, - 'tax_query' => array( - array( - 'taxonomy' => 'wp_theme', - 'field' => 'name', - 'terms' => $theme->get_stylesheet(), - ), - ), - ); - - $cache_key = sprintf( 'wp_global_styles_%s', md5( serialize( $args ) ) ); - $post_id = wp_cache_get( $cache_key ); - - if ( (int) $post_id > 0 ) { - return get_post( $post_id, ARRAY_A ); - } - - // Special case: '-1' is a results not found. - if ( -1 === $post_id && ! $create_post ) { - return $user_cpt; - } - - $recent_posts = wp_get_recent_posts( $args ); - if ( is_array( $recent_posts ) && ( count( $recent_posts ) === 1 ) ) { - $user_cpt = $recent_posts[0]; - } elseif ( $create_post ) { - $cpt_post_id = wp_insert_post( - array( - 'post_content' => '{"version": ' . WP_Theme_JSON_Gutenberg::LATEST_SCHEMA . ', "isGlobalStylesUserThemeJSON": true }', - 'post_status' => 'publish', - 'post_title' => __( 'Custom Styles', 'default' ), - 'post_type' => $post_type_filter, - 'post_name' => 'wp-global-styles-' . urlencode( wp_get_theme()->get_stylesheet() ), - 'tax_input' => array( - 'wp_theme' => array( wp_get_theme()->get_stylesheet() ), - ), - ), - true - ); - $user_cpt = get_post( $cpt_post_id, ARRAY_A ); - } - $cache_expiration = $user_cpt ? DAY_IN_SECONDS : HOUR_IN_SECONDS; - wp_cache_set( $cache_key, $user_cpt ? $user_cpt['ID'] : -1, '', $cache_expiration ); - - return $user_cpt; - } - - /** - * Returns the user's origin config. - * - * @return WP_Theme_JSON_Gutenberg Entity that holds styles for user data. - */ - public static function get_user_data() { - if ( null !== static::$user ) { - return static::$user; - } - - $config = array(); - $user_cpt = static::get_user_data_from_wp_global_styles( wp_get_theme() ); - - if ( array_key_exists( 'post_content', $user_cpt ) ) { - $decoded_data = json_decode( $user_cpt['post_content'], true ); - - $json_decoding_error = json_last_error(); - if ( JSON_ERROR_NONE !== $json_decoding_error ) { - trigger_error( 'Error when decoding a theme.json schema for user data. ' . json_last_error_msg() ); - return new WP_Theme_JSON_Gutenberg( $config, 'custom' ); - } - - // Very important to verify if the flag isGlobalStylesUserThemeJSON is true. - // If is not true the content was not escaped and is not safe. - if ( - is_array( $decoded_data ) && - isset( $decoded_data['isGlobalStylesUserThemeJSON'] ) && - $decoded_data['isGlobalStylesUserThemeJSON'] - ) { - unset( $decoded_data['isGlobalStylesUserThemeJSON'] ); - $config = $decoded_data; - } - } - static::$user = new WP_Theme_JSON_Gutenberg( $config, 'custom' ); - - return static::$user; - } - - /** - * There are three sources of data (origins) for a site: - * default, theme, and custom. The custom's has higher priority - * than the theme's, and the theme's higher than defaults's. - * - * Unlike the getters {@link get_core_data}, - * {@link get_theme_data}, and {@link get_user_data}, - * this method returns data after it has been merged - * with the previous origins. This means that if the same piece of data - * is declared in different origins (user, theme, and core), - * the last origin overrides the previous. - * - * For example, if the user has set a background color - * for the paragraph block, and the theme has done it as well, - * the user preference wins. - * - * @param string $origin Optional. To what level should we merge data. - * Valid values are 'theme' or 'custom'. - * Default is 'custom'. - * @return WP_Theme_JSON_Gutenberg - */ - public static function get_merged_data( $origin = 'custom' ) { - if ( is_array( $origin ) ) { - _deprecated_argument( __FUNCTION__, '5.9' ); - } - - $result = new WP_Theme_JSON_Gutenberg(); - $result->merge( static::get_core_data() ); - $result->merge( static::get_theme_data() ); - - if ( 'custom' === $origin ) { - $result->merge( static::get_user_data() ); - } - - return $result; - } - - /** - * Returns the ID of the custom post type - * that stores user data. - * - * @return integer|null - */ - public static function get_user_global_styles_post_id() { - if ( null !== static::$user_custom_post_type_id ) { - return static::$user_custom_post_type_id; - } - - $user_cpt = static::get_user_data_from_wp_global_styles( wp_get_theme(), true ); - - if ( array_key_exists( 'ID', $user_cpt ) ) { - static::$user_custom_post_type_id = $user_cpt['ID']; - } - - return static::$user_custom_post_type_id; - } - - /** - * Whether the current theme has a theme.json file. - * - * @return bool - */ - public static function theme_has_support() { - if ( ! isset( static::$theme_has_support ) ) { - static::$theme_has_support = ( - is_readable( static::get_file_path_from_theme( 'theme.json' ) ) || - is_readable( static::get_file_path_from_theme( 'theme.json', true ) ) - ); - } - - return static::$theme_has_support; - } - - /** - * Builds the path to the given file and checks that it is readable. - * - * If it isn't, returns an empty string, otherwise returns the whole file path. - * - * @param string $file_name Name of the file. - * @param bool $template Optional. Use template theme directory. Default false. - * @return string The whole file path or empty if the file doesn't exist. - */ - protected static function get_file_path_from_theme( $file_name, $template = false ) { - $path = $template ? get_template_directory() : get_stylesheet_directory(); - $candidate = $path . '/' . $file_name; - - return is_readable( $candidate ) ? $candidate : ''; - } - - /** - * Cleans the cached data so it can be recalculated. - */ - public static function clean_cached_data() { - static::$core = null; - static::$theme = null; - static::$user = null; - static::$user_custom_post_type_id = null; - static::$theme_has_support = null; - static::$i18n_schema = null; - } - -} - -add_action( 'switch_theme', array( 'WP_Theme_JSON_Resolver_Gutenberg', 'clean_cached_data' ) ); -add_action( 'start_previewing_theme', array( 'WP_Theme_JSON_Resolver_Gutenberg', 'clean_cached_data' ) ); diff --git a/lib/compat/wordpress-5.9/class-wp-theme-json-schema-gutenberg.php b/lib/compat/wordpress-5.9/class-wp-theme-json-schema-gutenberg.php deleted file mode 100644 index 0fa833c68072c..0000000000000 --- a/lib/compat/wordpress-5.9/class-wp-theme-json-schema-gutenberg.php +++ /dev/null @@ -1,489 +0,0 @@ -<?php -/** - * Class that implements a theme.json schema migration. - * - * @package gutenberg - */ - -/** - * Class that migrates a given structure in v0 schema to one - * that follows the v1 schema. - */ -class WP_Theme_JSON_Schema_Gutenberg { - - /** - * How to address all the blocks - * in the v0 of the theme.json file. - */ - const V0_ALL_BLOCKS_NAME = 'defaults'; - - /** - * How to address the root block - * in the v0 of the theme.json file. - * - * @var string - */ - const V0_ROOT_BLOCK_NAME = 'root'; - - /** - * Maps old properties to their new location within the schema's settings for v1. - * This will be applied at both the defaults and individual block levels. - */ - const V1_RENAMED_PATHS = array( - 'border.customColor' => 'border.color', - 'border.customStyle' => 'border.style', - 'border.customWidth' => 'border.width', - 'typography.customFontStyle' => 'typography.fontStyle', - 'typography.customFontWeight' => 'typography.fontWeight', - 'typography.customLetterSpacing' => 'typography.letterSpacing', - 'typography.customTextDecorations' => 'typography.textDecoration', - 'typography.customTextTransforms' => 'typography.textTransform', - ); - - /** - * Maps old properties to their new location within the schema's settings. - * This will be applied at both the defaults and individual block levels. - */ - const V1_TO_V2_RENAMED_PATHS = array( - 'border.customRadius' => 'border.radius', - 'spacing.customMargin' => 'spacing.margin', - 'spacing.customPadding' => 'spacing.padding', - 'typography.customLineHeight' => 'typography.lineHeight', - ); - - /** - * Function that migrates a given theme.json structure to the last version. - * - * @param array $theme_json The structure to migrate. - * - * @return array The structure in the last version. - */ - public static function migrate( $theme_json ) { - // Can be removed when the plugin minimum required version is WordPress 5.8. - // This doesn't need to land in WordPress core. - if ( ! isset( $theme_json['version'] ) || 0 === $theme_json['version'] ) { - $theme_json = self::migrate_v0_to_v1( $theme_json ); - } - - // Provide backwards compatibility for settings that did not land in 5.8 - // and have had their `custom` prefixed removed since. - // Can be removed when the plugin minimum required version is WordPress 5.9. - // This doesn't need to land in WordPress core. - if ( 1 === $theme_json['version'] ) { - $theme_json = self::migrate_v1_remove_custom_prefixes( $theme_json ); - } - - if ( 1 === $theme_json['version'] ) { - $theme_json = self::migrate_v1_to_v2( $theme_json ); - } - - return $theme_json; - } - - /** - * Converts a v0 data structure into a v1 one. - * - * It expects input data to come in v0 form: - * - * { - * 'root': { - * 'settings': { ... }, - * 'styles': { ... } - * } - * 'core/paragraph': { - * 'styles': { ... }, - * 'settings': { ... } - * }, - * 'core/heading/h1': { - * 'settings': { ... } - * 'styles': { ... } - * }, - * 'core/heading/h2': { - * 'settings': { ... } - * 'styles': { ... } - * }, - * } - * - * And it will return v1 form: - * - * { - * 'settings': { - * 'border': { ... } - * 'color': { ... }, - * 'typography': { ... }, - * 'spacing': { ... }, - * 'custom': { ... }, - * 'blocks': { - * 'core/paragraph': { ... } - * } - * }, - * styles: { - * border: { ... } - * color: { ... }, - * typography: { ... }, - * spacing: { ... }, - * custom: { ... }, - * blocks: { - * core/paragraph: { ... } - * core/heading: { - * elements: { - * h1: { ... }, - * h2: { ... } - * } - * } - * } - * } - * } - * - * @param array $old Data in v0 schema. - * - * @return array Data in v1 schema. - */ - private static function migrate_v0_to_v1( $old ) { - // Copy everything. - $new = $old; - - // Overwrite the things that change. - if ( isset( $old['settings'] ) ) { - $new['settings'] = self::migrate_v0_to_v1_process_settings( $old['settings'] ); - } - if ( isset( $old['styles'] ) ) { - $new['styles'] = self::migrate_v0_to_v1_process_styles( $old['styles'] ); - } - - $new['version'] = 1; - - return $new; - } - - /** - * Processes the settings subtree. - * - * @param array $settings Array to process. - * - * @return array The settings in the new format. - */ - private static function migrate_v0_to_v1_process_settings( $settings ) { - $new = array(); - $blocks_to_consolidate = array( - 'core/heading/h1' => 'core/heading', - 'core/heading/h2' => 'core/heading', - 'core/heading/h3' => 'core/heading', - 'core/heading/h4' => 'core/heading', - 'core/heading/h5' => 'core/heading', - 'core/heading/h6' => 'core/heading', - 'core/post-title/h1' => 'core/post-title', - 'core/post-title/h2' => 'core/post-title', - 'core/post-title/h3' => 'core/post-title', - 'core/post-title/h4' => 'core/post-title', - 'core/post-title/h5' => 'core/post-title', - 'core/post-title/h6' => 'core/post-title', - 'core/query-title/h1' => 'core/query-title', - 'core/query-title/h2' => 'core/query-title', - 'core/query-title/h3' => 'core/query-title', - 'core/query-title/h4' => 'core/query-title', - 'core/query-title/h5' => 'core/query-title', - 'core/query-title/h6' => 'core/query-title', - ); - - $paths_to_override = array( - array( 'color', 'palette' ), - array( 'color', 'gradients' ), - array( 'spacing', 'units' ), - array( 'typography', 'fontSizes' ), - array( 'typography', 'fontFamilies' ), - array( 'custom' ), - ); - - // 'defaults' settings become top-level. - if ( isset( $settings[ self::V0_ALL_BLOCKS_NAME ] ) ) { - $new = $settings[ self::V0_ALL_BLOCKS_NAME ]; - unset( $settings[ self::V0_ALL_BLOCKS_NAME ] ); - } - - // 'root' settings override 'defaults'. - if ( isset( $settings[ self::V0_ROOT_BLOCK_NAME ] ) ) { - $new = array_replace_recursive( $new, $settings[ self::V0_ROOT_BLOCK_NAME ] ); - - // The array_replace_recursive algorithm merges at the leaf level. - // This means that when a leaf value is an array, - // the incoming array won't replace the existing, - // but the numeric indexes are used for replacement. - // - // These cases we hold into $paths_to_override - // and need to replace them with the new data. - foreach ( $paths_to_override as $path ) { - $root_value = _wp_array_get( - $settings, - array_merge( array( self::V0_ROOT_BLOCK_NAME ), $path ), - null - ); - if ( null !== $root_value ) { - _wp_array_set( $new, $path, $root_value ); - } - } - - unset( $settings[ self::V0_ROOT_BLOCK_NAME ] ); - } - - if ( empty( $settings ) ) { - return $new; - } - - /* - * At this point, it only contains block's data. - * However, some block data we need to consolidate - * into a different section, as it's the case for: - * - * - core/heading/h1, core/heading/h2, ... => core/heading - * - core/post-title/h1, core/post-title/h2, ... => core/post-title - * - core/query-title/h1, core/query-title/h2, ... => core/query-title - * - */ - $new['blocks'] = $settings; - foreach ( $blocks_to_consolidate as $old_name => $new_name ) { - // Unset the $old_name. - unset( $new[ $old_name ] ); - - // Consolidate the $new value. - $block_settings = _wp_array_get( $settings, array( $old_name ), null ); - if ( null !== $block_settings ) { - $new_path = array( 'blocks', $new_name ); - $new_settings = array(); - _wp_array_set( $new_settings, $new_path, $block_settings ); - - $new = array_replace_recursive( $new, $new_settings ); - foreach ( $paths_to_override as $path ) { - $block_value = _wp_array_get( $block_settings, $path, null ); - if ( null !== $block_value ) { - _wp_array_set( $new, array_merge( $new_path, $path ), $block_value ); - } - } - } - } - - return $new; - } - - /** - * Processes the styles subtree. - * - * @param array $styles Array to process. - * - * @return array The styles in the new format. - */ - private static function migrate_v0_to_v1_process_styles( $styles ) { - $new = array(); - $block_heading = array( - 'core/heading/h1' => 'h1', - 'core/heading/h2' => 'h2', - 'core/heading/h3' => 'h3', - 'core/heading/h4' => 'h4', - 'core/heading/h5' => 'h5', - 'core/heading/h6' => 'h6', - ); - $blocks_to_consolidate = array( - 'core/post-title/h1' => 'core/post-title', - 'core/post-title/h2' => 'core/post-title', - 'core/post-title/h3' => 'core/post-title', - 'core/post-title/h4' => 'core/post-title', - 'core/post-title/h5' => 'core/post-title', - 'core/post-title/h6' => 'core/post-title', - 'core/query-title/h1' => 'core/query-title', - 'core/query-title/h2' => 'core/query-title', - 'core/query-title/h3' => 'core/query-title', - 'core/query-title/h4' => 'core/query-title', - 'core/query-title/h5' => 'core/query-title', - 'core/query-title/h6' => 'core/query-title', - ); - - // Styles within root become top-level. - if ( isset( $styles[ self::V0_ROOT_BLOCK_NAME ] ) ) { - $new = $styles[ self::V0_ROOT_BLOCK_NAME ]; - unset( $styles[ self::V0_ROOT_BLOCK_NAME ] ); - - // Transform root.styles.color.link into elements.link.color.text. - if ( isset( $new['color']['link'] ) ) { - $new['elements']['link']['color']['text'] = $new['color']['link']; - unset( $new['color']['link'] ); - } - } - - if ( empty( $styles ) ) { - return $new; - } - - /* - * At this point, it only contains block's data. - * However, we still need to consolidate a few things: - * - * - link element => transform from link color property - * - heading elements => consolidate multiple blocks (core/heading/h1, core/heading/h2) - * into a single one (core/heading). - */ - $new['blocks'] = $styles; - - // link elements. - foreach ( $new['blocks'] as $block_name => $metadata ) { - if ( isset( $metadata['color']['link'] ) ) { - $new['blocks'][ $block_name ]['elements']['link']['color']['text'] = $metadata['color']['link']; - unset( $new['blocks'][ $block_name ]['color']['link'] ); - } - } - - /* - * The heading block needs a special treatment: - * - * - if it has a link color => it needs to be moved to the blocks.core/heading - * - the rest is consolidated into the corresponding element - * - */ - foreach ( $block_heading as $old_name => $new_name ) { - if ( ! isset( $new['blocks'][ $old_name ] ) ) { - continue; - } - - _wp_array_set( $new, array( 'elements', $new_name ), $new['blocks'][ $old_name ] ); - - if ( isset( $new['blocks'][ $old_name ]['elements'] ) ) { - _wp_array_set( $new, array( 'blocks', 'core/heading', 'elements' ), $new['blocks'][ $old_name ]['elements'] ); - } - - unset( $new['blocks'][ $old_name ] ); - - } - - /* - * Port the styles from the old blocks to the new, - * overriding the previous values. - */ - foreach ( $blocks_to_consolidate as $old_name => $new_name ) { - if ( ! isset( $new['blocks'][ $old_name ] ) ) { - continue; - } - - _wp_array_set( $new, array( 'blocks', $new_name ), $new['blocks'][ $old_name ] ); - unset( $new['blocks'][ $old_name ] ); - - } - - return $new; - } - - /** - * Removes the custom prefixes for a few properties that only worked in the plugin: - * - * 'border.customColor' => 'border.color', - * 'border.customStyle' => 'border.style', - * 'border.customWidth' => 'border.width', - * 'typography.customFontStyle' => 'typography.fontStyle', - * 'typography.customFontWeight' => 'typography.fontWeight', - * 'typography.customLetterSpacing' => 'typography.letterSpacing', - * 'typography.customTextDecorations' => 'typography.textDecoration', - * 'typography.customTextTransforms' => 'typography.textTransform', - * - * @param array $old Data to migrate. - * - * @return array Data without the custom prefixes. - */ - private static function migrate_v1_remove_custom_prefixes( $old ) { - // Copy everything. - $new = $old; - - // Overwrite the things that change. - if ( isset( $old['settings'] ) ) { - $new['settings'] = self::rename_paths( $old['settings'], self::V1_RENAMED_PATHS ); - } - - return $new; - } - - /** - * Removes the custom prefixes for a few properties - * that were part of v1: - * - * 'border.customRadius' => 'border.radius', - * 'spacing.customMargin' => 'spacing.margin', - * 'spacing.customPadding' => 'spacing.padding', - * 'typography.customLineHeight' => 'typography.lineHeight', - * - * @param array $old Data to migrate. - * - * @return array Data without the custom prefixes. - */ - private static function migrate_v1_to_v2( $old ) { - // Copy everything. - $new = $old; - - // Overwrite the things that changed. - if ( isset( $old['settings'] ) ) { - $new['settings'] = self::rename_paths( $old['settings'], self::V1_TO_V2_RENAMED_PATHS ); - } - - // Set the new version. - $new['version'] = 2; - - return $new; - } - - /** - * Processes the settings subtree. - * - * @param array $settings Array to process. - * @param array $paths_to_rename Paths to rename. - * - * @return array The settings in the new format. - */ - private static function rename_paths( $settings, $paths_to_rename ) { - $new_settings = $settings; - - // Process any renamed/moved paths within default settings. - self::rename_settings( $new_settings, $paths_to_rename ); - - // Process individual block settings. - if ( isset( $new_settings['blocks'] ) && is_array( $new_settings['blocks'] ) ) { - foreach ( $new_settings['blocks'] as &$block_settings ) { - self::rename_settings( $block_settings, $paths_to_rename ); - } - } - - return $new_settings; - } - - /** - * Processes a settings array, renaming or moving properties. - * - * @param array $settings Reference to settings either defaults or an individual block's. - * @param array $paths_to_rename Paths to rename. - */ - private static function rename_settings( &$settings, $paths_to_rename ) { - foreach ( $paths_to_rename as $original => $renamed ) { - $original_path = explode( '.', $original ); - $renamed_path = explode( '.', $renamed ); - $current_value = _wp_array_get( $settings, $original_path, null ); - - if ( null !== $current_value ) { - _wp_array_set( $settings, $renamed_path, $current_value ); - self::unset_setting_by_path( $settings, $original_path ); - } - } - } - - /** - * Removes a property from within the provided settings by its path. - * - * @param array $settings Reference to the current settings array. - * @param array $path Path to the property to be removed. - * - * @return void - */ - private static function unset_setting_by_path( &$settings, $path ) { - $tmp_settings = &$settings; // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable - $last_key = array_pop( $path ); - foreach ( $path as $key ) { - $tmp_settings = &$tmp_settings[ $key ]; - } - - unset( $tmp_settings[ $last_key ] ); - } -} diff --git a/lib/compat/wordpress-5.9/default-editor-styles.php b/lib/compat/wordpress-5.9/default-editor-styles.php deleted file mode 100644 index ff796b53c02a7..0000000000000 --- a/lib/compat/wordpress-5.9/default-editor-styles.php +++ /dev/null @@ -1,41 +0,0 @@ -<?php -/** - * Loads the default editor styles. - * - * @package gutenberg - */ - -/** - * Load the default editor styles. - * These styles are used if the "no theme styles" options is triggered - * or on themes without their own editor styles. - * - * @param array $settings Default editor settings. - * - * @return array Filtered editor settings. - */ -function gutenberg_extend_block_editor_settings_with_default_editor_styles( $settings ) { - $default_editor_styles_file = gutenberg_dir_path() . 'build/block-editor/default-editor-styles.css'; - $settings['defaultEditorStyles'] = array( - array( - 'css' => file_get_contents( $default_editor_styles_file ), - ), - ); - - // Remove the default font addition from Core Code. - $styles_without_core_styles = array(); - if ( isset( $settings['styles'] ) ) { - foreach ( $settings['styles'] as $style ) { - if ( - ! isset( $style['__unstableType'] ) || - 'core' !== $style['__unstableType'] - ) { - $styles_without_core_styles[] = $style; - } - } - } - $settings['styles'] = $styles_without_core_styles; - - return $settings; -} -add_filter( 'block_editor_settings_all', 'gutenberg_extend_block_editor_settings_with_default_editor_styles' ); diff --git a/lib/compat/wordpress-5.9/default-theme-supports.php b/lib/compat/wordpress-5.9/default-theme-supports.php deleted file mode 100644 index 0c204b46d0a5e..0000000000000 --- a/lib/compat/wordpress-5.9/default-theme-supports.php +++ /dev/null @@ -1,23 +0,0 @@ -<?php -/** - * Loads default theme supports for block themes. - * - * @package gutenberg - */ - -if ( wp_is_block_theme() ) { - add_theme_support( 'post-thumbnails' ); - add_theme_support( 'responsive-embeds' ); - add_theme_support( 'editor-styles' ); - add_theme_support( - 'html5', - array( - 'style', - 'script', - 'comment-form', - 'comment-list', - ) - ); - add_theme_support( 'automatic-feed-links' ); - add_filter( 'should_load_separate_core_block_assets', '__return_true' ); -} diff --git a/lib/compat/wordpress-5.9/edit-site-page.php b/lib/compat/wordpress-5.9/edit-site-page.php deleted file mode 100644 index 2663eba8f1724..0000000000000 --- a/lib/compat/wordpress-5.9/edit-site-page.php +++ /dev/null @@ -1,240 +0,0 @@ -<?php -/** - * Bootstrapping the Gutenberg Edit Site Page. - * - * @package gutenberg - */ - -/** - * The main entry point for the Gutenberg Edit Site Page. - * - * @since 7.2.0 - */ -function gutenberg_edit_site_page() { - ?> - <div - id="edit-site-editor" - class="edit-site" - > - </div> - <?php -} - -/** - * Checks whether the provided page is one of allowed Site Editor pages. - * - * @param string $page Page to check. - * - * @return bool True for Site Editor pages, false otherwise. - */ -function gutenberg_is_edit_site_page( $page ) { - return 'appearance_page_gutenberg-edit-site' === $page; -} - -/** - * Checks whether the provided page is the templates list page. - * - * @return bool True for Site Editor pages, false otherwise. - */ -function gutenberg_is_edit_site_list_page() { - return isset( $_GET['postType'] ) && ! isset( $_GET['postId'] ); -} - -/** - * Load editor styles (this is copied from edit-form-blocks.php). - * Ideally the code is extracted into a reusable function. - * - * @return array Editor Styles Setting. - */ -function gutenberg_get_editor_styles() { - global $editor_styles; - - // Ideally the code is extracted into a reusable function. - $styles = array(); - - if ( $editor_styles && current_theme_supports( 'editor-styles' ) ) { - foreach ( $editor_styles as $style ) { - if ( preg_match( '~^(https?:)?//~', $style ) ) { - $response = wp_remote_get( $style ); - if ( ! is_wp_error( $response ) ) { - $styles[] = array( - 'css' => wp_remote_retrieve_body( $response ), - ); - } - } else { - $file = get_theme_file_path( $style ); - if ( is_file( $file ) ) { - $styles[] = array( - 'css' => file_get_contents( $file ), - 'baseURL' => get_theme_file_uri( $style ), - ); - } - } - } - } - - return $styles; -} - -/** - * Initialize the Gutenberg Site Editor. - * - * @since 7.2.0 - * - * @param string $hook Page. - */ -function gutenberg_edit_site_init( $hook ) { - global $current_screen, $post, $editor_styles; - - if ( ! gutenberg_is_edit_site_page( $hook ) ) { - return; - } - - if ( gutenberg_is_edit_site_list_page() ) { - $post_type = get_post_type_object( $_GET['postType'] ); - - if ( ! $post_type ) { - wp_die( __( 'Invalid post type.', 'gutenberg' ) ); - } - } - - // Default to is-fullscreen-mode to avoid rendering wp-admin navigation menu while loading and - // having jumps in the UI. - add_filter( - 'admin_body_class', - static function( $classes ) { - return "$classes is-fullscreen-mode"; - } - ); - - $indexed_template_types = array(); - foreach ( get_default_block_template_types() as $slug => $template_type ) { - $template_type['slug'] = (string) $slug; - $indexed_template_types[] = $template_type; - } - - $custom_settings = array( - 'siteUrl' => site_url(), - 'postsPerPage' => get_option( 'posts_per_page' ), - 'styles' => gutenberg_get_editor_styles(), - 'defaultTemplateTypes' => $indexed_template_types, - 'defaultTemplatePartAreas' => get_allowed_block_template_part_areas(), - '__unstableHomeTemplate' => gutenberg_resolve_home_template(), - ); - - // Add additional back-compat patterns registered by `current_screen` et al. - $custom_settings['__experimentalAdditionalBlockPatterns'] = WP_Block_Patterns_Registry::get_instance()->get_all_registered( true ); - $custom_settings['__experimentalAdditionalBlockPatternCategories'] = WP_Block_Pattern_Categories_Registry::get_instance()->get_all_registered( true ); - - /** - * Make the WP Screen object aware that this is a block editor page. - * Since custom blocks check whether the screen is_block_editor, - * this is required for custom blocks to be loaded. - * See wp_enqueue_registered_block_scripts_and_styles in wp-includes/script-loader.php - */ - $current_screen->is_block_editor( true ); - - $site_editor_context = new WP_Block_Editor_Context( array( 'name' => 'core/edit-site' ) ); - $settings = get_block_editor_settings( $custom_settings, $site_editor_context ); - $active_global_styles_id = WP_Theme_JSON_Resolver_Gutenberg::get_user_global_styles_post_id(); - $active_theme = wp_get_theme()->get_stylesheet(); - gutenberg_initialize_editor( - 'edit_site_editor', - 'edit-site', - array( - 'preload_paths' => array_merge( - array( - array( '/wp/v2/media', 'OPTIONS' ), - '/wp/v2/types?context=view', - '/wp/v2/types/wp_template?context=edit', - '/wp/v2/types/wp_template-part?context=edit', - '/wp/v2/taxonomies?context=view', - '/wp/v2/pages?context=edit', - '/wp/v2/categories?context=edit', - '/wp/v2/posts?context=edit', - '/wp/v2/tags?context=edit', - '/wp/v2/templates?context=edit&per_page=-1', - '/wp/v2/template-parts?context=edit&per_page=-1', - '/wp/v2/settings', - '/wp/v2/themes?context=edit&status=active', - '/wp/v2/global-styles/' . $active_global_styles_id . '?context=edit', - '/wp/v2/global-styles/' . $active_global_styles_id, - '/wp/v2/global-styles/themes/' . $active_theme, - ) - ), - 'initializer_name' => 'initializeEditor', - 'editor_settings' => $settings, - ) - ); - - wp_add_inline_script( - 'wp-blocks', - sprintf( 'wp.blocks.setCategories( %s );', wp_json_encode( get_block_categories( $post ) ) ), - 'after' - ); - - wp_enqueue_script( 'wp-edit-site' ); - wp_enqueue_script( 'wp-format-library' ); - wp_enqueue_style( 'wp-edit-site' ); - wp_enqueue_style( 'wp-format-library' ); - wp_enqueue_media(); - - if ( - current_theme_supports( 'wp-block-styles' ) || - ( ! is_array( $editor_styles ) || count( $editor_styles ) === 0 ) - ) { - wp_enqueue_style( 'wp-block-library-theme' ); - } - - /** - * Fires after block assets have been enqueued for the editing interface. - * - * Call `add_action` on any hook before 'admin_enqueue_scripts'. - * - * In the function call you supply, simply use `wp_enqueue_script` and - * `wp_enqueue_style` to add your functionality to the block editor. - * - * @since 5.0.0 - */ - - do_action( 'enqueue_block_editor_assets' ); - -} -add_action( 'admin_enqueue_scripts', 'gutenberg_edit_site_init' ); - -/** - * Tells the script loader to load the scripts and styles of custom block on site editor screen. - * - * @param bool $is_block_editor_screen Current decision about loading block assets. - * @return bool Filtered decision about loading block assets. - */ -function gutenberg_site_editor_load_block_editor_scripts_and_styles( $is_block_editor_screen ) { - return ( is_callable( 'get_current_screen' ) && get_current_screen() && 'appearance_page_gutenberg-edit-site' === get_current_screen()->base ) - ? true - : $is_block_editor_screen; -} -add_filter( 'should_load_block_editor_scripts_and_styles', 'gutenberg_site_editor_load_block_editor_scripts_and_styles' ); - -/** - * Do a server-side redirection if missing `postType` and `postId` - * query args when visiting site editor. - * - * Note: The `site-editor.php` should handle redirection when migrated into the WP core. - * - * @return void - */ -function gutenberg_maybe_redirect_to_homepage() { - if ( empty( $_GET['postType'] ) && empty( $_GET['postId'] ) ) { - $template = gutenberg_resolve_home_template(); - if ( ! $template ) { - return; - } - - $redirect_url = add_query_arg( - $template, - admin_url( 'themes.php?page=gutenberg-edit-site' ) - ); - wp_safe_redirect( $redirect_url ); - } -} -add_action( 'load-appearance_page_gutenberg-edit-site', 'gutenberg_maybe_redirect_to_homepage' ); diff --git a/lib/compat/wordpress-5.9/global-styles-css-custom-properties.php b/lib/compat/wordpress-5.9/global-styles-css-custom-properties.php deleted file mode 100644 index bcc30c1411700..0000000000000 --- a/lib/compat/wordpress-5.9/global-styles-css-custom-properties.php +++ /dev/null @@ -1,20 +0,0 @@ -<?php -/** - * Loads the CSS Custom Properties in use by Global Styles. - * - * @package Gutenberg - */ - -// This has been ported as part of default-filters.php. -if ( ! function_exists( 'wp_enqueue_global_styles_css_custom_properties' ) ) { - /** - * Function to enqueue the CSS Custom Properties - * coming from theme.json. - */ - function wp_enqueue_global_styles_css_custom_properties() { - wp_register_style( 'global-styles-css-custom-properties', false, array(), true, true ); - wp_add_inline_style( 'global-styles-css-custom-properties', gutenberg_get_global_stylesheet( array( 'variables' ) ) ); - wp_enqueue_style( 'global-styles-css-custom-properties' ); - } - add_filter( 'enqueue_block_editor_assets', 'wp_enqueue_global_styles_css_custom_properties' ); -} diff --git a/lib/compat/wordpress-5.9/json-file-decode.php b/lib/compat/wordpress-5.9/json-file-decode.php deleted file mode 100644 index 8f7f897227a07..0000000000000 --- a/lib/compat/wordpress-5.9/json-file-decode.php +++ /dev/null @@ -1,54 +0,0 @@ -<?php -/** - * Function to read json files. - * - * @package gutenberg - */ - -if ( ! function_exists( 'wp_json_file_decode' ) ) { - /** - * Reads and decodes a JSON file. - * - * @param string $filename Path to the JSON file. - * @param array $options { - * Optional. Options to be used with `json_decode()`. - * - * @type bool associative Optional. When `true`, JSON objects will be returned as associative arrays. - * When `false`, JSON objects will be returned as objects. - * } - * - * @return mixed Returns the value encoded in JSON in appropriate PHP type. - * `null` is returned if the file is not found, or its content can't be decoded. - */ - function wp_json_file_decode( $filename, $options = array() ) { - $result = null; - $filename = wp_normalize_path( realpath( $filename ) ); - if ( ! file_exists( $filename ) ) { - trigger_error( - sprintf( - /* translators: %s: Path to the JSON file. */ - __( "File %s doesn't exist!", 'gutenberg' ), - $filename - ) - ); - return $result; - } - - $options = wp_parse_args( $options, array( 'associative' => false ) ); - $decoded_file = json_decode( file_get_contents( $filename ), $options['associative'] ); - - if ( JSON_ERROR_NONE !== json_last_error() ) { - trigger_error( - sprintf( - /* translators: 1: Path to the JSON file, 2: Error message. */ - __( 'Error when decoding a JSON file at path %1$s: %2$s', 'gutenberg' ), - $filename, - json_last_error_msg() - ) - ); - return $result; - } - - return $decoded_file; - } -} diff --git a/lib/compat/wordpress-5.9/kses.php b/lib/compat/wordpress-5.9/kses.php deleted file mode 100644 index 648718d6facd2..0000000000000 --- a/lib/compat/wordpress-5.9/kses.php +++ /dev/null @@ -1,82 +0,0 @@ -<?php -/** - * Filters to adjust the KSES behavior. - * - * @package gutenberg - */ - -/** - * Sanitizes global styles user content removing unsafe rules. - * - * @param string $content Post content to filter. - * @return string Filtered post content with unsafe rules removed. - */ -function gutenberg_global_styles_filter_post( $content ) { - $decoded_data = json_decode( wp_unslash( $content ), true ); - $json_decoding_error = json_last_error(); - if ( - JSON_ERROR_NONE === $json_decoding_error && - is_array( $decoded_data ) && - isset( $decoded_data['isGlobalStylesUserThemeJSON'] ) && - $decoded_data['isGlobalStylesUserThemeJSON'] - ) { - unset( $decoded_data['isGlobalStylesUserThemeJSON'] ); - - $data_to_encode = WP_Theme_JSON_Gutenberg::remove_insecure_properties( $decoded_data ); - - $data_to_encode['isGlobalStylesUserThemeJSON'] = true; - return wp_slash( wp_json_encode( $data_to_encode ) ); - } - return $content; -} - -/** - * Adds the filters to filter global styles user theme.json. - */ -function gutenberg_global_styles_kses_init_filters() { - // Global Styles filters should be executed before normal post_kses HTML filters which has the default priority of 10. - add_filter( 'content_save_pre', 'gutenberg_global_styles_filter_post', 9 ); -} - -/** - * Removes the filters to filter global styles user theme.json. - */ -function gutenberg_global_styles_kses_remove_filters() { - remove_filter( 'content_save_pre', 'gutenberg_global_styles_filter_post', 9 ); -} - -/** - * Register global styles kses filters if the user does not have unfiltered_html capability. - * - * @uses render_block_core_navigation() - * @throws WP_Error An WP_Error exception parsing the block definition. - */ -function gutenberg_global_styles_kses_init() { - gutenberg_global_styles_kses_remove_filters(); - if ( ! current_user_can( 'unfiltered_html' ) ) { - gutenberg_global_styles_kses_init_filters(); - } -} - -/** - * This filter is the last being executed on force_filtered_html_on_import. - * If the input of the filter is true it means we are in an import situation and should - * enable kses, independently of the user capabilities. - * So in that case we call gutenberg_global_styles_kses_init_filters; - * - * @param string $arg Input argument of the filter. - * @return string Exactly what was passed as argument. - */ -function gutenberg_global_styles_force_filtered_html_on_import_filter( $arg ) { - // force_filtered_html_on_import is true we need to init the global styles kses filters. - if ( $arg ) { - gutenberg_global_styles_kses_init_filters(); - } - return $arg; -} - -// kses actions&filters. -add_action( 'init', 'gutenberg_global_styles_kses_init' ); -add_action( 'set_current_user', 'gutenberg_global_styles_kses_init' ); -add_filter( 'force_filtered_html_on_import', 'gutenberg_global_styles_force_filtered_html_on_import_filter', 999 ); -// This filter needs to be executed last. diff --git a/lib/compat/wordpress-5.9/move-theme-editor-menu-item.php b/lib/compat/wordpress-5.9/move-theme-editor-menu-item.php deleted file mode 100644 index 65be09ae737a7..0000000000000 --- a/lib/compat/wordpress-5.9/move-theme-editor-menu-item.php +++ /dev/null @@ -1,24 +0,0 @@ -<?php -/** - * Moves the theme editor menu items for FSE themes. - * - * @package gutenberg - */ - -/* - * If wp_list_users is defined, it means the plugin - * is running on WordPress 5.9, so no need to change menu location. - */ -if ( ! function_exists( 'wp_list_users' ) ) { - /** - * Moves the "theme editor" under "tools" in block themes. - */ - function gutenberg_move_theme_editor_in_block_themes() { - if ( ! wp_is_block_theme() || is_multisite() ) { - return; - } - remove_submenu_page( 'themes.php', 'theme-editor.php' ); - add_submenu_page( 'tools.php', __( 'Theme File Editor', 'gutenberg' ), __( 'Theme File Editor', 'gutenberg' ), 'edit_themes', 'theme-editor.php' ); - } - add_action( 'admin_menu', 'gutenberg_move_theme_editor_in_block_themes', 102 ); -} diff --git a/lib/compat/wordpress-5.9/navigation.php b/lib/compat/wordpress-5.9/navigation.php deleted file mode 100644 index 017bb63a1e126..0000000000000 --- a/lib/compat/wordpress-5.9/navigation.php +++ /dev/null @@ -1,243 +0,0 @@ -<?php -/** - * Functions used in making nav menus interopable with block editors. - * - * @package gutenberg - */ - -/** - * Registers block editor 'wp_navigation' post type. - */ -function gutenberg_register_navigation_post_type() { - $labels = array( - 'name' => __( 'Navigation Menus', 'gutenberg' ), - 'singular_name' => __( 'Navigation Menu', 'gutenberg' ), - 'menu_name' => _x( 'Navigation Menus', 'Admin Menu text', 'gutenberg' ), - 'add_new' => _x( 'Add New', 'Navigation Menu', 'gutenberg' ), - 'add_new_item' => __( 'Add New Navigation Menu', 'gutenberg' ), - 'new_item' => __( 'New Navigation Menu', 'gutenberg' ), - 'edit_item' => __( 'Edit Navigation Menu', 'gutenberg' ), - 'view_item' => __( 'View Navigation Menu', 'gutenberg' ), - 'all_items' => __( 'All Navigation Menus', 'gutenberg' ), - 'search_items' => __( 'Search Navigation Menus', 'gutenberg' ), - 'parent_item_colon' => __( 'Parent Navigation Menu:', 'gutenberg' ), - 'not_found' => __( 'No Navigation Menu found.', 'gutenberg' ), - 'not_found_in_trash' => __( 'No Navigation Menu found in Trash.', 'gutenberg' ), - 'archives' => __( 'Navigation Menu archives', 'gutenberg' ), - 'insert_into_item' => __( 'Insert into Navigation Menu', 'gutenberg' ), - 'uploaded_to_this_item' => __( 'Uploaded to this Navigation Menu', 'gutenberg' ), - // Some of these are a bit weird, what are they for? - 'filter_items_list' => __( 'Filter Navigation Menu list', 'gutenberg' ), - 'items_list_navigation' => __( 'Navigation Menus list navigation', 'gutenberg' ), - 'items_list' => __( 'Navigation Menus list', 'gutenberg' ), - ); - - $args = array( - 'labels' => $labels, - 'description' => __( 'Navigation menus.', 'gutenberg' ), - 'public' => false, - 'has_archive' => false, - 'show_ui' => true, - 'show_in_menu' => false, - 'show_in_admin_bar' => false, - 'show_in_rest' => true, - 'map_meta_cap' => true, - 'rest_base' => 'navigation', - 'rest_controller_class' => WP_REST_Posts_Controller::class, - 'supports' => array( - 'title', - 'editor', - 'revisions', - ), - 'capabilities' => array( - 'edit_others_posts' => 'edit_theme_options', - 'delete_posts' => 'edit_theme_options', - 'publish_posts' => 'edit_theme_options', - 'create_posts' => 'edit_theme_options', - 'read_private_posts' => 'edit_theme_options', - 'delete_private_posts' => 'edit_theme_options', - 'delete_published_posts' => 'edit_theme_options', - 'delete_others_posts' => 'edit_theme_options', - 'edit_private_posts' => 'edit_theme_options', - 'edit_published_posts' => 'edit_theme_options', - ), - ); - - register_post_type( 'wp_navigation', $args ); -} -add_action( 'init', 'gutenberg_register_navigation_post_type' ); - -/** - * Disable "Post Attributes" for wp_navigation post type. - * - * The attributes are also conditionally enabled when a site has custom templates. - * Block Theme templates can be available for every post type. - */ -add_filter( 'theme_wp_navigation_templates', '__return_empty_array' ); - -/** - * Disable block editor for wp_navigation type posts so they can be managed via the UI. - * - * @param bool $value Whether the CPT supports block editor or not. - * @param string $post_type Post type. - * - * @return bool - */ -function gutenberg_disable_block_editor_for_navigation_post_type( $value, $post_type ) { - if ( 'wp_navigation' === $post_type ) { - return false; - } - - return $value; -} - -add_filter( 'use_block_editor_for_post_type', 'gutenberg_disable_block_editor_for_navigation_post_type', 10, 2 ); - -/** - * This callback disables the content editor for wp_navigation type posts. - * Content editor cannot handle wp_navigation type posts correctly. - * We cannot disable the "editor" feature in the wp_navigation's CPT definition - * because it disables the ability to save navigation blocks via REST API. - * - * @param WP_Post $post An instance of WP_Post class. - */ -function gutenberg_disable_content_editor_for_navigation_post_type( $post ) { - $post_type = get_post_type( $post ); - if ( 'wp_navigation' !== $post_type ) { - return; - } - - remove_post_type_support( $post_type, 'editor' ); -} - -add_action( 'edit_form_after_title', 'gutenberg_disable_content_editor_for_navigation_post_type', 10, 1 ); - -/** - * This callback enables content editor for wp_navigation type posts. - * We need to enable it back because we disable it to hide - * the content editor for wp_navigation type posts. - * - * @see gutenberg_disable_content_editor_for_navigation_post_type - * - * @param WP_Post $post An instance of WP_Post class. - */ -function gutenberg_enable_content_editor_for_navigation_post_type( $post ) { - $post_type = get_post_type( $post ); - if ( 'wp_navigation' !== $post_type ) { - return; - } - - add_post_type_support( $post_type, 'editor' ); -} - -add_action( 'edit_form_after_editor', 'gutenberg_enable_content_editor_for_navigation_post_type', 10, 1 ); - -/** - * Rename the menu title from "All Navigation Menus" to "Navigation Menus". - */ -function gutenberg_rename_navigation_post_type_admin_menu_entry() { - global $submenu; - if ( ! isset( $submenu['themes.php'] ) ) { - return; - } - - $post_type = get_post_type_object( 'wp_navigation' ); - if ( ! $post_type ) { - return; - } - - $menu_title_index = 0; - foreach ( $submenu['themes.php'] as $key => $menu_item ) { - if ( $post_type->labels->all_items === $menu_item[ $menu_title_index ] ) { - $submenu['themes.php'][ $key ][ $menu_title_index ] = $post_type->labels->menu_name; // phpcs:ignore WordPress.WP.GlobalVariablesOverride - return; - } - } -} - -add_action( 'admin_menu', 'gutenberg_rename_navigation_post_type_admin_menu_entry' ); - -/** - * Registers the navigation areas supported by the current theme. The expected - * shape of the argument is: - * array( - * 'primary' => 'Primary', - * 'secondary' => 'Secondary', - * 'tertiary' => 'Tertiary', - * ) - * - * @param array $new_areas Supported navigation areas. - */ -function gutenberg_register_navigation_areas( $new_areas ) { - global $gutenberg_navigation_areas; - $gutenberg_navigation_areas = $new_areas; -} - -// Register the default navigation areas. -gutenberg_register_navigation_areas( - array( - 'primary' => 'Primary', - 'secondary' => 'Secondary', - 'tertiary' => 'Tertiary', - ) -); - -/** - * Returns the available navigation areas. - * - * @return array Registered navigation areas. - */ -function gutenberg_get_navigation_areas() { - global $gutenberg_navigation_areas; - return $gutenberg_navigation_areas; -} - -/** - * Retrieves navigation areas. - * - * @return array Navigation areas. - */ -function gutenberg_get_navigation_areas_menus() { - $areas = get_option( 'wp_navigation_areas', array() ); - if ( ! $areas ) { - // Original key used `fse` prefix but Core options should use `wp`. - // We fallback to the legacy option to catch sites with values in the - // original location. - $legacy_option_key = 'fse_navigation_areas'; - $areas = get_option( $legacy_option_key, array() ); - } - return $areas; -} - -/** - * Shim that hides ability to edit visibility and status for wp_navigation type posts. - * When merged to Core, the CSS below should be moved to wp-admin/css/edit.css. - * - * This shim can be removed when the Gutenberg plugin requires a WordPress - * version that has the ticket below. - * - * @see https://core.trac.wordpress.org/ticket/54407 - * - * @param string $hook The current admin page. - */ -function gutenberg_hide_visibility_and_status_for_navigation_posts( $hook ) { - $allowed_hooks = array( 'post.php', 'post-new.php' ); - if ( ! in_array( $hook, $allowed_hooks, true ) ) { - return; - } - - /** - * HACK: We're hiding the description field using CSS because this - * cannot be done using a filter or an action. - */ - - $css = <<<CSS - body.post-type-wp_navigation div#minor-publishing { - display: none; - } -CSS; - - wp_add_inline_style( 'common', $css ); -} - -add_action( 'admin_enqueue_scripts', 'gutenberg_hide_visibility_and_status_for_navigation_posts' ); diff --git a/lib/compat/wordpress-5.9/polyfills.php b/lib/compat/wordpress-5.9/polyfills.php deleted file mode 100644 index 30078b7e1a052..0000000000000 --- a/lib/compat/wordpress-5.9/polyfills.php +++ /dev/null @@ -1,70 +0,0 @@ -<?php -/** - * Polyfills for functions missing from older WP versions. - * - * This file should be removed when WordPress 5.9.0 becomes the lowest - * supported version by this plugin. - * - * @package gutenberg - */ - -if ( ! function_exists( 'str_contains' ) ) { - /** - * Polyfill for `str_contains()` function added in PHP 8.0. - * - * Performs a case-sensitive check indicating if needle is - * contained in haystack. - * - * @since 5.9.0 - * - * @param string $haystack The string to search in. - * @param string $needle The substring to search for in the haystack. - * @return bool True if `$needle` is in `$haystack`, otherwise false. - */ - function str_contains( $haystack, $needle ) { - return ( '' === $needle || false !== strpos( $haystack, $needle ) ); - } -} - -if ( ! function_exists( 'str_starts_with' ) ) { - /** - * Polyfill for `str_starts_with()` function added in PHP 8.0. - * - * Performs a case-sensitive check indicating if - * the haystack begins with needle. - * - * @since 5.9.0 - * - * @param string $haystack The string to search in. - * @param string $needle The substring to search for in the `$haystack`. - * @return bool True if `$haystack` starts with `$needle`, otherwise false. - */ - function str_starts_with( $haystack, $needle ) { - if ( '' === $needle ) { - return true; - } - return 0 === strpos( $haystack, $needle ); - } -} - -if ( ! function_exists( 'str_ends_with' ) ) { - /** - * Polyfill for `str_ends_with()` function added in PHP 8.0. - * - * Performs a case-sensitive check indicating if - * the haystack ends with needle. - * - * @since 5.9.0 - * - * @param string $haystack The string to search in. - * @param string $needle The substring to search for in the `$haystack`. - * @return bool True if `$haystack` ends with `$needle`, otherwise false. - */ - function str_ends_with( $haystack, $needle ) { - if ( '' === $haystack && '' !== $needle ) { - return false; - } - $len = strlen( $needle ); - return 0 === substr_compare( $haystack, $needle, -$len, $len ); - } -} diff --git a/lib/compat/wordpress-5.9/register-global-styles-cpt.php b/lib/compat/wordpress-5.9/register-global-styles-cpt.php deleted file mode 100644 index 0a18bc547fe62..0000000000000 --- a/lib/compat/wordpress-5.9/register-global-styles-cpt.php +++ /dev/null @@ -1,49 +0,0 @@ -<?php -/** - * Function to register the Custom Post Type - * to be used to store user's config. - * - * @package gutenberg - */ - -/* - * If wp_get_global_settings is defined, it means the plugin - * is running on WordPress 5.9, so don't need to register the CPT - * as it was already done by WordPress core. - */ -if ( ! function_exists( 'wp_get_global_settings' ) ) { - - /** - * Registers a Custom Post Type to store the user's origin config. - * - * This has been ported to src/wp-includes/post.php - */ - function register_global_styles_custom_post_type() { - $args = array( - 'label' => __( 'Global Styles', 'gutenberg' ), - 'description' => 'Global styles to include in themes.', - 'public' => false, - 'show_ui' => false, - 'show_in_rest' => false, - 'rewrite' => false, - 'capabilities' => array( - 'read' => 'edit_theme_options', - 'create_posts' => 'edit_theme_options', - 'edit_posts' => 'edit_theme_options', - 'edit_published_posts' => 'edit_theme_options', - 'delete_published_posts' => 'edit_theme_options', - 'edit_others_posts' => 'edit_theme_options', - 'delete_others_posts' => 'edit_theme_options', - ), - 'map_meta_cap' => true, - 'supports' => array( - 'title', - 'editor', - 'revisions', - ), - ); - register_post_type( 'wp_global_styles', $args ); - } - - add_action( 'init', 'register_global_styles_custom_post_type' ); -} diff --git a/lib/compat/wordpress-5.9/rest-active-global-styles.php b/lib/compat/wordpress-5.9/rest-active-global-styles.php deleted file mode 100644 index af7f789110f66..0000000000000 --- a/lib/compat/wordpress-5.9/rest-active-global-styles.php +++ /dev/null @@ -1,33 +0,0 @@ -<?php -/** - * Extends the themes endpoint to add the global styles link. - * - * @package gutenberg - */ - -/** - * Adds the current global styles link to the theme's REST API response. - * - * @param WP_REST_Response $response The response object. - * @param WP_Theme $theme The theme object. - */ -function gutenberg_add_active_global_styles_link( $response, $theme ) { - if ( $theme->get_stylesheet() === wp_get_theme()->get_stylesheet() ) { - // This creates a record for the current theme if not existent. - $id = WP_Theme_JSON_Resolver_Gutenberg::get_user_global_styles_post_id(); - } else { - $user_cpt = WP_Theme_JSON_Resolver_Gutenberg::get_user_data_from_wp_global_styles( $theme ); - $id = isset( $user_cpt['ID'] ) ? $user_cpt['ID'] : null; - } - - if ( $id ) { - $response->add_link( - 'https://api.w.org/user-global-styles', - rest_url( 'wp/v2/global-styles/' . $id ) - ); - } - - return $response; -} - -add_filter( 'rest_prepare_theme', 'gutenberg_add_active_global_styles_link', 10, 2 ); diff --git a/lib/compat/wordpress-5.9/rest-api.php b/lib/compat/wordpress-5.9/rest-api.php deleted file mode 100644 index 8414f9e514d15..0000000000000 --- a/lib/compat/wordpress-5.9/rest-api.php +++ /dev/null @@ -1,148 +0,0 @@ -<?php -/** - * Extends the REST API endpoints. - * - * @package gutenberg - */ - -if ( ! defined( 'ABSPATH' ) ) { - die( 'Silence is golden.' ); -} - -/** - * Registers the REST API routes for URL Details. - */ -function gutenberg_register_url_details_routes() { - $url_details_controller = new WP_REST_URL_Details_Controller(); - $url_details_controller->register_routes(); -} -add_action( 'rest_api_init', 'gutenberg_register_url_details_routes' ); - -/** - * Registers the menu locations REST API routes. - */ -function gutenberg_register_rest_menu_location() { - $nav_menu_location = new WP_REST_Menu_Locations_Controller(); - $nav_menu_location->register_routes(); -} -add_action( 'rest_api_init', 'gutenberg_register_rest_menu_location' ); - -/** - * Hook in to the nav menu item post type and enable a post type rest endpoint. - * - * @param array $args Current registered post type args. - * @param string $post_type Name of post type. - * - * @return array - */ -function gutenberg_api_nav_menus_post_type_args( $args, $post_type ) { - if ( 'nav_menu_item' === $post_type ) { - $args['show_in_rest'] = true; - $args['rest_base'] = 'menu-items'; - $args['rest_controller_class'] = 'WP_REST_Menu_Items_Controller'; - } - - return $args; -} -add_filter( 'register_post_type_args', 'gutenberg_api_nav_menus_post_type_args', 10, 2 ); - -/** - * Hook in to the nav_menu taxonomy and enable a taxonomy rest endpoint. - * - * @param array $args Current registered taxonomy args. - * @param string $taxonomy Name of taxonomy. - * - * @return array - */ -function gutenberg_api_nav_menus_taxonomy_args( $args, $taxonomy ) { - if ( 'nav_menu' === $taxonomy ) { - $args['show_in_rest'] = true; - $args['rest_base'] = 'menus'; - $args['rest_controller_class'] = 'WP_REST_Menus_Controller'; - } - - return $args; -} -add_filter( 'register_taxonomy_args', 'gutenberg_api_nav_menus_taxonomy_args', 10, 2 ); - -/** - * Exposes the site icon url to the Gutenberg editor through the WordPress REST - * API. The site icon url should instead be fetched from the wp/v2/settings - * endpoint when https://github.com/WordPress/gutenberg/pull/19967 is complete. - * - * @param WP_REST_Response $response Response data served by the WordPress REST index endpoint. - * @return WP_REST_Response - */ -function gutenberg_register_site_icon_url( $response ) { - $data = $response->data; - $data['site_icon_url'] = get_site_icon_url(); - $response->set_data( $data ); - return $response; -} - -add_filter( 'rest_index', 'gutenberg_register_site_icon_url' ); - -/** - * Exposes the site logo to the Gutenberg editor through the WordPress REST - * API. This is used for fetching this information when user has no rights - * to update settings. - * - * @param WP_REST_Response $response Response data served by the WordPress REST index endpoint. - * @return WP_REST_Response - */ -function gutenberg_register_site_logo_to_rest_index( $response ) { - $site_logo_id = get_theme_mod( 'custom_logo' ); - $response->data['site_logo'] = $site_logo_id; - if ( $site_logo_id ) { - $response->add_link( - 'https://api.w.org/featuredmedia', - rest_url( 'wp/v2/media/' . $site_logo_id ), - array( - 'embeddable' => true, - ) - ); - } - return $response; -} - -add_filter( 'rest_index', 'gutenberg_register_site_logo_to_rest_index' ); - -/** - * Filters WP_User_Query arguments when querying users via the REST API. - * - * Allow using the has_published_post argument. - * - * @param array $prepared_args Array of arguments for WP_User_Query. - * @param WP_REST_Request $request The REST API request. - * - * @return array Returns modified $prepared_args. - */ -function gutenberg_rest_user_query_has_published_posts( $prepared_args, $request ) { - if ( ! empty( $request['has_published_posts'] ) ) { - $prepared_args['has_published_posts'] = ( true === $request['has_published_posts'] ) - ? get_post_types( array( 'show_in_rest' => true ), 'names' ) - : (array) $request['has_published_posts']; - } - return $prepared_args; -} -add_filter( 'rest_user_query', 'gutenberg_rest_user_query_has_published_posts', 10, 2 ); - -/** - * Filters REST API collection parameters for the users controller. - * - * @param array $query_params JSON Schema-formatted collection parameters. - * - * @return array Returns the $query_params with "has_published_posts". - */ -function gutenberg_rest_user_collection_params_has_published_posts( $query_params ) { - $query_params['has_published_posts'] = array( - 'description' => __( 'Limit result set to users who have published posts.', 'gutenberg' ), - 'type' => array( 'boolean', 'array' ), - 'items' => array( - 'type' => 'string', - 'enum' => get_post_types( array( 'show_in_rest' => true ), 'names' ), - ), - ); - return $query_params; -} -add_filter( 'rest_user_collection_params', 'gutenberg_rest_user_collection_params_has_published_posts' ); diff --git a/lib/compat/wordpress-5.9/script-loader.php b/lib/compat/wordpress-5.9/script-loader.php deleted file mode 100644 index 7f407ec1b2900..0000000000000 --- a/lib/compat/wordpress-5.9/script-loader.php +++ /dev/null @@ -1,48 +0,0 @@ -<?php -/** - * Load global styles assets in the front-end. - * - * @package gutenberg - */ - -/** - * This code lives in script-loader.php - * where we just load styles using wp_get_global_stylesheet. - */ -function gutenberg_enqueue_global_styles_assets() { - $separate_assets = wp_should_load_separate_core_block_assets(); - $is_block_theme = wp_is_block_theme(); - $is_classic_theme = ! $is_block_theme; - - /* - * Global styles should be printed in the head when loading all styles combined. - * The footer should only be used to print global styles for classic themes with separate core assets enabled. - * - * See https://core.trac.wordpress.org/ticket/53494. - */ - if ( - ( $is_block_theme && doing_action( 'wp_footer' ) ) || - ( $is_classic_theme && doing_action( 'wp_footer' ) && ! $separate_assets ) || - ( $is_classic_theme && doing_action( 'wp_enqueue_scripts' ) && $separate_assets ) - ) { - return; - } - - $stylesheet = gutenberg_get_global_stylesheet(); - - if ( empty( $stylesheet ) ) { - return; - } - - if ( isset( wp_styles()->registered['global-styles'] ) ) { - // There's a GS stylesheet (theme has theme.json), so we overwrite it. - wp_styles()->registered['global-styles']->extra['after'][0] = $stylesheet; - } else { - // No GS stylesheet (theme has no theme.json), so we enqueue a new one. - wp_register_style( 'global-styles', false, array(), true, true ); - wp_add_inline_style( 'global-styles', $stylesheet ); - wp_enqueue_style( 'global-styles' ); - } -} -add_action( 'wp_enqueue_scripts', 'gutenberg_enqueue_global_styles_assets' ); -add_action( 'wp_footer', 'gutenberg_enqueue_global_styles_assets' ); diff --git a/lib/compat/wordpress-5.9/template-canvas.php b/lib/compat/wordpress-5.9/template-canvas.php deleted file mode 100644 index 66805be797dcd..0000000000000 --- a/lib/compat/wordpress-5.9/template-canvas.php +++ /dev/null @@ -1,27 +0,0 @@ -<?php -/** - * Template canvas file to render the current 'wp_template'. - * - * @package gutenberg - */ - -/** - * Get the template HTML. - * This needs to run before <head> so that blocks can add scripts and styles in wp_head(). - */ -$template_html = gutenberg_get_the_template_html(); -?><!DOCTYPE html> -<html <?php language_attributes(); ?>> -<head> - <meta charset="<?php bloginfo( 'charset' ); ?>" /> - <?php wp_head(); ?> -</head> - -<body <?php body_class(); ?>> -<?php wp_body_open(); ?> - -<?php echo $template_html; // phpcs:ignore WordPress.Security.EscapeOutput ?> - -<?php wp_footer(); ?> -</body> -</html> diff --git a/lib/compat/wordpress-5.9/template-parts.php b/lib/compat/wordpress-5.9/template-parts.php deleted file mode 100644 index 102c9275be408..0000000000000 --- a/lib/compat/wordpress-5.9/template-parts.php +++ /dev/null @@ -1,129 +0,0 @@ -<?php -/** - * Block template part functions. - * - * This is a temporary compatibility fix for WordPress 5.8.x, which is missing - * some features for template parts that are present in 5.9. - * - * Once 5.9 is the minimum supported WordPress version for the Gutenberg - * plugin, this shim can be removed. - * - * @package gutenberg - */ - -// Only run any of the code in this file if the version is less than 5.9. -// wp_list_users was introduced in 5.9. -if ( ! function_exists( 'wp_list_users' ) ) { - /** - * Registers block editor 'wp_template_part' post type. - */ - function gutenberg_register_template_part_post_type() { - $labels = array( - 'name' => __( 'Template Parts', 'gutenberg' ), - 'singular_name' => __( 'Template Part', 'gutenberg' ), - 'menu_name' => _x( 'Template Parts', 'Admin Menu text', 'gutenberg' ), - 'add_new' => _x( 'Add New', 'Template Part', 'gutenberg' ), - 'add_new_item' => __( 'Add New Template Part', 'gutenberg' ), - 'new_item' => __( 'New Template Part', 'gutenberg' ), - 'edit_item' => __( 'Edit Template Part', 'gutenberg' ), - 'view_item' => __( 'View Template Part', 'gutenberg' ), - 'view_items' => __( 'View Template Parts', 'gutenberg' ), - 'all_items' => __( 'All Template Parts', 'gutenberg' ), - 'search_items' => __( 'Search Template Parts', 'gutenberg' ), - 'parent_item_colon' => __( 'Parent Template Part:', 'gutenberg' ), - 'not_found' => __( 'No template parts found.', 'gutenberg' ), - 'not_found_in_trash' => __( 'No template parts found in Trash.', 'gutenberg' ), - 'archives' => __( 'Template part archives', 'gutenberg' ), - 'insert_into_item' => __( 'Insert into template part', 'gutenberg' ), - 'uploaded_to_this_item' => __( 'Uploaded to this template part', 'gutenberg' ), - 'filter_items_list' => __( 'Filter template parts list', 'gutenberg' ), - 'items_list_navigation' => __( 'Template parts list navigation', 'gutenberg' ), - 'items_list' => __( 'Template parts list', 'gutenberg' ), - ); - - $args = array( - 'labels' => $labels, - 'description' => __( 'Template parts to include in your templates.', 'gutenberg' ), - 'public' => false, - 'has_archive' => false, - 'show_ui' => true, - 'show_in_menu' => false, - 'show_in_admin_bar' => false, - 'show_in_rest' => true, - 'rest_base' => 'template-parts', - 'rest_controller_class' => 'Gutenberg_REST_Templates_Controller', - 'map_meta_cap' => true, - 'supports' => array( - 'title', - 'slug', - 'excerpt', - 'editor', - 'revisions', - 'author', - ), - ); - - register_post_type( 'wp_template_part', $args ); - } - add_action( 'init', 'gutenberg_register_template_part_post_type' ); - - /** - * Registers the 'wp_template_part_area' taxonomy. - */ - function gutenberg_register_wp_template_part_area_taxonomy() { - register_taxonomy( - 'wp_template_part_area', - array( 'wp_template_part' ), - array( - 'public' => false, - 'hierarchical' => false, - 'labels' => array( - 'name' => __( 'Template Part Areas', 'gutenberg' ), - 'singular_name' => __( 'Template Part Area', 'gutenberg' ), - ), - 'query_var' => false, - 'rewrite' => false, - 'show_ui' => false, - '_builtin' => true, - 'show_in_nav_menus' => false, - 'show_in_rest' => false, - ) - ); - } - add_action( 'init', 'gutenberg_register_wp_template_part_area_taxonomy' ); - - /** - * Sets a custom slug when creating auto-draft template parts. - * This is only needed for auto-drafts created by the regular WP editor. - * If this page is to be removed, this won't be necessary. - * - * @param int $post_id Post ID. - */ - function gutenberg_set_unique_slug_on_create_template_part( $post_id ) { - // This is the core function with the same functionality. - if ( function_exists( 'wp_set_unique_slug_on_create_template_part' ) ) { - return; - } - - $post = get_post( $post_id ); - if ( 'auto-draft' !== $post->post_status ) { - return; - } - - if ( ! $post->post_name ) { - wp_update_post( - array( - 'ID' => $post_id, - 'post_name' => 'custom_slug_' . uniqid(), - ) - ); - } - - $terms = get_the_terms( $post_id, 'wp_theme' ); - if ( ! $terms || ! count( $terms ) ) { - wp_set_post_terms( $post_id, wp_get_theme()->get_stylesheet(), 'wp_theme' ); - } - } - - add_action( 'save_post_wp_template_part', 'gutenberg_set_unique_slug_on_create_template_part' ); -} diff --git a/lib/compat/wordpress-5.9/templates.php b/lib/compat/wordpress-5.9/templates.php deleted file mode 100644 index d5bdf1c55c486..0000000000000 --- a/lib/compat/wordpress-5.9/templates.php +++ /dev/null @@ -1,255 +0,0 @@ -<?php -/** - * Block template functions. - * - * This is a temporary compatibility fix for WordPress 5.8.x, which is missing - * some features for templates that are present in 5.9. - * - * Once 5.9 is the minimum supported WordPress version for the Gutenberg - * plugin, this shim can be removed. - * - * @package gutenberg - */ - -// Only run any of the code in this file if the version is less than 5.9. -// wp_list_users was introduced in 5.9. -if ( ! function_exists( 'wp_list_users' ) ) { - /** - * Registers block editor 'wp_template' post type. - */ - function gutenberg_register_template_post_type() { - $labels = array( - 'name' => __( 'Templates', 'gutenberg' ), - 'singular_name' => __( 'Template', 'gutenberg' ), - 'menu_name' => _x( 'Templates', 'Admin Menu text', 'gutenberg' ), - 'add_new' => _x( 'Add New', 'Template', 'gutenberg' ), - 'add_new_item' => __( 'Add New Template', 'gutenberg' ), - 'new_item' => __( 'New Template', 'gutenberg' ), - 'edit_item' => __( 'Edit Template', 'gutenberg' ), - 'view_item' => __( 'View Template', 'gutenberg' ), - 'view_items' => __( 'View Templates', 'gutenberg' ), - 'all_items' => __( 'All Templates', 'gutenberg' ), - 'search_items' => __( 'Search Templates', 'gutenberg' ), - 'parent_item_colon' => __( 'Parent Template:', 'gutenberg' ), - 'not_found' => __( 'No templates found.', 'gutenberg' ), - 'not_found_in_trash' => __( 'No templates found in Trash.', 'gutenberg' ), - 'archives' => __( 'Template archives', 'gutenberg' ), - 'insert_into_item' => __( 'Insert into template', 'gutenberg' ), - 'uploaded_to_this_item' => __( 'Uploaded to this template', 'gutenberg' ), - 'filter_items_list' => __( 'Filter templates list', 'gutenberg' ), - 'items_list_navigation' => __( 'Templates list navigation', 'gutenberg' ), - 'items_list' => __( 'Templates list', 'gutenberg' ), - ); - - $args = array( - 'labels' => $labels, - 'description' => __( 'Templates to include in your theme.', 'gutenberg' ), - 'public' => false, - 'has_archive' => false, - 'show_ui' => true, - 'show_in_menu' => false, - 'show_in_admin_bar' => false, - 'show_in_rest' => true, - 'rest_base' => 'templates', - 'rest_controller_class' => 'Gutenberg_REST_Templates_Controller', - 'capability_type' => array( 'template', 'templates' ), - 'map_meta_cap' => true, - 'supports' => array( - 'title', - 'slug', - 'excerpt', - 'editor', - 'revisions', - 'author', - ), - ); - - register_post_type( 'wp_template', $args ); - } - add_action( 'init', 'gutenberg_register_template_post_type' ); - - /** - * Registers block editor 'wp_theme' taxonomy. - */ - function gutenberg_register_wp_theme_taxonomy() { - register_taxonomy( - 'wp_theme', - array( 'wp_template', 'wp_template_part', 'wp_global_styles' ), - array( - 'public' => false, - 'hierarchical' => false, - 'labels' => array( - 'name' => __( 'Themes', 'gutenberg' ), - 'singular_name' => __( 'Theme', 'gutenberg' ), - ), - 'query_var' => false, - 'rewrite' => false, - 'show_ui' => false, - '_builtin' => true, - 'show_in_nav_menus' => false, - 'show_in_rest' => false, - ) - ); - } - add_action( 'init', 'gutenberg_register_wp_theme_taxonomy' ); - - /** - * Filters the capabilities of a user to conditionally grant them capabilities for managing 'wp_template' posts. - * - * Any user who can 'edit_theme_options' will have access. - * - * @param array $allcaps A user's capabilities. - * @return array Filtered $allcaps. - */ - function gutenberg_grant_template_caps( array $allcaps ) { - if ( isset( $allcaps['edit_theme_options'] ) ) { - $allcaps['edit_templates'] = $allcaps['edit_theme_options']; - $allcaps['edit_others_templates'] = $allcaps['edit_theme_options']; - $allcaps['edit_published_templates'] = $allcaps['edit_theme_options']; - $allcaps['edit_private_templates'] = $allcaps['edit_theme_options']; - $allcaps['delete_templates'] = $allcaps['edit_theme_options']; - $allcaps['delete_others_templates'] = $allcaps['edit_theme_options']; - $allcaps['delete_published_templates'] = $allcaps['edit_theme_options']; - $allcaps['delete_private_templates'] = $allcaps['edit_theme_options']; - $allcaps['publish_templates'] = $allcaps['edit_theme_options']; - $allcaps['read_private_templates'] = $allcaps['edit_theme_options']; - } - - return $allcaps; - } - add_filter( 'user_has_cap', 'gutenberg_grant_template_caps' ); - - /** - * Sets a custom slug when creating auto-draft templates. - * This is only needed for auto-drafts created by the regular WP editor. - * If this page is to be removed, this won't be necessary. - * - * @param int $post_id Post ID. - */ - function set_unique_slug_on_create_template( $post_id ) { - $post = get_post( $post_id ); - if ( 'auto-draft' !== $post->post_status ) { - return; - } - - if ( ! $post->post_name ) { - wp_update_post( - array( - 'ID' => $post_id, - 'post_name' => 'custom_slug_' . uniqid(), - ) - ); - } - - $terms = get_the_terms( $post_id, 'wp_theme' ); - if ( ! $terms || ! count( $terms ) ) { - wp_set_post_terms( $post_id, wp_get_theme()->get_stylesheet(), 'wp_theme' ); - } - } - add_action( 'save_post_wp_template', 'set_unique_slug_on_create_template' ); - - /** - * Print the skip-link script & styles. - * - * @todo Remove this when WP 5.8 is the minimum required version. - * - * @return void - */ - function gutenberg_the_skip_link() { - // Early exit if not a block theme. - if ( ! current_theme_supports( 'block-templates' ) ) { - return; - } - - // Early exit if not a block template. - global $_wp_current_template_content; - if ( ! $_wp_current_template_content ) { - return; - } - ?> - - <?php - /** - * Print the skip-link styles. - */ - ?> - <style id="skip-link-styles"> - .skip-link.screen-reader-text { - border: 0; - clip: rect(1px,1px,1px,1px); - clip-path: inset(50%); - height: 1px; - margin: -1px; - overflow: hidden; - padding: 0; - position: absolute !important; - width: 1px; - word-wrap: normal !important; - } - - .skip-link.screen-reader-text:focus { - background-color: #eee; - clip: auto !important; - clip-path: none; - color: #444; - display: block; - font-size: 1em; - height: auto; - left: 5px; - line-height: normal; - padding: 15px 23px 14px; - text-decoration: none; - top: 5px; - width: auto; - z-index: 100000; - } - </style> - <?php - /** - * Print the skip-link script. - */ - ?> - <script> - ( function() { - var skipLinkTarget = document.querySelector( 'main' ), - sibling, - skipLinkTargetID, - skipLink; - - // Early exit if a skip-link target can't be located. - if ( ! skipLinkTarget ) { - return; - } - - // Get the site wrapper. - // The skip-link will be injected in the beginning of it. - sibling = document.querySelector( '.wp-site-blocks' ); - - // Early exit if the root element was not found. - if ( ! sibling ) { - return; - } - - // Get the skip-link target's ID, and generate one if it doesn't exist. - skipLinkTargetID = skipLinkTarget.id; - if ( ! skipLinkTargetID ) { - skipLinkTargetID = 'wp--skip-link--target'; - skipLinkTarget.id = skipLinkTargetID; - } - - // Create the skip link. - skipLink = document.createElement( 'a' ); - skipLink.classList.add( 'skip-link', 'screen-reader-text' ); - skipLink.href = '#' + skipLinkTargetID; - skipLink.innerHTML = '<?php esc_html_e( 'Skip to content', 'gutenberg' ); ?>'; - - // Inject the skip link. - sibling.parentElement.insertBefore( skipLink, sibling ); - }() ); - </script> - <?php - } - - remove_action( 'wp_footer', 'the_block_template_skip_link' ); - add_action( 'wp_footer', 'gutenberg_the_skip_link' ); -} diff --git a/lib/compat/wordpress-5.9/theme-templates.php b/lib/compat/wordpress-5.9/theme-templates.php deleted file mode 100644 index 77a186274fe54..0000000000000 --- a/lib/compat/wordpress-5.9/theme-templates.php +++ /dev/null @@ -1,87 +0,0 @@ -<?php -/** - * Utilities used to fetch and create templates and template parts. - * - * @package Gutenberg - * @subpackage REST_API - */ - -/** - * Generates a unique slug for templates or template parts. - * - * @param string $override_slug The filtered value of the slug (starts as `null` from apply_filter). - * @param string $slug The original/un-filtered slug (post_name). - * @param int $post_ID Post ID. - * @param string $post_status No uniqueness checks are made if the post is still draft or pending. - * @param string $post_type Post type. - * @return string The original, desired slug. - */ -function gutenberg_filter_wp_template_unique_post_slug( $override_slug, $slug, $post_ID, $post_status, $post_type ) { - if ( 'wp_template' !== $post_type && 'wp_template_part' !== $post_type ) { - return $override_slug; - } - - if ( ! $override_slug ) { - $override_slug = $slug; - } - - // Template slugs must be unique within the same theme. - // TODO - Figure out how to update this to work for a multi-theme - // environment. Unfortunately using `get_the_terms` for the 'wp-theme' - // term does not work in the case of new entities since is too early in - // the process to have been saved to the entity. So for now we use the - // currently activated theme for creation. - $theme = wp_get_theme()->get_stylesheet(); - $terms = get_the_terms( $post_ID, 'wp_theme' ); - if ( $terms && ! is_wp_error( $terms ) ) { - $theme = $terms[0]->name; - } - - $check_query_args = array( - 'post_name__in' => array( $override_slug ), - 'post_type' => $post_type, - 'posts_per_page' => 1, - 'no_found_rows' => true, - 'post__not_in' => array( $post_ID ), - 'tax_query' => array( - array( - 'taxonomy' => 'wp_theme', - 'field' => 'name', - 'terms' => $theme, - ), - ), - ); - $check_query = new WP_Query( $check_query_args ); - $posts = $check_query->posts; - - if ( count( $posts ) > 0 ) { - $suffix = 2; - do { - $query_args = $check_query_args; - $alt_post_name = _truncate_post_slug( $override_slug, 200 - ( strlen( $suffix ) + 1 ) ) . "-$suffix"; - $query_args['post_name__in'] = array( $alt_post_name ); - $query = new WP_Query( $query_args ); - $suffix++; - } while ( count( $query->posts ) > 0 ); - $override_slug = $alt_post_name; - } - - return $override_slug; -} - -// Remove 5.8 filter if it exists. -remove_filter( 'pre_wp_unique_post_slug', 'wp_filter_wp_template_unique_post_slug' ); -add_filter( 'pre_wp_unique_post_slug', 'gutenberg_filter_wp_template_unique_post_slug', 10, 5 ); - -/** - * Enable block templates (editor mode) for themes with theme.json. - */ -function gutenberg_enable_block_templates() { - if ( wp_is_block_theme() || WP_Theme_JSON_Resolver_Gutenberg::theme_has_support() ) { - add_theme_support( 'block-templates' ); - } -} - -// Remove 5.8 filter if it exists. -remove_action( 'setup_theme', 'wp_enable_block_templates' ); -add_action( 'setup_theme', 'gutenberg_enable_block_templates' ); diff --git a/lib/compat/wordpress-5.9/theme.php b/lib/compat/wordpress-5.9/theme.php deleted file mode 100644 index 7bfe11f604c5b..0000000000000 --- a/lib/compat/wordpress-5.9/theme.php +++ /dev/null @@ -1,28 +0,0 @@ -<?php -/** - * Additions to the WordPress theme.php file - * - * @package gutenberg - */ - -if ( ! function_exists( 'wp_is_block_theme' ) ) { - /** - * Returns whether the current theme is a block theme or not. - * - * @return boolean Whether the current theme is a block theme or not. - */ - function wp_is_block_theme() { - return is_readable( get_theme_file_path( '/block-templates/index.html' ) ) || - is_readable( get_theme_file_path( '/templates/index.html' ) ); - } -} - -/** - * Note: We have to maintain this function for backward compatibility with WP 5.8. - * The `validate_theme_requirements` method is using `gutenberg_is_fse_theme` in older versions of WP. - * - * @return boolean Whether the current theme is a block theme or not. - */ -function gutenberg_is_fse_theme() { - return wp_is_block_theme(); -} diff --git a/lib/compat/wordpress-5.9/translate-settings-using-i18n-schema.php b/lib/compat/wordpress-5.9/translate-settings-using-i18n-schema.php deleted file mode 100644 index 8b88c93fec623..0000000000000 --- a/lib/compat/wordpress-5.9/translate-settings-using-i18n-schema.php +++ /dev/null @@ -1,50 +0,0 @@ -<?php -/** - * Function to translate json files. - * - * @package gutenberg - */ - -if ( ! function_exists( 'translate_settings_using_i18n_schema' ) ) { - /** - * Translates the provided settings value using its i18n schema. - * - * @param string|string[]|array[]|object $i18n_schema I18n schema for the setting. - * @param string|string[]|array[] $settings Value for the settings. - * @param string $textdomain Textdomain to use with translations. - * - * @return string|string[]|array[] Translated settings. - */ - function translate_settings_using_i18n_schema( $i18n_schema, $settings, $textdomain ) { - if ( empty( $i18n_schema ) || empty( $settings ) || empty( $textdomain ) ) { - return $settings; - } - - if ( is_string( $i18n_schema ) && is_string( $settings ) ) { - //phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText, WordPress.WP.I18n.NonSingularStringLiteralContext, WordPress.WP.I18n.NonSingularStringLiteralDomain, WordPress.WP.I18n.LowLevelTranslationFunction - return translate_with_gettext_context( $settings, $i18n_schema, $textdomain ); - } - if ( is_array( $i18n_schema ) && is_array( $settings ) ) { - $translated_settings = array(); - foreach ( $settings as $value ) { - $translated_settings[] = translate_settings_using_i18n_schema( $i18n_schema[0], $value, $textdomain ); - } - return $translated_settings; - } - if ( is_object( $i18n_schema ) && is_array( $settings ) ) { - $group_key = '*'; - $translated_settings = array(); - foreach ( $settings as $key => $value ) { - if ( isset( $i18n_schema->$key ) ) { - $translated_settings[ $key ] = translate_settings_using_i18n_schema( $i18n_schema->$key, $value, $textdomain ); - } elseif ( isset( $i18n_schema->$group_key ) ) { - $translated_settings[ $key ] = translate_settings_using_i18n_schema( $i18n_schema->$group_key, $value, $textdomain ); - } else { - $translated_settings[ $key ] = $value; - } - } - return $translated_settings; - } - return $settings; - } -} diff --git a/lib/compat/wordpress-5.9/widget-render-api-endpoint/class-gb-rest-widget-render-endpoint-polyfill.php b/lib/compat/wordpress-5.9/widget-render-api-endpoint/class-gb-rest-widget-render-endpoint-polyfill.php deleted file mode 100644 index 6c8f2ee2a51e3..0000000000000 --- a/lib/compat/wordpress-5.9/widget-render-api-endpoint/class-gb-rest-widget-render-endpoint-polyfill.php +++ /dev/null @@ -1,121 +0,0 @@ -<?php -/** - * REST API: GB_REST_Widget_Render_Endpoint_Polyfill class - * - * @package gutenberg - */ - -/** - * Polyfill API class to render widgets via the REST API. - * - * @see \WP_REST_Controller - */ -class GB_REST_Widget_Render_Endpoint_Polyfill extends \WP_REST_Widget_Types_Controller { - - /** - * Registers the widget type routes. - * - * @see register_rest_route() - */ - public function register_routes() { - $route = '/' . $this->rest_base . '/(?P<id>[a-zA-Z0-9_-]+)/render'; - - // Don't override if already registered. - $registered_routes = rest_get_server()->get_routes( 'wp/v2' ); - if ( array_key_exists( $route, $registered_routes ) ) { - return; - } - - register_rest_route( - $this->namespace, - $route, - array( - array( - 'methods' => WP_REST_Server::CREATABLE, - 'permission_callback' => array( $this, 'get_item_permissions_check' ), - 'callback' => array( $this, 'render' ), - 'args' => array( - 'id' => array( - 'description' => __( 'The widget type id.', 'default' ), - 'type' => 'string', - 'required' => true, - ), - 'instance' => array( - 'description' => __( 'Current instance settings of the widget.', 'default' ), - 'type' => 'object', - ), - ), - ), - ) - ); - } - - /** - * Renders a single Legacy Widget and wraps it in a JSON-encodable array. - * - * @param WP_REST_Request $request Full details about the request. - * - * @return array An array with rendered Legacy Widget HTML. - */ - public function render( $request ) { - return array( - 'preview' => $this->render_legacy_widget_preview_iframe( - $request['id'], - isset( $request['instance'] ) ? $request['instance'] : null - ), - ); - } - - /** - * Renders a page containing a preview of the requested Legacy Widget block. - * - * @param string $id_base The id base of the requested widget. - * @param array $instance The widget instance attributes. - * - * @return string Rendered Legacy Widget block preview. - */ - private function render_legacy_widget_preview_iframe( $id_base, $instance ) { - if ( ! defined( 'IFRAME_REQUEST' ) ) { - define( 'IFRAME_REQUEST', true ); - } - - ob_start(); - ?> - <!doctype html> - <html <?php language_attributes(); ?>> - <head> - <meta charset="<?php bloginfo( 'charset' ); ?>" /> - <meta name="viewport" content="width=device-width, initial-scale=1" /> - <link rel="profile" href="https://gmpg.org/xfn/11" /> - <?php wp_head(); ?> - <style> - /* Reset theme styles */ - html, body, #page, #content { - padding: 0 !important; - margin: 0 !important; - } - </style> - </head> - <body <?php body_class(); ?>> - <div id="page" class="site"> - <div id="content" class="site-content"> - <?php - $registry = WP_Block_Type_Registry::get_instance(); - $block = $registry->get_registered( 'core/legacy-widget' ); - echo $block->render( - array( - 'idBase' => $id_base, - 'instance' => $instance, - ) - ); - ?> - </div><!-- #content --> - </div><!-- #page --> - <?php wp_footer(); ?> - </body> - </html> - <?php - return ob_get_clean(); - } - -} diff --git a/lib/compat/wordpress-5.9/widget-render-api-endpoint/index.php b/lib/compat/wordpress-5.9/widget-render-api-endpoint/index.php deleted file mode 100644 index 32e6106966a38..0000000000000 --- a/lib/compat/wordpress-5.9/widget-render-api-endpoint/index.php +++ /dev/null @@ -1,22 +0,0 @@ -<?php -/** - * Shims the /wp/v2/widget-types/<id>/render endpoint in WP versions where it's missing - * - * @package gutenberg - */ - -// Load the polyfill class. -require_once __DIR__ . '/class-gb-rest-widget-render-endpoint-polyfill.php'; - -/** - * Registers routes from the GB_REST_Widget_Render_Endpoint_Polyfill class. - */ -function setup_widget_render_api_endpoint_polyfill() { - $polyfill = new GB_REST_Widget_Render_Endpoint_Polyfill(); - $polyfill->register_routes(); -} - -// Priority should be larger than 99 which is the one used for registering the core routes. -add_action( 'rest_api_init', 'setup_widget_render_api_endpoint_polyfill', 100 ); - - diff --git a/lib/compat/wordpress-6.0/block-editor-settings.php b/lib/compat/wordpress-6.0/block-editor-settings.php index 21f1f05bb45f1..8aae673bff334 100644 --- a/lib/compat/wordpress-6.0/block-editor-settings.php +++ b/lib/compat/wordpress-6.0/block-editor-settings.php @@ -5,20 +5,6 @@ * @package gutenberg */ -/** - * Returns true if the style is coming from global styles. - * - * @param array $style Array containing a '__unstableType' key. - * @return boolean - */ -function gutenberg_is_global_styles_in_5_8( $style ) { - if ( isset( $style['__unstableType'] ) && ( 'globalStyles' === $style['__unstableType'] ) ) { - return true; - } - - return false; -} - /** * Returns true if the style is coming from global styles. * diff --git a/lib/compat/wordpress-6.0/class-wp-theme-json-6-0.php b/lib/compat/wordpress-6.0/class-wp-theme-json-6-0.php index 77d731b9d4544..878ac30c1b2c7 100644 --- a/lib/compat/wordpress-6.0/class-wp-theme-json-6-0.php +++ b/lib/compat/wordpress-6.0/class-wp-theme-json-6-0.php @@ -14,7 +14,7 @@ * * @access private */ -class WP_Theme_JSON_6_0 extends WP_Theme_JSON_5_9 { +class WP_Theme_JSON_6_0 extends WP_Theme_JSON { /** * Metadata for style properties. * diff --git a/lib/compat/wordpress-6.0/class-wp-theme-json-resolver-6-0.php b/lib/compat/wordpress-6.0/class-wp-theme-json-resolver-6-0.php index cc6aa688493de..1de501b7b98c3 100644 --- a/lib/compat/wordpress-6.0/class-wp-theme-json-resolver-6-0.php +++ b/lib/compat/wordpress-6.0/class-wp-theme-json-resolver-6-0.php @@ -15,7 +15,7 @@ * * @access private */ -class WP_Theme_JSON_Resolver_6_0 extends WP_Theme_JSON_Resolver_5_9 { +class WP_Theme_JSON_Resolver_6_0 extends WP_Theme_JSON_Resolver { /** * Given a theme.json structure modifies it in place * to update certain values by its translated strings diff --git a/lib/load.php b/lib/load.php index d16096e03dabc..61d7bedba5b86 100644 --- a/lib/load.php +++ b/lib/load.php @@ -31,25 +31,9 @@ function gutenberg_is_experiment_enabled( $name ) { // 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' ) ) { - // WordPress 5.9 compat. - require_once __DIR__ . '/compat/wordpress-5.9/class-wp-rest-global-styles-controller.php'; - require_once __DIR__ . '/compat/wordpress-5.9/rest-active-global-styles.php'; - if ( ! class_exists( 'WP_REST_Menus_Controller' ) ) { - require_once __DIR__ . '/compat/wordpress-5.9/class-wp-rest-menus-controller.php'; - } - if ( ! class_exists( 'WP_REST_Menu_Items_Controller' ) ) { - require_once __DIR__ . '/compat/wordpress-5.9/class-wp-rest-menu-items-controller.php'; - } - if ( ! class_exists( 'WP_REST_Menu_Locations_Controller' ) ) { - require_once __DIR__ . '/compat/wordpress-5.9/class-wp-rest-menu-locations-controller.php'; - } if ( ! class_exists( 'WP_REST_Block_Editor_Settings_Controller' ) ) { require_once __DIR__ . '/experimental/class-wp-rest-block-editor-settings-controller.php'; } - if ( ! class_exists( 'WP_REST_URL_Details_Controller' ) ) { - require_once __DIR__ . '/compat/wordpress-5.9/class-wp-rest-url-details-controller.php'; - } - require_once __DIR__ . '/compat/wordpress-5.9/rest-api.php'; // WordPress 6.0 compat. require_once __DIR__ . '/compat/wordpress-6.0/class-gutenberg-rest-global-styles-controller.php'; @@ -74,36 +58,6 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/experimental/editor-settings.php'; -// WordPress 5.9 compat. -require __DIR__ . '/compat/wordpress-5.9/polyfills.php'; -require __DIR__ . '/compat/wordpress-5.9/block-gallery.php'; -require __DIR__ . '/compat/wordpress-5.9/widget-render-api-endpoint/index.php'; -require __DIR__ . '/compat/wordpress-5.9/blocks.php'; -require __DIR__ . '/compat/wordpress-5.9/block-editor-settings.php'; -require __DIR__ . '/compat/wordpress-5.9/block-patterns.php'; -require __DIR__ . '/compat/wordpress-5.9/block-template-utils.php'; -require __DIR__ . '/compat/wordpress-5.9/default-editor-styles.php'; -require __DIR__ . '/compat/wordpress-5.9/register-global-styles-cpt.php'; -require __DIR__ . '/compat/wordpress-5.9/script-loader.php'; -require __DIR__ . '/compat/wordpress-5.9/json-file-decode.php'; -require __DIR__ . '/compat/wordpress-5.9/translate-settings-using-i18n-schema.php'; -require __DIR__ . '/compat/wordpress-5.9/global-styles-css-custom-properties.php'; -require __DIR__ . '/compat/wordpress-5.9/class-gutenberg-block-template.php'; -require __DIR__ . '/compat/wordpress-5.9/templates.php'; -require __DIR__ . '/compat/wordpress-5.9/template-parts.php'; -require __DIR__ . '/compat/wordpress-5.9/theme-templates.php'; -require __DIR__ . '/compat/wordpress-5.9/class-wp-theme-json-schema-gutenberg.php'; -require __DIR__ . '/compat/wordpress-5.9/class-wp-theme-json-5-9.php'; -require __DIR__ . '/compat/wordpress-5.9/class-wp-theme-json-resolver-5-9.php'; -require __DIR__ . '/compat/wordpress-5.9/theme.php'; -require __DIR__ . '/compat/wordpress-5.9/admin-menu.php'; -require __DIR__ . '/compat/wordpress-5.9/edit-site-page.php'; -require __DIR__ . '/compat/wordpress-5.9/block-template.php'; -require __DIR__ . '/compat/wordpress-5.9/default-theme-supports.php'; -require __DIR__ . '/compat/wordpress-5.9/move-theme-editor-menu-item.php'; -require __DIR__ . '/compat/wordpress-5.9/navigation.php'; -require __DIR__ . '/compat/wordpress-5.9/kses.php'; - // WordPress 6.0 compat. require __DIR__ . '/compat/wordpress-6.0/block-editor-settings.php'; require __DIR__ . '/compat/wordpress-6.0/get-global-styles-and-settings.php'; diff --git a/packages/block-library/src/gallery/deprecated.js b/packages/block-library/src/gallery/deprecated.js index e71b87d2ab791..1ac1034cb9b60 100644 --- a/packages/block-library/src/gallery/deprecated.js +++ b/packages/block-library/src/gallery/deprecated.js @@ -2,7 +2,7 @@ * External dependencies */ import classnames from 'classnames'; -import { map, some, omit } from 'lodash'; +import { some, omit } from 'lodash'; /** * WordPress dependencies @@ -19,7 +19,6 @@ import { LINK_DESTINATION_MEDIA, LINK_DESTINATION_NONE, } from './constants'; -import { isGalleryV2Enabled } from './shared'; const DEPRECATED_LINK_DESTINATION_MEDIA = 'file'; const DEPRECATED_LINK_DESTINATION_ATTACHMENT = 'post'; @@ -282,11 +281,7 @@ const v6 = { ); }, migrate( attributes ) { - if ( isGalleryV2Enabled() ) { - return runV2Migration( attributes ); - } - - return attributes; + return runV2Migration( attributes ); }, }; const v5 = { @@ -372,23 +367,7 @@ const v5 = { return ! linkTo || linkTo === 'attachment' || linkTo === 'media'; }, migrate( attributes ) { - if ( isGalleryV2Enabled() ) { - return runV2Migration( attributes ); - } - - let linkTo = attributes.linkTo; - - if ( ! attributes.linkTo ) { - linkTo = 'none'; - } else if ( attributes.linkTo === 'attachment' ) { - linkTo = 'post'; - } else if ( attributes.linkTo === 'media' ) { - linkTo = 'file'; - } - return { - ...attributes, - linkTo, - }; + return runV2Migration( attributes ); }, save( { attributes } ) { const { @@ -535,17 +514,7 @@ const v4 = { return ids && ids.some( ( id ) => typeof id === 'string' ); }, migrate( attributes ) { - if ( isGalleryV2Enabled() ) { - return runV2Migration( attributes ); - } - - return { - ...attributes, - ids: map( attributes.ids, ( id ) => { - const parsedId = parseInt( id, 10 ); - return Number.isInteger( parsedId ) ? parsedId : null; - } ), - }; + return runV2Migration( attributes ); }, save( { attributes } ) { const { @@ -741,10 +710,7 @@ const v3 = { ); }, migrate( attributes ) { - if ( isGalleryV2Enabled() ) { - return runV2Migration( attributes ); - } - return attributes; + return runV2Migration( attributes ); }, }; const v2 = { @@ -810,18 +776,7 @@ const v2 = { ); }, migrate( attributes ) { - if ( isGalleryV2Enabled() ) { - return runV2Migration( attributes ); - } - return { - ...attributes, - ids: map( attributes.images, ( { id } ) => { - if ( ! id ) { - return null; - } - return parseInt( id, 10 ); - } ), - }; + return runV2Migration( attributes ); }, supports: { align: true, @@ -974,11 +929,7 @@ const v1 = { ); }, migrate( attributes ) { - if ( isGalleryV2Enabled() ) { - return runV2Migration( attributes ); - } - - return attributes; + return runV2Migration( attributes ); }, }; diff --git a/packages/block-library/src/gallery/edit-wrapper.js b/packages/block-library/src/gallery/edit-wrapper.js deleted file mode 100644 index 2c81271902d31..0000000000000 --- a/packages/block-library/src/gallery/edit-wrapper.js +++ /dev/null @@ -1,27 +0,0 @@ -/** - * WordPress dependencies - */ -import { compose } from '@wordpress/compose'; -import { withNotices } from '@wordpress/components'; - -/** - * Internal dependencies - */ -import EditWithInnerBlocks from './edit'; -import EditWithoutInnerBlocks from './v1/edit'; -import { isGalleryV2Enabled } from './shared'; - -/* - * Using a wrapper around the logic to load the edit for v1 of Gallery block - * or the refactored version with InnerBlocks. This is to prevent conditional - * use of hooks lint errors if adding this logic to the top of the edit component. - */ -function GalleryEditWrapper( props ) { - if ( ! isGalleryV2Enabled() ) { - return <EditWithoutInnerBlocks { ...props } />; - } - - return <EditWithInnerBlocks { ...props } />; -} - -export default compose( [ withNotices ] )( GalleryEditWrapper ); diff --git a/packages/block-library/src/gallery/gallery-styles.native.scss b/packages/block-library/src/gallery/gallery-styles.native.scss index a3073592291b9..f9b4d63c55b81 100644 --- a/packages/block-library/src/gallery/gallery-styles.native.scss +++ b/packages/block-library/src/gallery/gallery-styles.native.scss @@ -1,5 +1,3 @@ -@import "./v1/gallery-styles.native.scss"; - .galleryAppender { padding-top: $grid-unit-20; } diff --git a/packages/block-library/src/gallery/index.js b/packages/block-library/src/gallery/index.js index c9b82278a507a..ca74aec021b28 100644 --- a/packages/block-library/src/gallery/index.js +++ b/packages/block-library/src/gallery/index.js @@ -7,7 +7,7 @@ import { gallery as icon } from '@wordpress/icons'; * Internal dependencies */ import deprecated from './deprecated'; -import edit from './edit-wrapper'; +import edit from './edit'; import metadata from './block.json'; import save from './save'; import transforms from './transforms'; diff --git a/packages/block-library/src/gallery/save.js b/packages/block-library/src/gallery/save.js index 66253001e6418..acb689e4427ba 100644 --- a/packages/block-library/src/gallery/save.js +++ b/packages/block-library/src/gallery/save.js @@ -13,17 +13,7 @@ import { __experimentalGetElementClassName, } from '@wordpress/block-editor'; -/** - * Internal dependencies - */ -import saveWithoutInnerBlocks from './v1/save'; -import { isGalleryV2Enabled } from './shared'; - export default function saveWithInnerBlocks( { attributes } ) { - if ( ! isGalleryV2Enabled() ) { - return saveWithoutInnerBlocks( { attributes } ); - } - const { caption, columns, imageCrop } = attributes; const className = classnames( 'has-nested-images', { diff --git a/packages/block-library/src/gallery/transforms.js b/packages/block-library/src/gallery/transforms.js index 53a08bd590e71..74d5e1149f908 100644 --- a/packages/block-library/src/gallery/transforms.js +++ b/packages/block-library/src/gallery/transforms.js @@ -18,11 +18,6 @@ import { LINK_DESTINATION_NONE, LINK_DESTINATION_MEDIA, } from './constants'; -import { - LINK_DESTINATION_ATTACHMENT as DEPRECATED_LINK_DESTINATION_ATTACHMENT, - LINK_DESTINATION_MEDIA as DEPRECATED_LINK_DESTINATION_MEDIA, -} from './v1/constants'; -import { pickRelevantMediaFiles, isGalleryV2Enabled } from './shared'; const parseShortcodeIds = ( ids ) => { if ( ! ids ) { @@ -48,7 +43,6 @@ const parseShortcodeIds = ( ids ) => { */ function updateThirdPartyTransformToGallery( block ) { if ( - isGalleryV2Enabled() && block.name === 'core/gallery' && block.attributes?.images.length > 0 ) { @@ -142,34 +136,18 @@ const transforms = { const validImages = filter( attributes, ( { url } ) => url ); - if ( isGalleryV2Enabled() ) { - const innerBlocks = validImages.map( ( image ) => { - return createBlock( 'core/image', image ); - } ); - - return createBlock( - 'core/gallery', - { - align, - sizeSlug, - }, - innerBlocks - ); - } - - return createBlock( 'core/gallery', { - images: validImages.map( - ( { id, url, alt, caption } ) => ( { - id: id.toString(), - url, - alt, - caption, - } ) - ), - ids: validImages.map( ( { id } ) => parseInt( id, 10 ) ), - align, - sizeSlug, + const innerBlocks = validImages.map( ( image ) => { + return createBlock( 'core/image', image ); } ); + + return createBlock( + 'core/gallery', + { + align, + sizeSlug, + }, + innerBlocks + ); }, }, { @@ -177,32 +155,12 @@ const transforms = { tag: 'gallery', attributes: { - images: { - type: 'array', - shortcode: ( { named: { ids } } ) => { - if ( ! isGalleryV2Enabled() ) { - return parseShortcodeIds( ids ).map( ( id ) => ( { - id: id.toString(), - } ) ); - } - }, - }, - ids: { - type: 'array', - shortcode: ( { named: { ids } } ) => { - if ( ! isGalleryV2Enabled() ) { - return parseShortcodeIds( ids ); - } - }, - }, shortCodeTransforms: { type: 'array', shortcode: ( { named: { ids } } ) => { - if ( isGalleryV2Enabled() ) { - return parseShortcodeIds( ids ).map( ( id ) => ( { - id: parseInt( id ), - } ) ); - } + return parseShortcodeIds( ids ).map( ( id ) => ( { + id: parseInt( id ), + } ) ); }, }, columns: { @@ -214,16 +172,6 @@ const transforms = { linkTo: { type: 'string', shortcode: ( { named: { link } } ) => { - if ( ! isGalleryV2Enabled() ) { - switch ( link ) { - case 'post': - return DEPRECATED_LINK_DESTINATION_ATTACHMENT; - case 'file': - return DEPRECATED_LINK_DESTINATION_MEDIA; - default: - return DEPRECATED_LINK_DESTINATION_ATTACHMENT; - } - } switch ( link ) { case 'post': return LINK_DESTINATION_ATTACHMENT; @@ -257,23 +205,13 @@ const transforms = { ); }, transform( files ) { - if ( isGalleryV2Enabled() ) { - const innerBlocks = files.map( ( file ) => - createBlock( 'core/image', { - url: createBlobURL( file ), - } ) - ); + const innerBlocks = files.map( ( file ) => + createBlock( 'core/image', { + url: createBlobURL( file ), + } ) + ); - return createBlock( 'core/gallery', {}, innerBlocks ); - } - const block = createBlock( 'core/gallery', { - images: files.map( ( file ) => - pickRelevantMediaFiles( { - url: createBlobURL( file ), - } ) - ), - } ); - return block; + return createBlock( 'core/gallery', {}, innerBlocks ); }, }, ], @@ -281,47 +219,32 @@ const transforms = { { type: 'block', blocks: [ 'core/image' ], - transform: ( { align, images, ids, sizeSlug }, innerBlocks ) => { - if ( isGalleryV2Enabled() ) { - if ( innerBlocks.length > 0 ) { - return innerBlocks.map( - ( { - attributes: { - id, - url, - alt, - caption, - sizeSlug: imageSizeSlug, - linkDestination, - href, - linkTarget, - }, - } ) => - createBlock( 'core/image', { - id, - url, - alt, - caption, - sizeSlug: imageSizeSlug, - align, - linkDestination, - href, - linkTarget, - } ) - ); - } - return createBlock( 'core/image', { align } ); - } - if ( images.length > 0 ) { - return images.map( ( { url, alt, caption }, index ) => - createBlock( 'core/image', { - id: ids[ index ], - url, - alt, - caption, - align, - sizeSlug, - } ) + transform: ( { align }, innerBlocks ) => { + if ( innerBlocks.length > 0 ) { + return innerBlocks.map( + ( { + attributes: { + id, + url, + alt, + caption, + sizeSlug: imageSizeSlug, + linkDestination, + href, + linkTarget, + }, + } ) => + createBlock( 'core/image', { + id, + url, + alt, + caption, + sizeSlug: imageSizeSlug, + align, + linkDestination, + href, + linkTarget, + } ) ); } return createBlock( 'core/image', { align } ); diff --git a/packages/block-library/src/gallery/v1/constants.js b/packages/block-library/src/gallery/v1/constants.js deleted file mode 100644 index f4b6e7af56d47..0000000000000 --- a/packages/block-library/src/gallery/v1/constants.js +++ /dev/null @@ -1,3 +0,0 @@ -export const LINK_DESTINATION_NONE = 'none'; -export const LINK_DESTINATION_MEDIA = 'file'; -export const LINK_DESTINATION_ATTACHMENT = 'post'; diff --git a/packages/block-library/src/gallery/v1/gallery-button.native.js b/packages/block-library/src/gallery/v1/gallery-button.native.js deleted file mode 100644 index 8804e99cf2e7e..0000000000000 --- a/packages/block-library/src/gallery/v1/gallery-button.native.js +++ /dev/null @@ -1,47 +0,0 @@ -/** - * External dependencies - */ -import { StyleSheet, TouchableOpacity } from 'react-native'; - -/** - * WordPress dependencies - */ -import { Icon } from '@wordpress/components'; - -/** - * Internal dependencies - */ -import style from './gallery-image-style.scss'; - -export function Button( props ) { - const { - icon, - iconSize = 24, - onClick, - disabled, - 'aria-disabled': ariaDisabled, - accessibilityLabel = 'button', - style: customStyle, - } = props; - - const buttonStyle = StyleSheet.compose( style.buttonActive, customStyle ); - - const isDisabled = disabled || ariaDisabled; - - const { fill } = isDisabled ? style.buttonDisabled : style.button; - - return ( - <TouchableOpacity - style={ buttonStyle } - activeOpacity={ 0.7 } - accessibilityLabel={ accessibilityLabel } - accessibilityRole={ 'button' } - onPress={ onClick } - disabled={ isDisabled } - > - <Icon icon={ icon } fill={ fill } size={ iconSize } /> - </TouchableOpacity> - ); -} - -export default Button; diff --git a/packages/block-library/src/gallery/v1/gallery-image-style.native.scss b/packages/block-library/src/gallery/v1/gallery-image-style.native.scss deleted file mode 100644 index 9b221a56981f6..0000000000000 --- a/packages/block-library/src/gallery/v1/gallery-image-style.native.scss +++ /dev/null @@ -1,109 +0,0 @@ -$gallery-image-container-height: 150px; -$overlay-border-width: 2px; -$caption-background-color: rgba(0, 0, 0, 0.4); - -.galleryImageContainer { - flex: 1; - height: $gallery-image-container-height; - overflow: hidden; - background-color: $gray-lighten-30; -} - -.galleryImageContainerDark { - background-color: $gray-90; -} - -.image { - height: 100%; -} - -.button { - fill: $gray-0; - width: 30px; -} - -.buttonDisabled { - fill: $gray-30; -} - -.buttonActive { - flex-direction: row; - justify-content: center; - align-items: center; - border-radius: 6px; - border-color: $gray-70; - background-color: $gray-70; -} - -.moverButtonContainer { - flex-direction: row; - align-items: center; - border-radius: 3px; - background-color: $gray-70; -} - -.separator { - border-right-color: $gray-30; - border-right-width: 1px; - height: 20px; -} - -.toolbarContainer { - position: absolute; -} - -.toolbar { - padding: 5px; - flex-direction: row; - justify-content: space-between; -} - -.captionContainer { - flex: 1; - flex-direction: row; - align-items: flex-end; - position: absolute; - bottom: $overlay-border-width; - left: $overlay-border-width; - right: $overlay-border-width; - top: 45; -} - -@mixin caption-shared { - font-size: 12px; - background-color: #0000; - color: #fff; - font-family: $default-regular-font; - text-align: center; -} - -.caption { - @include caption-shared; - background-color: $caption-background-color; - padding-top: $grid-unit; - padding-bottom: $grid-unit; -} - -.captionPlaceholder { - color: #ccc; -} - -// expand caption container to compensate for overlay border -.captionExpandedContainer { - // constrain height to gallery image height for caption scroll - max-height: $gallery-image-container-height; - position: absolute; - bottom: 0; - left: 0; - right: 0; - padding-top: $grid-unit; - padding-bottom: $overlay-border-width + $grid-unit; - padding-left: $overlay-border-width; - padding-right: $overlay-border-width; - // use caption background color on container when expanded - background-color: $caption-background-color; -} - -.captionExpanded { - @include caption-shared; -} diff --git a/packages/block-library/src/gallery/v1/gallery-image.js b/packages/block-library/src/gallery/v1/gallery-image.js deleted file mode 100644 index 9520a5ba76349..0000000000000 --- a/packages/block-library/src/gallery/v1/gallery-image.js +++ /dev/null @@ -1,282 +0,0 @@ -/** - * External dependencies - */ -import classnames from 'classnames'; -import { get, omit } from 'lodash'; - -/** - * WordPress dependencies - */ -import { Component } from '@wordpress/element'; -import { Button, Spinner, ButtonGroup } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; -import { BACKSPACE, DELETE } from '@wordpress/keycodes'; -import { withSelect, withDispatch } from '@wordpress/data'; -import { - RichText, - MediaPlaceholder, - store as blockEditorStore, - __experimentalGetElementClassName, -} from '@wordpress/block-editor'; -import { isBlobURL } from '@wordpress/blob'; -import { compose } from '@wordpress/compose'; -import { - closeSmall, - chevronLeft, - chevronRight, - edit, - image as imageIcon, -} from '@wordpress/icons'; -import { store as coreStore } from '@wordpress/core-data'; - -/** - * Internal dependencies - */ -import { pickRelevantMediaFiles } from './shared'; -import { - LINK_DESTINATION_ATTACHMENT, - LINK_DESTINATION_MEDIA, -} from './constants'; - -const isTemporaryImage = ( id, url ) => ! id && isBlobURL( url ); - -class GalleryImage extends Component { - constructor() { - super( ...arguments ); - - this.onSelectImage = this.onSelectImage.bind( this ); - this.onRemoveImage = this.onRemoveImage.bind( this ); - this.bindContainer = this.bindContainer.bind( this ); - this.onEdit = this.onEdit.bind( this ); - this.onSelectImageFromLibrary = - this.onSelectImageFromLibrary.bind( this ); - this.onSelectCustomURL = this.onSelectCustomURL.bind( this ); - this.state = { - isEditing: false, - }; - } - - bindContainer( ref ) { - this.container = ref; - } - - onSelectImage() { - if ( ! this.props.isSelected ) { - this.props.onSelect(); - } - } - - onRemoveImage( event ) { - if ( - this.container === this.container.ownerDocument.activeElement && - this.props.isSelected && - [ BACKSPACE, DELETE ].indexOf( event.keyCode ) !== -1 - ) { - event.preventDefault(); - this.props.onRemove(); - } - } - - onEdit() { - this.setState( { - isEditing: true, - } ); - } - - componentDidUpdate() { - const { image, url, __unstableMarkNextChangeAsNotPersistent } = - this.props; - if ( image && ! url ) { - __unstableMarkNextChangeAsNotPersistent(); - this.props.setAttributes( { - url: image.source_url, - alt: image.alt_text, - } ); - } - } - - deselectOnBlur() { - this.props.onDeselect(); - } - - onSelectImageFromLibrary( media ) { - const { setAttributes, id, url, alt, caption, sizeSlug } = this.props; - if ( ! media || ! media.url ) { - return; - } - - let mediaAttributes = pickRelevantMediaFiles( media, sizeSlug ); - - // If the current image is temporary but an alt text was meanwhile - // written by the user, make sure the text is not overwritten. - if ( isTemporaryImage( id, url ) ) { - if ( alt ) { - mediaAttributes = omit( mediaAttributes, [ 'alt' ] ); - } - } - - // If a caption text was meanwhile written by the user, - // make sure the text is not overwritten by empty captions. - if ( caption && ! get( mediaAttributes, [ 'caption' ] ) ) { - mediaAttributes = omit( mediaAttributes, [ 'caption' ] ); - } - - setAttributes( mediaAttributes ); - this.setState( { - isEditing: false, - } ); - } - - onSelectCustomURL( newURL ) { - const { setAttributes, url } = this.props; - if ( newURL !== url ) { - setAttributes( { - url: newURL, - id: undefined, - } ); - this.setState( { - isEditing: false, - } ); - } - } - - render() { - const { - url, - alt, - id, - linkTo, - link, - isFirstItem, - isLastItem, - isSelected, - caption, - onRemove, - onMoveForward, - onMoveBackward, - setAttributes, - 'aria-label': ariaLabel, - } = this.props; - const { isEditing } = this.state; - - let href; - - switch ( linkTo ) { - case LINK_DESTINATION_MEDIA: - href = url; - break; - case LINK_DESTINATION_ATTACHMENT: - href = link; - break; - } - - const img = ( - // Disable reason: Image itself is not meant to be interactive, but should - // direct image selection and unfocus caption fields. - /* eslint-disable jsx-a11y/no-noninteractive-element-interactions */ - <> - <img - src={ url } - alt={ alt } - data-id={ id } - onKeyDown={ this.onRemoveImage } - tabIndex="0" - aria-label={ ariaLabel } - ref={ this.bindContainer } - /> - { isBlobURL( url ) && <Spinner /> } - </> - /* eslint-enable jsx-a11y/no-noninteractive-element-interactions */ - ); - - const className = classnames( { - 'is-selected': isSelected, - 'is-transient': isBlobURL( url ), - } ); - - return ( - // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions - <figure - className={ className } - onClick={ this.onSelectImage } - onFocus={ this.onSelectImage } - > - { ! isEditing && ( href ? <a href={ href }>{ img }</a> : img ) } - { isEditing && ( - <MediaPlaceholder - labels={ { title: __( 'Edit gallery image' ) } } - icon={ imageIcon } - onSelect={ this.onSelectImageFromLibrary } - onSelectURL={ this.onSelectCustomURL } - accept="image/*" - allowedTypes={ [ 'image' ] } - value={ { id, src: url } } - /> - ) } - <ButtonGroup className="block-library-gallery-item__inline-menu is-left"> - <Button - icon={ chevronLeft } - onClick={ isFirstItem ? undefined : onMoveBackward } - label={ __( 'Move image backward' ) } - aria-disabled={ isFirstItem } - disabled={ ! isSelected } - /> - <Button - icon={ chevronRight } - onClick={ isLastItem ? undefined : onMoveForward } - label={ __( 'Move image forward' ) } - aria-disabled={ isLastItem } - disabled={ ! isSelected } - /> - </ButtonGroup> - <ButtonGroup className="block-library-gallery-item__inline-menu is-right"> - <Button - icon={ edit } - onClick={ this.onEdit } - label={ __( 'Replace image' ) } - disabled={ ! isSelected } - /> - <Button - icon={ closeSmall } - onClick={ onRemove } - label={ __( 'Remove image' ) } - disabled={ ! isSelected } - /> - </ButtonGroup> - { ! isEditing && ( isSelected || caption ) && ( - <RichText - tagName="figcaption" - className={ __experimentalGetElementClassName( - 'caption' - ) } - aria-label={ __( 'Image caption text' ) } - placeholder={ isSelected ? __( 'Add caption' ) : null } - value={ caption } - onChange={ ( newCaption ) => - setAttributes( { caption: newCaption } ) - } - inlineToolbar - /> - ) } - </figure> - ); - } -} - -export default compose( [ - withSelect( ( select, ownProps ) => { - const { getMedia } = select( coreStore ); - const { id } = ownProps; - - return { - image: id ? getMedia( parseInt( id, 10 ) ) : null, - }; - } ), - withDispatch( ( dispatch ) => { - const { __unstableMarkNextChangeAsNotPersistent } = - dispatch( blockEditorStore ); - return { - __unstableMarkNextChangeAsNotPersistent, - }; - } ), -] )( GalleryImage ); diff --git a/packages/block-library/src/gallery/v1/gallery-styles.native.scss b/packages/block-library/src/gallery/v1/gallery-styles.native.scss deleted file mode 100644 index 9b3169da048b2..0000000000000 --- a/packages/block-library/src/gallery/v1/gallery-styles.native.scss +++ /dev/null @@ -1,8 +0,0 @@ -.galleryTilesContainerSelected { - margin-bottom: 16px; -} - -.fullWidth { - margin-left: $block-edge-to-content; - margin-right: $block-edge-to-content; -} diff --git a/packages/block-library/src/gallery/v1/gallery.js b/packages/block-library/src/gallery/v1/gallery.js deleted file mode 100644 index 7fc587f27911a..0000000000000 --- a/packages/block-library/src/gallery/v1/gallery.js +++ /dev/null @@ -1,125 +0,0 @@ -/** - * External dependencies - */ -import classnames from 'classnames'; - -/** - * WordPress dependencies - */ -import { - RichText, - __experimentalGetElementClassName, -} from '@wordpress/block-editor'; -import { VisuallyHidden } from '@wordpress/components'; -import { __, sprintf } from '@wordpress/i18n'; -import { createBlock, getDefaultBlockName } from '@wordpress/blocks'; - -/** - * Internal dependencies - */ -import GalleryImage from './gallery-image'; -import { defaultColumnsNumberV1 } from '../deprecated'; - -export const Gallery = ( props ) => { - const { - attributes, - isSelected, - setAttributes, - selectedImage, - mediaPlaceholder, - onMoveBackward, - onMoveForward, - onRemoveImage, - onSelectImage, - onDeselectImage, - onSetImageAttributes, - insertBlocksAfter, - blockProps, - } = props; - - const { - align, - columns = defaultColumnsNumberV1( attributes ), - caption, - imageCrop, - images, - } = attributes; - - return ( - <figure - { ...blockProps } - className={ classnames( blockProps.className, { - [ `align${ align }` ]: align, - [ `columns-${ columns }` ]: columns, - 'is-cropped': imageCrop, - } ) } - > - <ul className="blocks-gallery-grid"> - { images.map( ( img, index ) => { - const ariaLabel = sprintf( - /* translators: 1: the order number of the image. 2: the total number of images. */ - __( 'image %1$d of %2$d in gallery' ), - index + 1, - images.length - ); - - return ( - <li - className="blocks-gallery-item" - key={ img.id ? `${ img.id }-${ index }` : img.url } - > - <GalleryImage - url={ img.url } - alt={ img.alt } - id={ img.id } - isFirstItem={ index === 0 } - isLastItem={ index + 1 === images.length } - isSelected={ - isSelected && selectedImage === index - } - onMoveBackward={ onMoveBackward( index ) } - onMoveForward={ onMoveForward( index ) } - onRemove={ onRemoveImage( index ) } - onSelect={ onSelectImage( index ) } - onDeselect={ onDeselectImage( index ) } - setAttributes={ ( attrs ) => - onSetImageAttributes( index, attrs ) - } - caption={ img.caption } - aria-label={ ariaLabel } - sizeSlug={ attributes.sizeSlug } - /> - </li> - ); - } ) } - </ul> - { mediaPlaceholder } - <RichTextVisibilityHelper - isHidden={ ! isSelected && RichText.isEmpty( caption ) } - tagName="figcaption" - className={ classnames( - 'blocks-gallery-caption', - __experimentalGetElementClassName( 'caption' ) - ) } - aria-label={ __( 'Gallery caption text' ) } - placeholder={ __( 'Write gallery caption…' ) } - value={ caption } - onChange={ ( value ) => setAttributes( { caption: value } ) } - inlineToolbar - __unstableOnSplitAtEnd={ () => - insertBlocksAfter( createBlock( getDefaultBlockName() ) ) - } - /> - </figure> - ); -}; - -function RichTextVisibilityHelper( { isHidden, ...richTextProps } ) { - return isHidden ? ( - <VisuallyHidden as={ RichText } { ...richTextProps } /> - ) : ( - <RichText { ...richTextProps } /> - ); -} - -export default Gallery; diff --git a/packages/block-library/src/gallery/v1/gallery.native.js b/packages/block-library/src/gallery/v1/gallery.native.js deleted file mode 100644 index 7908d17988a1a..0000000000000 --- a/packages/block-library/src/gallery/v1/gallery.native.js +++ /dev/null @@ -1,162 +0,0 @@ -/** - * External dependencies - */ -import { View } from 'react-native'; -import { isEmpty } from 'lodash'; - -/** - * Internal dependencies - */ -import GalleryImage from './gallery-image'; -import { defaultColumnsNumberV1 } from '../deprecated'; -import styles from './gallery-styles.scss'; -import Tiles from './tiles'; - -/** - * WordPress dependencies - */ -import { __, sprintf } from '@wordpress/i18n'; -import { - BlockCaption, - store as blockEditorStore, -} from '@wordpress/block-editor'; -import { useState, useEffect } from '@wordpress/element'; -import { mediaUploadSync } from '@wordpress/react-native-bridge'; -import { useSelect } from '@wordpress/data'; -import { alignmentHelpers } from '@wordpress/components'; - -const TILE_SPACING = 15; - -// we must limit displayed columns since readable content max-width is 580px -const MAX_DISPLAYED_COLUMNS = 4; -const MAX_DISPLAYED_COLUMNS_NARROW = 2; - -const { isFullWidth } = alignmentHelpers; - -export const Gallery = ( props ) => { - const [ isCaptionSelected, setIsCaptionSelected ] = useState( false ); - useEffect( mediaUploadSync, [] ); - - const isRTL = useSelect( ( select ) => { - return !! select( blockEditorStore ).getSettings().isRTL; - }, [] ); - - const { - clientId, - selectedImage, - mediaPlaceholder, - onBlur, - onMoveBackward, - onMoveForward, - onRemoveImage, - onSelectImage, - onSetImageAttributes, - onFocusGalleryCaption, - attributes, - isSelected, - isNarrow, - onFocus, - insertBlocksAfter, - } = props; - - const { - align, - columns = defaultColumnsNumberV1( attributes ), - imageCrop, - images, - } = attributes; - - // limit displayed columns when isNarrow is true (i.e. when viewport width is - // less than "small", where small = 600) - const displayedColumns = isNarrow - ? Math.min( columns, MAX_DISPLAYED_COLUMNS_NARROW ) - : Math.min( columns, MAX_DISPLAYED_COLUMNS ); - - const selectImage = ( index ) => { - return () => { - if ( isCaptionSelected ) { - setIsCaptionSelected( false ); - } - // We need to fully invoke the curried function here. - onSelectImage( index )(); - }; - }; - - const focusGalleryCaption = () => { - if ( ! isCaptionSelected ) { - setIsCaptionSelected( true ); - } - onFocusGalleryCaption(); - }; - - return ( - <View style={ { flex: 1 } }> - <Tiles - columns={ displayedColumns } - spacing={ TILE_SPACING } - style={ - isSelected - ? styles.galleryTilesContainerSelected - : undefined - } - > - { images.map( ( img, index ) => { - const ariaLabel = sprintf( - /* translators: 1: the order number of the image. 2: the total number of images. */ - __( 'image %1$d of %2$d in gallery' ), - index + 1, - images.length - ); - - return ( - <GalleryImage - key={ img.id ? `${ img.id }-${ index }` : img.url } - url={ img.url } - alt={ img.alt } - id={ parseInt( img.id, 10 ) } // make id an integer explicitly - isCropped={ imageCrop } - isFirstItem={ index === 0 } - isLastItem={ index + 1 === images.length } - isSelected={ isSelected && selectedImage === index } - isBlockSelected={ isSelected } - onMoveBackward={ onMoveBackward( index ) } - onMoveForward={ onMoveForward( index ) } - onRemove={ onRemoveImage( index ) } - onSelect={ selectImage( index ) } - onSelectBlock={ onFocus } - setAttributes={ ( attrs ) => - onSetImageAttributes( index, attrs ) - } - caption={ img.caption } - aria-label={ ariaLabel } - isRTL={ isRTL } - /> - ); - } ) } - </Tiles> - <View style={ isFullWidth( align ) && styles.fullWidth }> - { mediaPlaceholder } - </View> - <BlockCaption - clientId={ clientId } - isSelected={ isCaptionSelected } - accessible={ true } - accessibilityLabelCreator={ ( caption ) => - isEmpty( caption ) - ? /* translators: accessibility text. Empty gallery caption. */ - 'Gallery caption. Empty' - : sprintf( - /* translators: accessibility text. %s: gallery caption. */ - __( 'Gallery caption. %s' ), - caption - ) - } - onFocus={ focusGalleryCaption } - onBlur={ onBlur } // Always assign onBlur as props. - insertBlocksAfter={ insertBlocksAfter } - /> - </View> - ); -}; - -export default Gallery; diff --git a/packages/block-library/src/gallery/v1/save.js b/packages/block-library/src/gallery/v1/save.js deleted file mode 100644 index bf8be9ba413b4..0000000000000 --- a/packages/block-library/src/gallery/v1/save.js +++ /dev/null @@ -1,98 +0,0 @@ -/** - * External dependencies - */ -import classnames from 'classnames'; - -/** - * WordPress dependencies - */ -import { - RichText, - useBlockProps, - __experimentalGetElementClassName, -} from '@wordpress/block-editor'; - -/** - * Internal dependencies - */ -import { defaultColumnsNumberV1 } from '../deprecated'; -import { - LINK_DESTINATION_ATTACHMENT, - LINK_DESTINATION_MEDIA, -} from './constants'; - -export default function saveV1( { attributes } ) { - const { - images, - columns = defaultColumnsNumberV1( attributes ), - imageCrop, - caption, - linkTo, - } = attributes; - const className = `columns-${ columns } ${ imageCrop ? 'is-cropped' : '' }`; - - return ( - <figure { ...useBlockProps.save( { className } ) }> - <ul className="blocks-gallery-grid"> - { images.map( ( image ) => { - let href; - - switch ( linkTo ) { - case LINK_DESTINATION_MEDIA: - href = image.fullUrl || image.url; - break; - case LINK_DESTINATION_ATTACHMENT: - href = image.link; - break; - } - - const img = ( - <img - src={ image.url } - alt={ image.alt } - data-id={ image.id } - data-full-url={ image.fullUrl } - data-link={ image.link } - className={ - image.id ? `wp-image-${ image.id }` : null - } - /> - ); - - return ( - <li - key={ image.id || image.url } - className="blocks-gallery-item" - > - <figure> - { href ? <a href={ href }>{ img }</a> : img } - { ! RichText.isEmpty( image.caption ) && ( - <RichText.Content - tagName="figcaption" - className={ classnames( - 'blocks-gallery-item', - __experimentalGetElementClassName( - 'caption' - ) - ) } - value={ image.caption } - /> - ) } - </figure> - </li> - ); - } ) } - </ul> - { ! RichText.isEmpty( caption ) && ( - <RichText.Content - tagName="figcaption" - className={ classnames( - 'blocks-gallery-caption', - __experimentalGetElementClassName( 'caption' ) - ) } - value={ caption } - /> - ) } - </figure> - ); -} diff --git a/packages/block-library/src/gallery/v1/shared.js b/packages/block-library/src/gallery/v1/shared.js deleted file mode 100644 index 484020cb9d58c..0000000000000 --- a/packages/block-library/src/gallery/v1/shared.js +++ /dev/null @@ -1,19 +0,0 @@ -/** - * External dependencies - */ -import { get, pick } from 'lodash'; - -export const pickRelevantMediaFiles = ( image, sizeSlug = 'large' ) => { - const imageProps = pick( image, [ 'alt', 'id', 'link', 'caption' ] ); - imageProps.url = - get( image, [ 'sizes', sizeSlug, 'url' ] ) || - get( image, [ 'media_details', 'sizes', sizeSlug, 'source_url' ] ) || - image.url; - const fullUrl = - get( image, [ 'sizes', 'full', 'url' ] ) || - get( image, [ 'media_details', 'sizes', 'full', 'source_url' ] ); - if ( fullUrl ) { - imageProps.fullUrl = fullUrl; - } - return imageProps; -}; diff --git a/packages/block-library/src/gallery/v1/tiles-styles.native.scss b/packages/block-library/src/gallery/v1/tiles-styles.native.scss deleted file mode 100644 index c2e3d4bad7979..0000000000000 --- a/packages/block-library/src/gallery/v1/tiles-styles.native.scss +++ /dev/null @@ -1,11 +0,0 @@ -.containerStyle { - flex-direction: row; - flex-wrap: wrap; -} - -.tileStyle { - overflow: hidden; - flex-direction: row; - align-items: center; - border-color: transparent; -} diff --git a/packages/block-library/src/gallery/v1/tiles.native.js b/packages/block-library/src/gallery/v1/tiles.native.js deleted file mode 100644 index 30126ce872593..0000000000000 --- a/packages/block-library/src/gallery/v1/tiles.native.js +++ /dev/null @@ -1,79 +0,0 @@ -/** - * External dependencies - */ -import { View, StyleSheet } from 'react-native'; - -/** - * WordPress dependencies - */ -import { Children } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import styles from './tiles-styles.scss'; - -function Tiles( props ) { - const { columns, children, spacing = 10, style } = props; - - const { compose } = StyleSheet; - - const tileCount = Children.count( children ); - const lastTile = tileCount - 1; - const lastRow = Math.floor( lastTile / columns ); - - const wrappedChildren = Children.map( children, ( child, index ) => { - /** - * Since we don't have `calc()`, we must calculate our spacings here in - * order to preserve even spacing between tiles and equal width for tiles - * in a given row. - * - * In order to ensure equal sizing of tile contents, we distribute the - * spacing such that each tile has an equal "share" of the fixed spacing. To - * keep the tiles properly aligned within their rows, we calculate the left - * and right paddings based on the tile's relative position within the row. - * - * Note: we use padding instead of margins so that the fixed spacing is - * included within the relative spacing (i.e. width percentage), and - * wrapping behavior is preserved. - * - * - The left most tile in a row must have left padding of zero. - * - The right most tile in a row must have a right padding of zero. - * - * The values of these left and right paddings are interpolated for tiles in - * between. The right padding is complementary with the left padding of the - * next tile (i.e. the right padding of [tile n] + the left padding of - * [tile n + 1] will be equal for all tiles except the last one in a given - * row). - */ - - const row = Math.floor( index / columns ); - const rowLength = - row === lastRow ? ( lastTile % columns ) + 1 : columns; - const indexInRow = index % columns; - - return ( - <View - style={ [ - styles.tileStyle, - { - width: `${ 100 / rowLength }%`, - paddingLeft: spacing * ( indexInRow / rowLength ), - paddingRight: - spacing * ( 1 - ( indexInRow + 1 ) / rowLength ), - paddingTop: row === 0 ? 0 : spacing / 2, - paddingBottom: row === lastRow ? 0 : spacing / 2, - }, - ] } - > - { child } - </View> - ); - } ); - - const containerStyle = compose( styles.containerStyle, style ); - - return <View style={ containerStyle }>{ wrappedChildren }</View>; -} - -export default Tiles; From 7121691c209ca4c99ec6533ab02e6597e6e77ce0 Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Wed, 25 May 2022 10:24:53 +0100 Subject: [PATCH 02/13] Use the code version of wp_enqueue_block_support_styles --- lib/block-supports/layout.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/block-supports/layout.php b/lib/block-supports/layout.php index e239ed023bd62..b1ba529baae75 100644 --- a/lib/block-supports/layout.php +++ b/lib/block-supports/layout.php @@ -219,7 +219,7 @@ function gutenberg_render_layout_support_flag( $block_content, $block ) { 1 ); - gutenberg_enqueue_block_support_styles( $style ); + wp_enqueue_block_support_styles( $style ); return $content; } From df5e9f3bedb9b1d11654376dc4c33989afe54864 Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Wed, 25 May 2022 10:39:38 +0100 Subject: [PATCH 03/13] More tweaks --- .../class-wp-theme-json-resolver-6-0.php | 152 +++++ ...ss-wp-theme-json-schema-gutenberg-test.php | 522 ------------------ 2 files changed, 152 insertions(+), 522 deletions(-) delete mode 100644 phpunit/class-wp-theme-json-schema-gutenberg-test.php diff --git a/lib/compat/wordpress-6.0/class-wp-theme-json-resolver-6-0.php b/lib/compat/wordpress-6.0/class-wp-theme-json-resolver-6-0.php index 1de501b7b98c3..13e00171ce082 100644 --- a/lib/compat/wordpress-6.0/class-wp-theme-json-resolver-6-0.php +++ b/lib/compat/wordpress-6.0/class-wp-theme-json-resolver-6-0.php @@ -16,6 +16,9 @@ * @access private */ class WP_Theme_JSON_Resolver_6_0 extends WP_Theme_JSON_Resolver { + + + /** * Given a theme.json structure modifies it in place * to update certain values by its translated strings @@ -159,4 +162,153 @@ public static function get_style_variations() { } return $variations; } + + /** + * Returns the custom post type that contains the user's origin config + * for the current theme or a void array if none are found. + * + * This can also create and return a new draft custom post type. + * + * @param WP_Theme $theme The theme object. If empty, it + * defaults to the current theme. + * @param bool $create_post Optional. Whether a new custom post + * type should be created if none are + * found. False by default. + * @param array $post_status_filter Filter Optional. custom post type by + * post status. ['publish'] by default, + * so it only fetches published posts. + * @return array Custom Post Type for the user's origin config. + */ + public static function get_user_data_from_wp_global_styles( $theme, $create_post = false, $post_status_filter = array( 'publish' ) ) { + if ( ! $theme instanceof WP_Theme ) { + $theme = wp_get_theme(); + } + $user_cpt = array(); + $post_type_filter = 'wp_global_styles'; + $args = array( + 'numberposts' => 1, + 'orderby' => 'date', + 'order' => 'desc', + 'post_type' => $post_type_filter, + 'post_status' => $post_status_filter, + 'tax_query' => array( + array( + 'taxonomy' => 'wp_theme', + 'field' => 'name', + 'terms' => $theme->get_stylesheet(), + ), + ), + ); + + $cache_key = sprintf( 'wp_global_styles_%s', md5( serialize( $args ) ) ); + $post_id = wp_cache_get( $cache_key ); + + if ( (int) $post_id > 0 ) { + return get_post( $post_id, ARRAY_A ); + } + + // Special case: '-1' is a results not found. + if ( -1 === $post_id && ! $create_post ) { + return $user_cpt; + } + + $recent_posts = wp_get_recent_posts( $args ); + if ( is_array( $recent_posts ) && ( count( $recent_posts ) === 1 ) ) { + $user_cpt = $recent_posts[0]; + } elseif ( $create_post ) { + $cpt_post_id = wp_insert_post( + array( + 'post_content' => '{"version": ' . WP_Theme_JSON_Gutenberg::LATEST_SCHEMA . ', "isGlobalStylesUserThemeJSON": true }', + 'post_status' => 'publish', + 'post_title' => __( 'Custom Styles', 'default' ), + 'post_type' => $post_type_filter, + 'post_name' => 'wp-global-styles-' . urlencode( wp_get_theme()->get_stylesheet() ), + 'tax_input' => array( + 'wp_theme' => array( wp_get_theme()->get_stylesheet() ), + ), + ), + true + ); + $user_cpt = get_post( $cpt_post_id, ARRAY_A ); + } + $cache_expiration = $user_cpt ? DAY_IN_SECONDS : HOUR_IN_SECONDS; + wp_cache_set( $cache_key, $user_cpt ? $user_cpt['ID'] : -1, '', $cache_expiration ); + + return $user_cpt; + } + + /** + * Returns the user's origin config. + * + * @return WP_Theme_JSON_Gutenberg Entity that holds styles for user data. + */ + public static function get_user_data() { + if ( null !== static::$user ) { + return static::$user; + } + + $config = array(); + $user_cpt = static::get_user_data_from_wp_global_styles( wp_get_theme() ); + + if ( array_key_exists( 'post_content', $user_cpt ) ) { + $decoded_data = json_decode( $user_cpt['post_content'], true ); + + $json_decoding_error = json_last_error(); + if ( JSON_ERROR_NONE !== $json_decoding_error ) { + trigger_error( 'Error when decoding a theme.json schema for user data. ' . json_last_error_msg() ); + return new WP_Theme_JSON_Gutenberg( $config, 'custom' ); + } + + // Very important to verify if the flag isGlobalStylesUserThemeJSON is true. + // If is not true the content was not escaped and is not safe. + if ( + is_array( $decoded_data ) && + isset( $decoded_data['isGlobalStylesUserThemeJSON'] ) && + $decoded_data['isGlobalStylesUserThemeJSON'] + ) { + unset( $decoded_data['isGlobalStylesUserThemeJSON'] ); + $config = $decoded_data; + } + } + static::$user = new WP_Theme_JSON_Gutenberg( $config, 'custom' ); + + return static::$user; + } + + /** + * There are three sources of data (origins) for a site: + * default, theme, and custom. The custom's has higher priority + * than the theme's, and the theme's higher than defaults's. + * + * Unlike the getters {@link get_core_data}, + * {@link get_theme_data}, and {@link get_user_data}, + * this method returns data after it has been merged + * with the previous origins. This means that if the same piece of data + * is declared in different origins (user, theme, and core), + * the last origin overrides the previous. + * + * For example, if the user has set a background color + * for the paragraph block, and the theme has done it as well, + * the user preference wins. + * + * @param string $origin Optional. To what level should we merge data. + * Valid values are 'theme' or 'custom'. + * Default is 'custom'. + * @return WP_Theme_JSON_Gutenberg + */ + public static function get_merged_data( $origin = 'custom' ) { + if ( is_array( $origin ) ) { + _deprecated_argument( __FUNCTION__, '5.9' ); + } + + $result = new WP_Theme_JSON_Gutenberg(); + $result->merge( static::get_core_data() ); + $result->merge( static::get_theme_data() ); + + if ( 'custom' === $origin ) { + $result->merge( static::get_user_data() ); + } + + return $result; + } } diff --git a/phpunit/class-wp-theme-json-schema-gutenberg-test.php b/phpunit/class-wp-theme-json-schema-gutenberg-test.php deleted file mode 100644 index 6f6c2a323409c..0000000000000 --- a/phpunit/class-wp-theme-json-schema-gutenberg-test.php +++ /dev/null @@ -1,522 +0,0 @@ -<?php - -/** - * Test WP_Theme_JSON_Gutenberg class. - * - * @package Gutenberg - */ - -class WP_Theme_JSON_Schema_Gutenberg_Test extends WP_UnitTestCase { - /** - * The current theme.json schema version. - */ - const LATEST_SCHEMA_VERSION = WP_Theme_JSON_Gutenberg::LATEST_SCHEMA; - - function test_migrate_v0_to_v1() { - $theme_json_v0 = array( - 'settings' => array( - 'defaults' => array( - 'color' => array( - 'palette' => array( - array( - 'name' => 'Black', - 'slug' => 'black', - 'color' => '#00000', - ), - array( - 'name' => 'White', - 'slug' => 'white', - 'color' => '#ffffff', - ), - array( - 'name' => 'Pale Pink', - 'slug' => 'pale-pink', - 'color' => '#f78da7', - ), - array( - 'name' => 'Vivid Red', - 'slug' => 'vivid-red', - 'color' => '#cf2e2', - ), - ), - 'custom' => false, - 'link' => false, - ), - 'typography' => array( - 'customFontStyle' => false, - 'customFontWeight' => false, - 'customTextDecorations' => false, - 'customTextTransforms' => false, - ), - ), - 'root' => array( - 'color' => array( - 'palette' => array( - array( - 'name' => 'Pale Pink', - 'slug' => 'pale-pink', - 'color' => '#f78da7', - ), - array( - 'name' => 'Vivid Red', - 'slug' => 'vivid-red', - 'color' => '#cf2e2e', - ), - ), - 'link' => true, - ), - 'border' => array( - 'customColor' => false, - 'customRadius' => false, - 'customStyle' => false, - 'customWidth' => false, - ), - ), - 'core/paragraph' => array( - 'typography' => array( - 'dropCap' => false, - ), - ), - ), - 'styles' => array( - 'root' => array( - 'color' => array( - 'background' => 'purple', - 'link' => 'red', - ), - ), - 'core/group' => array( - 'color' => array( - 'background' => 'red', - 'link' => 'yellow', - ), - 'spacing' => array( - 'padding' => array( - 'top' => '10px', - ), - ), - ), - ), - ); - - $actual = WP_Theme_JSON_Schema_Gutenberg::migrate( $theme_json_v0 ); - - $expected = array( - 'version' => self::LATEST_SCHEMA_VERSION, - 'settings' => array( - 'color' => array( - 'palette' => array( - array( - 'name' => 'Pale Pink', - 'slug' => 'pale-pink', - 'color' => '#f78da7', - ), - array( - 'name' => 'Vivid Red', - 'slug' => 'vivid-red', - 'color' => '#cf2e2e', - ), - ), - 'custom' => false, - 'link' => true, - ), - 'border' => array( - 'color' => false, - 'radius' => false, - 'style' => false, - 'width' => false, - ), - 'typography' => array( - 'fontStyle' => false, - 'fontWeight' => false, - 'textDecoration' => false, - 'textTransform' => false, - ), - 'blocks' => array( - 'core/paragraph' => array( - 'typography' => array( - 'dropCap' => false, - ), - ), - ), - ), - 'styles' => array( - 'color' => array( - 'background' => 'purple', - ), - 'blocks' => array( - 'core/group' => array( - 'color' => array( - 'background' => 'red', - ), - 'spacing' => array( - 'padding' => array( - 'top' => '10px', - ), - ), - 'elements' => array( - 'link' => array( - 'color' => array( - 'text' => 'yellow', - ), - ), - ), - ), - ), - 'elements' => array( - 'link' => array( - 'color' => array( - 'text' => 'red', - ), - ), - ), - ), - ); - - $this->assertEqualSetsWithIndex( $expected, $actual ); - } - - function test_migrate_v1_remove_custom_prefixes() { - $theme_json_v1 = array( - 'version' => 1, - 'settings' => array( - 'color' => array( - 'palette' => array( - array( - 'name' => 'Pale Pink', - 'slug' => 'pale-pink', - 'color' => '#f78da7', - ), - array( - 'name' => 'Vivid Red', - 'slug' => 'vivid-red', - 'color' => '#cf2e2e', - ), - ), - 'custom' => false, - 'link' => true, - ), - 'border' => array( - 'customColor' => false, - 'customRadius' => false, - 'customStyle' => false, - 'customWidth' => false, - ), - 'typography' => array( - 'customFontStyle' => false, - 'customFontWeight' => false, - 'customLetterSpacing' => false, - 'customTextDecorations' => false, - 'customTextTransforms' => false, - ), - 'blocks' => array( - 'core/group' => array( - 'border' => array( - 'customColor' => true, - 'customRadius' => true, - 'customStyle' => true, - 'customWidth' => true, - ), - 'typography' => array( - 'customFontStyle' => true, - 'customFontWeight' => true, - 'customLetterSpacing' => true, - 'customTextDecorations' => true, - 'customTextTransforms' => true, - ), - ), - ), - ), - 'styles' => array( - 'color' => array( - 'background' => 'purple', - ), - 'blocks' => array( - 'core/group' => array( - 'color' => array( - 'background' => 'red', - ), - 'spacing' => array( - 'padding' => array( - 'top' => '10px', - ), - ), - 'elements' => array( - 'link' => array( - 'color' => array( - 'text' => 'yellow', - ), - ), - ), - ), - ), - 'elements' => array( - 'link' => array( - 'color' => array( - 'text' => 'red', - ), - ), - ), - ), - ); - - $actual = WP_Theme_JSON_Schema_Gutenberg::migrate( $theme_json_v1 ); - - $expected = array( - 'version' => self::LATEST_SCHEMA_VERSION, - 'settings' => array( - 'color' => array( - 'palette' => array( - array( - 'name' => 'Pale Pink', - 'slug' => 'pale-pink', - 'color' => '#f78da7', - ), - array( - 'name' => 'Vivid Red', - 'slug' => 'vivid-red', - 'color' => '#cf2e2e', - ), - ), - 'custom' => false, - 'link' => true, - ), - 'border' => array( - 'color' => false, - 'radius' => false, - 'style' => false, - 'width' => false, - ), - 'typography' => array( - 'fontStyle' => false, - 'fontWeight' => false, - 'letterSpacing' => false, - 'textDecoration' => false, - 'textTransform' => false, - ), - 'blocks' => array( - 'core/group' => array( - 'border' => array( - 'color' => true, - 'radius' => true, - 'style' => true, - 'width' => true, - ), - 'typography' => array( - 'fontStyle' => true, - 'fontWeight' => true, - 'letterSpacing' => true, - 'textDecoration' => true, - 'textTransform' => true, - ), - ), - ), - ), - 'styles' => array( - 'color' => array( - 'background' => 'purple', - ), - 'blocks' => array( - 'core/group' => array( - 'color' => array( - 'background' => 'red', - ), - 'spacing' => array( - 'padding' => array( - 'top' => '10px', - ), - ), - 'elements' => array( - 'link' => array( - 'color' => array( - 'text' => 'yellow', - ), - ), - ), - ), - ), - 'elements' => array( - 'link' => array( - 'color' => array( - 'text' => 'red', - ), - ), - ), - ), - ); - - $this->assertEqualSetsWithIndex( $expected, $actual ); - } - - function test_migrate_v1_to_v2() { - $theme_json_v1 = array( - 'version' => 1, - 'settings' => array( - 'color' => array( - 'palette' => array( - array( - 'name' => 'Pale Pink', - 'slug' => 'pale-pink', - 'color' => '#f78da7', - ), - array( - 'name' => 'Vivid Red', - 'slug' => 'vivid-red', - 'color' => '#cf2e2e', - ), - ), - 'custom' => false, - 'link' => true, - ), - 'border' => array( - 'customColor' => false, - 'customRadius' => false, - 'customStyle' => false, - 'customWidth' => false, - ), - 'typography' => array( - 'customFontStyle' => false, - 'customFontWeight' => false, - 'customLetterSpacing' => false, - 'customTextDecorations' => false, - 'customTextTransforms' => false, - ), - 'blocks' => array( - 'core/group' => array( - 'border' => array( - 'customColor' => true, - 'customRadius' => true, - 'customStyle' => true, - 'customWidth' => true, - ), - 'typography' => array( - 'customFontStyle' => true, - 'customFontWeight' => true, - 'customLetterSpacing' => true, - 'customTextDecorations' => true, - 'customTextTransforms' => true, - ), - ), - ), - ), - 'styles' => array( - 'color' => array( - 'background' => 'purple', - ), - 'blocks' => array( - 'core/group' => array( - 'color' => array( - 'background' => 'red', - ), - 'spacing' => array( - 'padding' => array( - 'top' => '10px', - ), - ), - 'elements' => array( - 'link' => array( - 'color' => array( - 'text' => 'yellow', - ), - ), - ), - ), - ), - 'elements' => array( - 'link' => array( - 'color' => array( - 'text' => 'red', - ), - ), - ), - ), - ); - - $actual = WP_Theme_JSON_Schema_Gutenberg::migrate( $theme_json_v1 ); - - $expected = array( - 'version' => self::LATEST_SCHEMA_VERSION, - 'settings' => array( - 'color' => array( - 'palette' => array( - array( - 'name' => 'Pale Pink', - 'slug' => 'pale-pink', - 'color' => '#f78da7', - ), - array( - 'name' => 'Vivid Red', - 'slug' => 'vivid-red', - 'color' => '#cf2e2e', - ), - ), - 'custom' => false, - 'link' => true, - ), - 'border' => array( - 'color' => false, - 'radius' => false, - 'style' => false, - 'width' => false, - ), - 'typography' => array( - 'fontStyle' => false, - 'fontWeight' => false, - 'letterSpacing' => false, - 'textDecoration' => false, - 'textTransform' => false, - ), - 'blocks' => array( - 'core/group' => array( - 'border' => array( - 'color' => true, - 'radius' => true, - 'style' => true, - 'width' => true, - ), - 'typography' => array( - 'fontStyle' => true, - 'fontWeight' => true, - 'letterSpacing' => true, - 'textDecoration' => true, - 'textTransform' => true, - ), - ), - ), - ), - 'styles' => array( - 'color' => array( - 'background' => 'purple', - ), - 'blocks' => array( - 'core/group' => array( - 'color' => array( - 'background' => 'red', - ), - 'spacing' => array( - 'padding' => array( - 'top' => '10px', - ), - ), - 'elements' => array( - 'link' => array( - 'color' => array( - 'text' => 'yellow', - ), - ), - ), - ), - ), - 'elements' => array( - 'link' => array( - 'color' => array( - 'text' => 'red', - ), - ), - ), - ), - ); - - $this->assertEqualSetsWithIndex( $expected, $actual ); - } -} From 5a76cb1422f0741bd1e0d453c750b419a8dd3190 Mon Sep 17 00:00:00 2001 From: ntsekouras <ntsekouras@outlook.com> Date: Wed, 25 May 2022 17:18:32 +0300 Subject: [PATCH 04/13] more tweaks --- gutenberg.php | 4 ++-- lib/init.php | 24 ------------------- .../src/admin/visit-site-editor.ts | 3 +-- packages/e2e-test-utils/src/site-editor.js | 3 +-- 4 files changed, 4 insertions(+), 30 deletions(-) diff --git a/gutenberg.php b/gutenberg.php index 3488ca7f516c2..a965592e1baf6 100644 --- a/gutenberg.php +++ b/gutenberg.php @@ -26,7 +26,7 @@ function gutenberg_wordpress_version_notice() { echo '<div class="error"><p>'; /* translators: %s: Minimum required version */ - printf( __( 'Gutenberg requires WordPress %s or later to function properly. Please upgrade WordPress before activating Gutenberg.', 'gutenberg' ), '5.8' ); + printf( __( 'Gutenberg requires WordPress %s or later to function properly. Please upgrade WordPress before activating Gutenberg.', 'gutenberg' ), '5.9' ); echo '</p></div>'; deactivate_plugins( array( 'gutenberg/gutenberg.php' ) ); @@ -64,7 +64,7 @@ function gutenberg_pre_init() { // Compare against major release versions (X.Y) rather than minor (X.Y.Z) // unless a minor release is the actual minimum requirement. WordPress reports // X.Y for its major releases. - if ( version_compare( $version, '5.8', '<' ) ) { + if ( version_compare( $version, '5.9', '<' ) ) { add_action( 'admin_notices', 'gutenberg_wordpress_version_notice' ); return; } diff --git a/lib/init.php b/lib/init.php index 9ba46a1f41fa2..b867dd472552f 100644 --- a/lib/init.php +++ b/lib/init.php @@ -58,30 +58,6 @@ function gutenberg_menu() { } add_action( 'admin_menu', 'gutenberg_menu', 9 ); -/** - * Site editor's Menu. - * - * Adds a new wp-admin menu item for the Site editor. - * - * @since 9.4.0 - */ -function gutenberg_site_editor_menu() { - if ( wp_is_block_theme() ) { - add_theme_page( - __( 'Editor (beta)', 'gutenberg' ), - sprintf( - /* translators: %s: "beta" label. */ - __( 'Editor %s', 'gutenberg' ), - '<span class="awaiting-mod">' . __( 'beta', 'gutenberg' ) . '</span>' - ), - 'edit_theme_options', - 'gutenberg-edit-site', - 'gutenberg_edit_site_page' - ); - } -} -add_action( 'admin_menu', 'gutenberg_site_editor_menu', 9 ); - /** * Outputs a WP REST API nonce. */ diff --git a/packages/e2e-test-utils-playwright/src/admin/visit-site-editor.ts b/packages/e2e-test-utils-playwright/src/admin/visit-site-editor.ts index 0c374f2fe404b..ab66de030feb2 100644 --- a/packages/e2e-test-utils-playwright/src/admin/visit-site-editor.ts +++ b/packages/e2e-test-utils-playwright/src/admin/visit-site-editor.ts @@ -30,11 +30,10 @@ export async function visitSiteEditor( skipWelcomeGuide = true ) { const path = addQueryArgs( '', { - page: 'gutenberg-edit-site', ...query, } ).slice( 1 ); - await this.visitAdminPage( 'themes.php', path ); + await this.visitAdminPage( 'site-editor.php', path ); await this.page.waitForSelector( CANVAS_SELECTOR ); if ( skipWelcomeGuide ) { diff --git a/packages/e2e-test-utils/src/site-editor.js b/packages/e2e-test-utils/src/site-editor.js index e32ad01cc396e..a30d8b5455037 100644 --- a/packages/e2e-test-utils/src/site-editor.js +++ b/packages/e2e-test-utils/src/site-editor.js @@ -248,11 +248,10 @@ export async function siteEditorNavigateSequence( labels ) { */ export async function visitSiteEditor( query, skipWelcomeGuide = true ) { query = addQueryArgs( '', { - page: 'gutenberg-edit-site', ...query, } ).slice( 1 ); - await visitAdminPage( 'themes.php', query ); + await visitAdminPage( 'site-editor.php', query ); await page.waitForSelector( SELECTORS.visualEditor ); if ( skipWelcomeGuide ) { From 45249060ba1f0f76be7387a5453ef7ec29b72675 Mon Sep 17 00:00:00 2001 From: ntsekouras <ntsekouras@outlook.com> Date: Tue, 21 Jun 2022 09:53:27 +0300 Subject: [PATCH 05/13] remove v1 gallery files --- packages/block-library/src/gallery/v1/edit.js | 475 ------------------ .../src/gallery/v1/gallery-image.native.js | 349 ------------- 2 files changed, 824 deletions(-) delete mode 100644 packages/block-library/src/gallery/v1/edit.js delete mode 100644 packages/block-library/src/gallery/v1/gallery-image.native.js diff --git a/packages/block-library/src/gallery/v1/edit.js b/packages/block-library/src/gallery/v1/edit.js deleted file mode 100644 index e86fb49d4ad83..0000000000000 --- a/packages/block-library/src/gallery/v1/edit.js +++ /dev/null @@ -1,475 +0,0 @@ -/** - * External dependencies - */ -import { - every, - filter, - find, - forEach, - get, - isEmpty, - map, - reduce, - some, -} from 'lodash'; - -/** - * WordPress dependencies - */ -import { compose } from '@wordpress/compose'; -import { - PanelBody, - SelectControl, - ToggleControl, - withNotices, - RangeControl, -} from '@wordpress/components'; -import { - MediaPlaceholder, - InspectorControls, - useBlockProps, - store as blockEditorStore, -} from '@wordpress/block-editor'; -import { Platform, useEffect, useState, useMemo } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; -import { getBlobByURL, isBlobURL, revokeBlobURL } from '@wordpress/blob'; -import { useDispatch, useSelect } from '@wordpress/data'; -import { withViewportMatch } from '@wordpress/viewport'; -import { View } from '@wordpress/primitives'; -import { store as coreStore } from '@wordpress/core-data'; - -/** - * Internal dependencies - */ -import { sharedIcon } from '../shared-icon'; -import { pickRelevantMediaFiles } from './shared'; -import { defaultColumnsNumberV1 } from '../deprecated'; -import Gallery from './gallery'; -import { - LINK_DESTINATION_ATTACHMENT, - LINK_DESTINATION_MEDIA, - LINK_DESTINATION_NONE, -} from './constants'; - -const MAX_COLUMNS = 8; -const linkOptions = [ - { value: LINK_DESTINATION_ATTACHMENT, label: __( 'Attachment Page' ) }, - { value: LINK_DESTINATION_MEDIA, label: __( 'Media File' ) }, - { value: LINK_DESTINATION_NONE, label: __( 'None' ) }, -]; -const ALLOWED_MEDIA_TYPES = [ 'image' ]; - -const PLACEHOLDER_TEXT = Platform.select( { - web: __( - 'Drag images, upload new ones or select files from your library.' - ), - native: __( 'ADD MEDIA' ), -} ); - -const MOBILE_CONTROL_PROPS_RANGE_CONTROL = Platform.select( { - web: {}, - native: { type: 'stepper' }, -} ); - -function GalleryEdit( props ) { - const { - attributes, - clientId, - isSelected, - noticeUI, - noticeOperations, - onFocus, - } = props; - const { - columns = defaultColumnsNumberV1( attributes ), - imageCrop, - images, - linkTo, - sizeSlug, - } = attributes; - const [ selectedImage, setSelectedImage ] = useState(); - const [ attachmentCaptions, setAttachmentCaptions ] = useState(); - const { __unstableMarkNextChangeAsNotPersistent } = - useDispatch( blockEditorStore ); - - const { imageSizes, mediaUpload, getMedia, wasBlockJustInserted } = - useSelect( ( select ) => { - const settings = select( blockEditorStore ).getSettings(); - - return { - imageSizes: settings.imageSizes, - mediaUpload: settings.mediaUpload, - getMedia: select( coreStore ).getMedia, - wasBlockJustInserted: select( - blockEditorStore - ).wasBlockJustInserted( clientId, 'inserter_menu' ), - }; - } ); - - const resizedImages = useMemo( () => { - if ( isSelected ) { - return reduce( - attributes.ids, - ( currentResizedImages, id ) => { - if ( ! id ) { - return currentResizedImages; - } - const image = getMedia( id ); - const sizes = reduce( - imageSizes, - ( currentSizes, size ) => { - const defaultUrl = get( image, [ - 'sizes', - size.slug, - 'url', - ] ); - const mediaDetailsUrl = get( image, [ - 'media_details', - 'sizes', - size.slug, - 'source_url', - ] ); - return { - ...currentSizes, - [ size.slug ]: defaultUrl || mediaDetailsUrl, - }; - }, - {} - ); - return { - ...currentResizedImages, - [ parseInt( id, 10 ) ]: sizes, - }; - }, - {} - ); - } - return {}; - }, [ isSelected, attributes.ids, imageSizes ] ); - - function onFocusGalleryCaption() { - setSelectedImage(); - } - - function setAttributes( newAttrs ) { - if ( newAttrs.ids ) { - throw new Error( - 'The "ids" attribute should not be changed directly. It is managed automatically when "images" attribute changes' - ); - } - - if ( newAttrs.images ) { - newAttrs = { - ...newAttrs, - // Unlike images[ n ].id which is a string, always ensure the - // ids array contains numbers as per its attribute type. - ids: map( newAttrs.images, ( { id } ) => parseInt( id, 10 ) ), - }; - } - - props.setAttributes( newAttrs ); - } - - function onSelectImage( index ) { - return () => { - setSelectedImage( index ); - }; - } - - function onDeselectImage() { - return () => { - setSelectedImage(); - }; - } - - function onMove( oldIndex, newIndex ) { - const newImages = [ ...images ]; - newImages.splice( newIndex, 1, images[ oldIndex ] ); - newImages.splice( oldIndex, 1, images[ newIndex ] ); - setSelectedImage( newIndex ); - setAttributes( { images: newImages } ); - } - - function onMoveForward( oldIndex ) { - return () => { - if ( oldIndex === images.length - 1 ) { - return; - } - onMove( oldIndex, oldIndex + 1 ); - }; - } - - function onMoveBackward( oldIndex ) { - return () => { - if ( oldIndex === 0 ) { - return; - } - onMove( oldIndex, oldIndex - 1 ); - }; - } - - function onRemoveImage( index ) { - return () => { - const newImages = filter( images, ( img, i ) => index !== i ); - setSelectedImage(); - setAttributes( { - images: newImages, - columns: attributes.columns - ? Math.min( newImages.length, attributes.columns ) - : attributes.columns, - } ); - }; - } - - function selectCaption( newImage ) { - // The image id in both the images and attachmentCaptions arrays is a - // string, so ensure comparison works correctly by converting the - // newImage.id to a string. - const newImageId = newImage.id.toString(); - const currentImage = find( images, { id: newImageId } ); - const currentImageCaption = currentImage - ? currentImage.caption - : newImage.caption; - - if ( ! attachmentCaptions ) { - return currentImageCaption; - } - - const attachment = find( attachmentCaptions, { - id: newImageId, - } ); - - // If the attachment caption is updated. - if ( attachment && attachment.caption !== newImage.caption ) { - return newImage.caption; - } - - return currentImageCaption; - } - - function onSelectImages( newImages ) { - setAttachmentCaptions( - newImages.map( ( newImage ) => ( { - // Store the attachmentCaption id as a string for consistency - // with the type of the id in the images attribute. - id: newImage.id.toString(), - caption: newImage.caption, - } ) ) - ); - setAttributes( { - images: newImages.map( ( newImage ) => ( { - ...pickRelevantMediaFiles( newImage, sizeSlug ), - caption: selectCaption( newImage, images, attachmentCaptions ), - // The id value is stored in a data attribute, so when the - // block is parsed it's converted to a string. Converting - // to a string here ensures it's type is consistent. - id: newImage.id.toString(), - } ) ), - columns: attributes.columns - ? Math.min( newImages.length, attributes.columns ) - : attributes.columns, - } ); - } - - function onUploadError( message ) { - noticeOperations.removeAllNotices(); - noticeOperations.createErrorNotice( message ); - } - - function setLinkTo( value ) { - setAttributes( { linkTo: value } ); - } - - function setColumnsNumber( value ) { - setAttributes( { columns: value } ); - } - - function toggleImageCrop() { - setAttributes( { imageCrop: ! imageCrop } ); - } - - function getImageCropHelp( checked ) { - return checked - ? __( 'Thumbnails are cropped to align.' ) - : __( 'Thumbnails are not cropped.' ); - } - - function setImageAttributes( index, newAttributes ) { - if ( ! images[ index ] ) { - return; - } - - setAttributes( { - images: [ - ...images.slice( 0, index ), - { - ...images[ index ], - ...newAttributes, - }, - ...images.slice( index + 1 ), - ], - } ); - } - - function getImagesSizeOptions() { - return map( - filter( imageSizes, ( { slug } ) => - some( resizedImages, ( sizes ) => sizes[ slug ] ) - ), - ( { name, slug } ) => ( { value: slug, label: name } ) - ); - } - - function updateImagesSize( newSizeSlug ) { - const updatedImages = map( images, ( image ) => { - if ( ! image.id ) { - return image; - } - const url = get( resizedImages, [ - parseInt( image.id, 10 ), - newSizeSlug, - ] ); - return { - ...image, - ...( url && { url } ), - }; - } ); - - setAttributes( { images: updatedImages, sizeSlug: newSizeSlug } ); - } - - useEffect( () => { - if ( - Platform.OS === 'web' && - images && - images.length > 0 && - every( images, ( { url } ) => isBlobURL( url ) ) - ) { - const filesList = map( images, ( { url } ) => getBlobByURL( url ) ); - forEach( images, ( { url } ) => revokeBlobURL( url ) ); - mediaUpload( { - filesList, - onFileChange: onSelectImages, - allowedTypes: [ 'image' ], - } ); - } - }, [] ); - - useEffect( () => { - // Deselect images when deselecting the block. - if ( ! isSelected ) { - setSelectedImage(); - } - }, [ isSelected ] ); - - useEffect( () => { - // linkTo attribute must be saved so blocks don't break when changing - // image_default_link_type in options.php. - if ( ! linkTo ) { - __unstableMarkNextChangeAsNotPersistent(); - setAttributes( { - linkTo: - window?.wp?.media?.view?.settings?.defaultProps?.link || - LINK_DESTINATION_NONE, - } ); - } - }, [ linkTo ] ); - - const hasImages = !! images.length; - const hasImageIds = hasImages && images.some( ( image ) => !! image.id ); - - const mediaPlaceholder = ( - <MediaPlaceholder - addToGallery={ hasImageIds } - isAppender={ hasImages } - disableMediaButtons={ hasImages && ! isSelected } - icon={ ! hasImages && sharedIcon } - labels={ { - title: ! hasImages && __( 'Gallery' ), - instructions: ! hasImages && PLACEHOLDER_TEXT, - } } - onSelect={ onSelectImages } - accept="image/*" - allowedTypes={ ALLOWED_MEDIA_TYPES } - multiple - value={ hasImageIds ? images : {} } - onError={ onUploadError } - notices={ hasImages ? undefined : noticeUI } - onFocus={ onFocus } - autoOpenMediaUpload={ - ! hasImages && isSelected && wasBlockJustInserted - } - /> - ); - - const blockProps = useBlockProps(); - - if ( ! hasImages ) { - return <View { ...blockProps }>{ mediaPlaceholder }</View>; - } - - const imageSizeOptions = getImagesSizeOptions(); - const shouldShowSizeOptions = hasImages && ! isEmpty( imageSizeOptions ); - - return ( - <> - <InspectorControls> - <PanelBody title={ __( 'Settings' ) }> - { images.length > 1 && ( - <RangeControl - label={ __( 'Columns' ) } - value={ columns } - onChange={ setColumnsNumber } - min={ 1 } - max={ Math.min( MAX_COLUMNS, images.length ) } - { ...MOBILE_CONTROL_PROPS_RANGE_CONTROL } - required - /> - ) } - <ToggleControl - label={ __( 'Crop images' ) } - checked={ !! imageCrop } - onChange={ toggleImageCrop } - help={ getImageCropHelp } - /> - <SelectControl - label={ __( 'Link to' ) } - value={ linkTo } - onChange={ setLinkTo } - options={ linkOptions } - hideCancelButton={ true } - /> - { shouldShowSizeOptions && ( - <SelectControl - label={ __( 'Image size' ) } - value={ sizeSlug } - options={ imageSizeOptions } - onChange={ updateImagesSize } - hideCancelButton={ true } - /> - ) } - </PanelBody> - </InspectorControls> - { noticeUI } - <Gallery - { ...props } - selectedImage={ selectedImage } - mediaPlaceholder={ mediaPlaceholder } - onMoveBackward={ onMoveBackward } - onMoveForward={ onMoveForward } - onRemoveImage={ onRemoveImage } - onSelectImage={ onSelectImage } - onDeselectImage={ onDeselectImage } - onSetImageAttributes={ setImageAttributes } - blockProps={ blockProps } - // This prop is used by gallery.native.js. - onFocusGalleryCaption={ onFocusGalleryCaption } - /> - </> - ); -} - -export default compose( [ - withNotices, - withViewportMatch( { isNarrow: '< small' } ), -] )( GalleryEdit ); diff --git a/packages/block-library/src/gallery/v1/gallery-image.native.js b/packages/block-library/src/gallery/v1/gallery-image.native.js deleted file mode 100644 index 7fa6ab4ab7ea7..0000000000000 --- a/packages/block-library/src/gallery/v1/gallery-image.native.js +++ /dev/null @@ -1,349 +0,0 @@ -/** - * External dependencies - */ -import { - StyleSheet, - View, - ScrollView, - TouchableWithoutFeedback, -} from 'react-native'; -import { isEmpty } from 'lodash'; - -/** - * WordPress dependencies - */ -import { - requestImageFailedRetryDialog, - requestImageUploadCancelDialog, - requestImageFullscreenPreview, -} from '@wordpress/react-native-bridge'; -import { Component } from '@wordpress/element'; -import { Image } from '@wordpress/components'; -import { __, sprintf } from '@wordpress/i18n'; -import { Caption, MediaUploadProgress } from '@wordpress/block-editor'; -import { getProtocol } from '@wordpress/url'; -import { withPreferredColorScheme } from '@wordpress/compose'; -import { arrowLeft, arrowRight, warning } from '@wordpress/icons'; - -/** - * Internal dependencies - */ -import Button from './gallery-button'; -import style from './gallery-image-style.scss'; - -const { compose } = StyleSheet; - -const separatorStyle = compose( style.separator, { - borderRightWidth: StyleSheet.hairlineWidth, -} ); -const buttonStyle = compose( style.button, { aspectRatio: 1 } ); -const ICON_SIZE_ARROW = 15; - -class GalleryImage extends Component { - constructor() { - super( ...arguments ); - - this.onSelectImage = this.onSelectImage.bind( this ); - this.onSelectCaption = this.onSelectCaption.bind( this ); - this.onMediaPressed = this.onMediaPressed.bind( this ); - this.onCaptionChange = this.onCaptionChange.bind( this ); - this.onSelectMedia = this.onSelectMedia.bind( this ); - - this.updateMediaProgress = this.updateMediaProgress.bind( this ); - this.finishMediaUploadWithSuccess = - this.finishMediaUploadWithSuccess.bind( this ); - this.finishMediaUploadWithFailure = - this.finishMediaUploadWithFailure.bind( this ); - this.renderContent = this.renderContent.bind( this ); - - this.state = { - captionSelected: false, - isUploadInProgress: false, - didUploadFail: false, - }; - } - - onSelectCaption() { - if ( ! this.state.captionSelected ) { - this.setState( { - captionSelected: true, - } ); - } - - if ( ! this.props.isSelected ) { - this.props.onSelect(); - } - } - - onMediaPressed() { - const { id, url, isSelected } = this.props; - const { captionSelected, isUploadInProgress, didUploadFail } = - this.state; - - this.onSelectImage(); - - if ( isUploadInProgress ) { - requestImageUploadCancelDialog( id ); - } else if ( - didUploadFail || - ( id && getProtocol( url ) === 'file:' ) - ) { - requestImageFailedRetryDialog( id ); - } else if ( isSelected && ! captionSelected ) { - requestImageFullscreenPreview( url ); - } - } - - onSelectImage() { - if ( ! this.props.isBlockSelected ) { - this.props.onSelectBlock(); - } - - if ( ! this.props.isSelected ) { - this.props.onSelect(); - } - - if ( this.state.captionSelected ) { - this.setState( { - captionSelected: false, - } ); - } - } - - onSelectMedia( media ) { - const { setAttributes } = this.props; - setAttributes( media ); - } - - onCaptionChange( caption ) { - const { setAttributes } = this.props; - setAttributes( { caption } ); - } - - componentDidUpdate( prevProps ) { - const { isSelected, image, url } = this.props; - if ( image && ! url ) { - this.props.setAttributes( { - url: image.source_url, - alt: image.alt_text, - } ); - } - - // Unselect the caption so when the user selects other image and comeback - // the caption is not immediately selected. - if ( - this.state.captionSelected && - ! isSelected && - prevProps.isSelected - ) { - this.setState( { - captionSelected: false, - } ); - } - } - - updateMediaProgress() { - if ( ! this.state.isUploadInProgress ) { - this.setState( { isUploadInProgress: true } ); - } - } - - finishMediaUploadWithSuccess( payload ) { - this.setState( { - isUploadInProgress: false, - didUploadFail: false, - } ); - - this.props.setAttributes( { - id: payload.mediaServerId, - url: payload.mediaUrl, - } ); - } - - finishMediaUploadWithFailure() { - this.setState( { - isUploadInProgress: false, - didUploadFail: true, - } ); - } - - renderContent( params ) { - const { - url, - isFirstItem, - isLastItem, - isSelected, - caption, - onRemove, - onMoveForward, - onMoveBackward, - 'aria-label': ariaLabel, - isCropped, - getStylesFromColorScheme, - isRTL, - } = this.props; - - const { isUploadInProgress, captionSelected } = this.state; - const { isUploadFailed, retryMessage } = params; - const resizeMode = isCropped ? 'cover' : 'contain'; - - const captionPlaceholderStyle = getStylesFromColorScheme( - style.captionPlaceholder, - style.captionPlaceholderDark - ); - - const shouldShowCaptionEditable = ! isUploadFailed && isSelected; - const shouldShowCaptionExpanded = - ! isUploadFailed && ! isSelected && !! caption; - - const captionContainerStyle = shouldShowCaptionExpanded - ? style.captionExpandedContainer - : style.captionContainer; - - const captionStyle = shouldShowCaptionExpanded - ? style.captionExpanded - : style.caption; - - const mediaPickerOptions = [ - { - destructiveButton: true, - id: 'removeImage', - label: __( 'Remove' ), - onPress: onRemove, - separated: true, - value: 'removeImage', - }, - ]; - - return ( - <> - <Image - alt={ ariaLabel } - height={ style.image.height } - isSelected={ isSelected } - isUploadFailed={ isUploadFailed } - isUploadInProgress={ isUploadInProgress } - mediaPickerOptions={ mediaPickerOptions } - onSelectMediaUploadOption={ this.onSelectMedia } - resizeMode={ resizeMode } - url={ url } - retryMessage={ retryMessage } - retryIcon={ warning } - /> - - { ! isUploadInProgress && isSelected && ( - <View style={ style.toolbarContainer }> - <View style={ style.toolbar }> - <View style={ style.moverButtonContainer }> - <Button - style={ buttonStyle } - icon={ isRTL ? arrowRight : arrowLeft } - iconSize={ ICON_SIZE_ARROW } - onClick={ - isFirstItem ? undefined : onMoveBackward - } - accessibilityLabel={ __( - 'Move Image Backward' - ) } - aria-disabled={ isFirstItem } - disabled={ ! isSelected } - /> - <View style={ separatorStyle } /> - <Button - style={ buttonStyle } - icon={ isRTL ? arrowLeft : arrowRight } - iconSize={ ICON_SIZE_ARROW } - onClick={ - isLastItem ? undefined : onMoveForward - } - accessibilityLabel={ __( - 'Move Image Forward' - ) } - aria-disabled={ isLastItem } - disabled={ ! isSelected } - /> - </View> - </View> - </View> - ) } - - { ! isUploadInProgress && - ( shouldShowCaptionEditable || - shouldShowCaptionExpanded ) && ( - <View style={ captionContainerStyle }> - <ScrollView - nestedScrollEnabled - keyboardShouldPersistTaps="handled" - bounces={ false } - > - <Caption - inlineToolbar - isSelected={ isSelected && captionSelected } - onChange={ this.onCaptionChange } - onFocus={ this.onSelectCaption } - placeholder={ - isSelected ? __( 'Add caption' ) : null - } - placeholderTextColor={ - captionPlaceholderStyle.color - } - style={ captionStyle } - value={ caption } - /> - </ScrollView> - </View> - ) } - </> - ); - } - - render() { - const { id, onRemove, getStylesFromColorScheme, isSelected } = - this.props; - - const containerStyle = getStylesFromColorScheme( - style.galleryImageContainer, - style.galleryImageContainerDark - ); - - return ( - <TouchableWithoutFeedback - onPress={ this.onMediaPressed } - accessible={ ! isSelected } // We need only child views to be accessible after the selection. - accessibilityLabel={ this.accessibilityLabelImageContainer() } // if we don't set this explicitly it reads system provided accessibilityLabels of all child components and those include pretty technical words which don't make sense - accessibilityRole={ 'imagebutton' } // this makes VoiceOver to read a description of image provided by system on iOS and lets user know this is a button which conveys the message of tappablity - > - <View style={ containerStyle }> - <MediaUploadProgress - mediaId={ id } - onUpdateMediaProgress={ this.updateMediaProgress } - onFinishMediaUploadWithSuccess={ - this.finishMediaUploadWithSuccess - } - onFinishMediaUploadWithFailure={ - this.finishMediaUploadWithFailure - } - onMediaUploadStateReset={ onRemove } - renderContent={ this.renderContent } - /> - </View> - </TouchableWithoutFeedback> - ); - } - - accessibilityLabelImageContainer() { - const { caption, 'aria-label': ariaLabel } = this.props; - - return isEmpty( caption ) - ? ariaLabel - : ariaLabel + - '. ' + - sprintf( - /* translators: accessibility text. %s: image caption. */ - __( 'Image caption. %s' ), - caption - ); - } -} - -export default withPreferredColorScheme( GalleryImage ); From a4b71abb30e608345834c984981a259c31488f63 Mon Sep 17 00:00:00 2001 From: ntsekouras <ntsekouras@outlook.com> Date: Tue, 28 Jun 2022 21:04:58 +0300 Subject: [PATCH 06/13] revert gallery udpates --- .../block-library/src/gallery/deprecated.js | 63 ++- .../block-library/src/gallery/edit-wrapper.js | 27 + .../src/gallery/gallery-styles.native.scss | 2 + packages/block-library/src/gallery/index.js | 2 +- packages/block-library/src/gallery/save.js | 10 + .../block-library/src/gallery/transforms.js | 169 +++++-- .../block-library/src/gallery/v1/constants.js | 3 + packages/block-library/src/gallery/v1/edit.js | 475 ++++++++++++++++++ .../src/gallery/v1/gallery-button.native.js | 47 ++ .../v1/gallery-image-style.native.scss | 109 ++++ .../src/gallery/v1/gallery-image.js | 282 +++++++++++ .../src/gallery/v1/gallery-image.native.js | 349 +++++++++++++ .../src/gallery/v1/gallery-styles.native.scss | 8 + .../block-library/src/gallery/v1/gallery.js | 125 +++++ .../src/gallery/v1/gallery.native.js | 162 ++++++ packages/block-library/src/gallery/v1/save.js | 98 ++++ .../block-library/src/gallery/v1/shared.js | 19 + .../src/gallery/v1/tiles-styles.native.scss | 11 + .../src/gallery/v1/tiles.native.js | 79 +++ 19 files changed, 1986 insertions(+), 54 deletions(-) create mode 100644 packages/block-library/src/gallery/edit-wrapper.js create mode 100644 packages/block-library/src/gallery/v1/constants.js create mode 100644 packages/block-library/src/gallery/v1/edit.js create mode 100644 packages/block-library/src/gallery/v1/gallery-button.native.js create mode 100644 packages/block-library/src/gallery/v1/gallery-image-style.native.scss create mode 100644 packages/block-library/src/gallery/v1/gallery-image.js create mode 100644 packages/block-library/src/gallery/v1/gallery-image.native.js create mode 100644 packages/block-library/src/gallery/v1/gallery-styles.native.scss create mode 100644 packages/block-library/src/gallery/v1/gallery.js create mode 100644 packages/block-library/src/gallery/v1/gallery.native.js create mode 100644 packages/block-library/src/gallery/v1/save.js create mode 100644 packages/block-library/src/gallery/v1/shared.js create mode 100644 packages/block-library/src/gallery/v1/tiles-styles.native.scss create mode 100644 packages/block-library/src/gallery/v1/tiles.native.js diff --git a/packages/block-library/src/gallery/deprecated.js b/packages/block-library/src/gallery/deprecated.js index 1ac1034cb9b60..e71b87d2ab791 100644 --- a/packages/block-library/src/gallery/deprecated.js +++ b/packages/block-library/src/gallery/deprecated.js @@ -2,7 +2,7 @@ * External dependencies */ import classnames from 'classnames'; -import { some, omit } from 'lodash'; +import { map, some, omit } from 'lodash'; /** * WordPress dependencies @@ -19,6 +19,7 @@ import { LINK_DESTINATION_MEDIA, LINK_DESTINATION_NONE, } from './constants'; +import { isGalleryV2Enabled } from './shared'; const DEPRECATED_LINK_DESTINATION_MEDIA = 'file'; const DEPRECATED_LINK_DESTINATION_ATTACHMENT = 'post'; @@ -281,7 +282,11 @@ const v6 = { ); }, migrate( attributes ) { - return runV2Migration( attributes ); + if ( isGalleryV2Enabled() ) { + return runV2Migration( attributes ); + } + + return attributes; }, }; const v5 = { @@ -367,7 +372,23 @@ const v5 = { return ! linkTo || linkTo === 'attachment' || linkTo === 'media'; }, migrate( attributes ) { - return runV2Migration( attributes ); + if ( isGalleryV2Enabled() ) { + return runV2Migration( attributes ); + } + + let linkTo = attributes.linkTo; + + if ( ! attributes.linkTo ) { + linkTo = 'none'; + } else if ( attributes.linkTo === 'attachment' ) { + linkTo = 'post'; + } else if ( attributes.linkTo === 'media' ) { + linkTo = 'file'; + } + return { + ...attributes, + linkTo, + }; }, save( { attributes } ) { const { @@ -514,7 +535,17 @@ const v4 = { return ids && ids.some( ( id ) => typeof id === 'string' ); }, migrate( attributes ) { - return runV2Migration( attributes ); + if ( isGalleryV2Enabled() ) { + return runV2Migration( attributes ); + } + + return { + ...attributes, + ids: map( attributes.ids, ( id ) => { + const parsedId = parseInt( id, 10 ); + return Number.isInteger( parsedId ) ? parsedId : null; + } ), + }; }, save( { attributes } ) { const { @@ -710,7 +741,10 @@ const v3 = { ); }, migrate( attributes ) { - return runV2Migration( attributes ); + if ( isGalleryV2Enabled() ) { + return runV2Migration( attributes ); + } + return attributes; }, }; const v2 = { @@ -776,7 +810,18 @@ const v2 = { ); }, migrate( attributes ) { - return runV2Migration( attributes ); + if ( isGalleryV2Enabled() ) { + return runV2Migration( attributes ); + } + return { + ...attributes, + ids: map( attributes.images, ( { id } ) => { + if ( ! id ) { + return null; + } + return parseInt( id, 10 ); + } ), + }; }, supports: { align: true, @@ -929,7 +974,11 @@ const v1 = { ); }, migrate( attributes ) { - return runV2Migration( attributes ); + if ( isGalleryV2Enabled() ) { + return runV2Migration( attributes ); + } + + return attributes; }, }; diff --git a/packages/block-library/src/gallery/edit-wrapper.js b/packages/block-library/src/gallery/edit-wrapper.js new file mode 100644 index 0000000000000..2c81271902d31 --- /dev/null +++ b/packages/block-library/src/gallery/edit-wrapper.js @@ -0,0 +1,27 @@ +/** + * WordPress dependencies + */ +import { compose } from '@wordpress/compose'; +import { withNotices } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import EditWithInnerBlocks from './edit'; +import EditWithoutInnerBlocks from './v1/edit'; +import { isGalleryV2Enabled } from './shared'; + +/* + * Using a wrapper around the logic to load the edit for v1 of Gallery block + * or the refactored version with InnerBlocks. This is to prevent conditional + * use of hooks lint errors if adding this logic to the top of the edit component. + */ +function GalleryEditWrapper( props ) { + if ( ! isGalleryV2Enabled() ) { + return <EditWithoutInnerBlocks { ...props } />; + } + + return <EditWithInnerBlocks { ...props } />; +} + +export default compose( [ withNotices ] )( GalleryEditWrapper ); diff --git a/packages/block-library/src/gallery/gallery-styles.native.scss b/packages/block-library/src/gallery/gallery-styles.native.scss index f9b4d63c55b81..a3073592291b9 100644 --- a/packages/block-library/src/gallery/gallery-styles.native.scss +++ b/packages/block-library/src/gallery/gallery-styles.native.scss @@ -1,3 +1,5 @@ +@import "./v1/gallery-styles.native.scss"; + .galleryAppender { padding-top: $grid-unit-20; } diff --git a/packages/block-library/src/gallery/index.js b/packages/block-library/src/gallery/index.js index ca74aec021b28..c9b82278a507a 100644 --- a/packages/block-library/src/gallery/index.js +++ b/packages/block-library/src/gallery/index.js @@ -7,7 +7,7 @@ import { gallery as icon } from '@wordpress/icons'; * Internal dependencies */ import deprecated from './deprecated'; -import edit from './edit'; +import edit from './edit-wrapper'; import metadata from './block.json'; import save from './save'; import transforms from './transforms'; diff --git a/packages/block-library/src/gallery/save.js b/packages/block-library/src/gallery/save.js index acb689e4427ba..66253001e6418 100644 --- a/packages/block-library/src/gallery/save.js +++ b/packages/block-library/src/gallery/save.js @@ -13,7 +13,17 @@ import { __experimentalGetElementClassName, } from '@wordpress/block-editor'; +/** + * Internal dependencies + */ +import saveWithoutInnerBlocks from './v1/save'; +import { isGalleryV2Enabled } from './shared'; + export default function saveWithInnerBlocks( { attributes } ) { + if ( ! isGalleryV2Enabled() ) { + return saveWithoutInnerBlocks( { attributes } ); + } + const { caption, columns, imageCrop } = attributes; const className = classnames( 'has-nested-images', { diff --git a/packages/block-library/src/gallery/transforms.js b/packages/block-library/src/gallery/transforms.js index 74d5e1149f908..53a08bd590e71 100644 --- a/packages/block-library/src/gallery/transforms.js +++ b/packages/block-library/src/gallery/transforms.js @@ -18,6 +18,11 @@ import { LINK_DESTINATION_NONE, LINK_DESTINATION_MEDIA, } from './constants'; +import { + LINK_DESTINATION_ATTACHMENT as DEPRECATED_LINK_DESTINATION_ATTACHMENT, + LINK_DESTINATION_MEDIA as DEPRECATED_LINK_DESTINATION_MEDIA, +} from './v1/constants'; +import { pickRelevantMediaFiles, isGalleryV2Enabled } from './shared'; const parseShortcodeIds = ( ids ) => { if ( ! ids ) { @@ -43,6 +48,7 @@ const parseShortcodeIds = ( ids ) => { */ function updateThirdPartyTransformToGallery( block ) { if ( + isGalleryV2Enabled() && block.name === 'core/gallery' && block.attributes?.images.length > 0 ) { @@ -136,18 +142,34 @@ const transforms = { const validImages = filter( attributes, ( { url } ) => url ); - const innerBlocks = validImages.map( ( image ) => { - return createBlock( 'core/image', image ); - } ); + if ( isGalleryV2Enabled() ) { + const innerBlocks = validImages.map( ( image ) => { + return createBlock( 'core/image', image ); + } ); - return createBlock( - 'core/gallery', - { - align, - sizeSlug, - }, - innerBlocks - ); + return createBlock( + 'core/gallery', + { + align, + sizeSlug, + }, + innerBlocks + ); + } + + return createBlock( 'core/gallery', { + images: validImages.map( + ( { id, url, alt, caption } ) => ( { + id: id.toString(), + url, + alt, + caption, + } ) + ), + ids: validImages.map( ( { id } ) => parseInt( id, 10 ) ), + align, + sizeSlug, + } ); }, }, { @@ -155,12 +177,32 @@ const transforms = { tag: 'gallery', attributes: { + images: { + type: 'array', + shortcode: ( { named: { ids } } ) => { + if ( ! isGalleryV2Enabled() ) { + return parseShortcodeIds( ids ).map( ( id ) => ( { + id: id.toString(), + } ) ); + } + }, + }, + ids: { + type: 'array', + shortcode: ( { named: { ids } } ) => { + if ( ! isGalleryV2Enabled() ) { + return parseShortcodeIds( ids ); + } + }, + }, shortCodeTransforms: { type: 'array', shortcode: ( { named: { ids } } ) => { - return parseShortcodeIds( ids ).map( ( id ) => ( { - id: parseInt( id ), - } ) ); + if ( isGalleryV2Enabled() ) { + return parseShortcodeIds( ids ).map( ( id ) => ( { + id: parseInt( id ), + } ) ); + } }, }, columns: { @@ -172,6 +214,16 @@ const transforms = { linkTo: { type: 'string', shortcode: ( { named: { link } } ) => { + if ( ! isGalleryV2Enabled() ) { + switch ( link ) { + case 'post': + return DEPRECATED_LINK_DESTINATION_ATTACHMENT; + case 'file': + return DEPRECATED_LINK_DESTINATION_MEDIA; + default: + return DEPRECATED_LINK_DESTINATION_ATTACHMENT; + } + } switch ( link ) { case 'post': return LINK_DESTINATION_ATTACHMENT; @@ -205,13 +257,23 @@ const transforms = { ); }, transform( files ) { - const innerBlocks = files.map( ( file ) => - createBlock( 'core/image', { - url: createBlobURL( file ), - } ) - ); + if ( isGalleryV2Enabled() ) { + const innerBlocks = files.map( ( file ) => + createBlock( 'core/image', { + url: createBlobURL( file ), + } ) + ); - return createBlock( 'core/gallery', {}, innerBlocks ); + return createBlock( 'core/gallery', {}, innerBlocks ); + } + const block = createBlock( 'core/gallery', { + images: files.map( ( file ) => + pickRelevantMediaFiles( { + url: createBlobURL( file ), + } ) + ), + } ); + return block; }, }, ], @@ -219,32 +281,47 @@ const transforms = { { type: 'block', blocks: [ 'core/image' ], - transform: ( { align }, innerBlocks ) => { - if ( innerBlocks.length > 0 ) { - return innerBlocks.map( - ( { - attributes: { - id, - url, - alt, - caption, - sizeSlug: imageSizeSlug, - linkDestination, - href, - linkTarget, - }, - } ) => - createBlock( 'core/image', { - id, - url, - alt, - caption, - sizeSlug: imageSizeSlug, - align, - linkDestination, - href, - linkTarget, - } ) + transform: ( { align, images, ids, sizeSlug }, innerBlocks ) => { + if ( isGalleryV2Enabled() ) { + if ( innerBlocks.length > 0 ) { + return innerBlocks.map( + ( { + attributes: { + id, + url, + alt, + caption, + sizeSlug: imageSizeSlug, + linkDestination, + href, + linkTarget, + }, + } ) => + createBlock( 'core/image', { + id, + url, + alt, + caption, + sizeSlug: imageSizeSlug, + align, + linkDestination, + href, + linkTarget, + } ) + ); + } + return createBlock( 'core/image', { align } ); + } + if ( images.length > 0 ) { + return images.map( ( { url, alt, caption }, index ) => + createBlock( 'core/image', { + id: ids[ index ], + url, + alt, + caption, + align, + sizeSlug, + } ) ); } return createBlock( 'core/image', { align } ); diff --git a/packages/block-library/src/gallery/v1/constants.js b/packages/block-library/src/gallery/v1/constants.js new file mode 100644 index 0000000000000..f4b6e7af56d47 --- /dev/null +++ b/packages/block-library/src/gallery/v1/constants.js @@ -0,0 +1,3 @@ +export const LINK_DESTINATION_NONE = 'none'; +export const LINK_DESTINATION_MEDIA = 'file'; +export const LINK_DESTINATION_ATTACHMENT = 'post'; diff --git a/packages/block-library/src/gallery/v1/edit.js b/packages/block-library/src/gallery/v1/edit.js new file mode 100644 index 0000000000000..e86fb49d4ad83 --- /dev/null +++ b/packages/block-library/src/gallery/v1/edit.js @@ -0,0 +1,475 @@ +/** + * External dependencies + */ +import { + every, + filter, + find, + forEach, + get, + isEmpty, + map, + reduce, + some, +} from 'lodash'; + +/** + * WordPress dependencies + */ +import { compose } from '@wordpress/compose'; +import { + PanelBody, + SelectControl, + ToggleControl, + withNotices, + RangeControl, +} from '@wordpress/components'; +import { + MediaPlaceholder, + InspectorControls, + useBlockProps, + store as blockEditorStore, +} from '@wordpress/block-editor'; +import { Platform, useEffect, useState, useMemo } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { getBlobByURL, isBlobURL, revokeBlobURL } from '@wordpress/blob'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { withViewportMatch } from '@wordpress/viewport'; +import { View } from '@wordpress/primitives'; +import { store as coreStore } from '@wordpress/core-data'; + +/** + * Internal dependencies + */ +import { sharedIcon } from '../shared-icon'; +import { pickRelevantMediaFiles } from './shared'; +import { defaultColumnsNumberV1 } from '../deprecated'; +import Gallery from './gallery'; +import { + LINK_DESTINATION_ATTACHMENT, + LINK_DESTINATION_MEDIA, + LINK_DESTINATION_NONE, +} from './constants'; + +const MAX_COLUMNS = 8; +const linkOptions = [ + { value: LINK_DESTINATION_ATTACHMENT, label: __( 'Attachment Page' ) }, + { value: LINK_DESTINATION_MEDIA, label: __( 'Media File' ) }, + { value: LINK_DESTINATION_NONE, label: __( 'None' ) }, +]; +const ALLOWED_MEDIA_TYPES = [ 'image' ]; + +const PLACEHOLDER_TEXT = Platform.select( { + web: __( + 'Drag images, upload new ones or select files from your library.' + ), + native: __( 'ADD MEDIA' ), +} ); + +const MOBILE_CONTROL_PROPS_RANGE_CONTROL = Platform.select( { + web: {}, + native: { type: 'stepper' }, +} ); + +function GalleryEdit( props ) { + const { + attributes, + clientId, + isSelected, + noticeUI, + noticeOperations, + onFocus, + } = props; + const { + columns = defaultColumnsNumberV1( attributes ), + imageCrop, + images, + linkTo, + sizeSlug, + } = attributes; + const [ selectedImage, setSelectedImage ] = useState(); + const [ attachmentCaptions, setAttachmentCaptions ] = useState(); + const { __unstableMarkNextChangeAsNotPersistent } = + useDispatch( blockEditorStore ); + + const { imageSizes, mediaUpload, getMedia, wasBlockJustInserted } = + useSelect( ( select ) => { + const settings = select( blockEditorStore ).getSettings(); + + return { + imageSizes: settings.imageSizes, + mediaUpload: settings.mediaUpload, + getMedia: select( coreStore ).getMedia, + wasBlockJustInserted: select( + blockEditorStore + ).wasBlockJustInserted( clientId, 'inserter_menu' ), + }; + } ); + + const resizedImages = useMemo( () => { + if ( isSelected ) { + return reduce( + attributes.ids, + ( currentResizedImages, id ) => { + if ( ! id ) { + return currentResizedImages; + } + const image = getMedia( id ); + const sizes = reduce( + imageSizes, + ( currentSizes, size ) => { + const defaultUrl = get( image, [ + 'sizes', + size.slug, + 'url', + ] ); + const mediaDetailsUrl = get( image, [ + 'media_details', + 'sizes', + size.slug, + 'source_url', + ] ); + return { + ...currentSizes, + [ size.slug ]: defaultUrl || mediaDetailsUrl, + }; + }, + {} + ); + return { + ...currentResizedImages, + [ parseInt( id, 10 ) ]: sizes, + }; + }, + {} + ); + } + return {}; + }, [ isSelected, attributes.ids, imageSizes ] ); + + function onFocusGalleryCaption() { + setSelectedImage(); + } + + function setAttributes( newAttrs ) { + if ( newAttrs.ids ) { + throw new Error( + 'The "ids" attribute should not be changed directly. It is managed automatically when "images" attribute changes' + ); + } + + if ( newAttrs.images ) { + newAttrs = { + ...newAttrs, + // Unlike images[ n ].id which is a string, always ensure the + // ids array contains numbers as per its attribute type. + ids: map( newAttrs.images, ( { id } ) => parseInt( id, 10 ) ), + }; + } + + props.setAttributes( newAttrs ); + } + + function onSelectImage( index ) { + return () => { + setSelectedImage( index ); + }; + } + + function onDeselectImage() { + return () => { + setSelectedImage(); + }; + } + + function onMove( oldIndex, newIndex ) { + const newImages = [ ...images ]; + newImages.splice( newIndex, 1, images[ oldIndex ] ); + newImages.splice( oldIndex, 1, images[ newIndex ] ); + setSelectedImage( newIndex ); + setAttributes( { images: newImages } ); + } + + function onMoveForward( oldIndex ) { + return () => { + if ( oldIndex === images.length - 1 ) { + return; + } + onMove( oldIndex, oldIndex + 1 ); + }; + } + + function onMoveBackward( oldIndex ) { + return () => { + if ( oldIndex === 0 ) { + return; + } + onMove( oldIndex, oldIndex - 1 ); + }; + } + + function onRemoveImage( index ) { + return () => { + const newImages = filter( images, ( img, i ) => index !== i ); + setSelectedImage(); + setAttributes( { + images: newImages, + columns: attributes.columns + ? Math.min( newImages.length, attributes.columns ) + : attributes.columns, + } ); + }; + } + + function selectCaption( newImage ) { + // The image id in both the images and attachmentCaptions arrays is a + // string, so ensure comparison works correctly by converting the + // newImage.id to a string. + const newImageId = newImage.id.toString(); + const currentImage = find( images, { id: newImageId } ); + const currentImageCaption = currentImage + ? currentImage.caption + : newImage.caption; + + if ( ! attachmentCaptions ) { + return currentImageCaption; + } + + const attachment = find( attachmentCaptions, { + id: newImageId, + } ); + + // If the attachment caption is updated. + if ( attachment && attachment.caption !== newImage.caption ) { + return newImage.caption; + } + + return currentImageCaption; + } + + function onSelectImages( newImages ) { + setAttachmentCaptions( + newImages.map( ( newImage ) => ( { + // Store the attachmentCaption id as a string for consistency + // with the type of the id in the images attribute. + id: newImage.id.toString(), + caption: newImage.caption, + } ) ) + ); + setAttributes( { + images: newImages.map( ( newImage ) => ( { + ...pickRelevantMediaFiles( newImage, sizeSlug ), + caption: selectCaption( newImage, images, attachmentCaptions ), + // The id value is stored in a data attribute, so when the + // block is parsed it's converted to a string. Converting + // to a string here ensures it's type is consistent. + id: newImage.id.toString(), + } ) ), + columns: attributes.columns + ? Math.min( newImages.length, attributes.columns ) + : attributes.columns, + } ); + } + + function onUploadError( message ) { + noticeOperations.removeAllNotices(); + noticeOperations.createErrorNotice( message ); + } + + function setLinkTo( value ) { + setAttributes( { linkTo: value } ); + } + + function setColumnsNumber( value ) { + setAttributes( { columns: value } ); + } + + function toggleImageCrop() { + setAttributes( { imageCrop: ! imageCrop } ); + } + + function getImageCropHelp( checked ) { + return checked + ? __( 'Thumbnails are cropped to align.' ) + : __( 'Thumbnails are not cropped.' ); + } + + function setImageAttributes( index, newAttributes ) { + if ( ! images[ index ] ) { + return; + } + + setAttributes( { + images: [ + ...images.slice( 0, index ), + { + ...images[ index ], + ...newAttributes, + }, + ...images.slice( index + 1 ), + ], + } ); + } + + function getImagesSizeOptions() { + return map( + filter( imageSizes, ( { slug } ) => + some( resizedImages, ( sizes ) => sizes[ slug ] ) + ), + ( { name, slug } ) => ( { value: slug, label: name } ) + ); + } + + function updateImagesSize( newSizeSlug ) { + const updatedImages = map( images, ( image ) => { + if ( ! image.id ) { + return image; + } + const url = get( resizedImages, [ + parseInt( image.id, 10 ), + newSizeSlug, + ] ); + return { + ...image, + ...( url && { url } ), + }; + } ); + + setAttributes( { images: updatedImages, sizeSlug: newSizeSlug } ); + } + + useEffect( () => { + if ( + Platform.OS === 'web' && + images && + images.length > 0 && + every( images, ( { url } ) => isBlobURL( url ) ) + ) { + const filesList = map( images, ( { url } ) => getBlobByURL( url ) ); + forEach( images, ( { url } ) => revokeBlobURL( url ) ); + mediaUpload( { + filesList, + onFileChange: onSelectImages, + allowedTypes: [ 'image' ], + } ); + } + }, [] ); + + useEffect( () => { + // Deselect images when deselecting the block. + if ( ! isSelected ) { + setSelectedImage(); + } + }, [ isSelected ] ); + + useEffect( () => { + // linkTo attribute must be saved so blocks don't break when changing + // image_default_link_type in options.php. + if ( ! linkTo ) { + __unstableMarkNextChangeAsNotPersistent(); + setAttributes( { + linkTo: + window?.wp?.media?.view?.settings?.defaultProps?.link || + LINK_DESTINATION_NONE, + } ); + } + }, [ linkTo ] ); + + const hasImages = !! images.length; + const hasImageIds = hasImages && images.some( ( image ) => !! image.id ); + + const mediaPlaceholder = ( + <MediaPlaceholder + addToGallery={ hasImageIds } + isAppender={ hasImages } + disableMediaButtons={ hasImages && ! isSelected } + icon={ ! hasImages && sharedIcon } + labels={ { + title: ! hasImages && __( 'Gallery' ), + instructions: ! hasImages && PLACEHOLDER_TEXT, + } } + onSelect={ onSelectImages } + accept="image/*" + allowedTypes={ ALLOWED_MEDIA_TYPES } + multiple + value={ hasImageIds ? images : {} } + onError={ onUploadError } + notices={ hasImages ? undefined : noticeUI } + onFocus={ onFocus } + autoOpenMediaUpload={ + ! hasImages && isSelected && wasBlockJustInserted + } + /> + ); + + const blockProps = useBlockProps(); + + if ( ! hasImages ) { + return <View { ...blockProps }>{ mediaPlaceholder }</View>; + } + + const imageSizeOptions = getImagesSizeOptions(); + const shouldShowSizeOptions = hasImages && ! isEmpty( imageSizeOptions ); + + return ( + <> + <InspectorControls> + <PanelBody title={ __( 'Settings' ) }> + { images.length > 1 && ( + <RangeControl + label={ __( 'Columns' ) } + value={ columns } + onChange={ setColumnsNumber } + min={ 1 } + max={ Math.min( MAX_COLUMNS, images.length ) } + { ...MOBILE_CONTROL_PROPS_RANGE_CONTROL } + required + /> + ) } + <ToggleControl + label={ __( 'Crop images' ) } + checked={ !! imageCrop } + onChange={ toggleImageCrop } + help={ getImageCropHelp } + /> + <SelectControl + label={ __( 'Link to' ) } + value={ linkTo } + onChange={ setLinkTo } + options={ linkOptions } + hideCancelButton={ true } + /> + { shouldShowSizeOptions && ( + <SelectControl + label={ __( 'Image size' ) } + value={ sizeSlug } + options={ imageSizeOptions } + onChange={ updateImagesSize } + hideCancelButton={ true } + /> + ) } + </PanelBody> + </InspectorControls> + { noticeUI } + <Gallery + { ...props } + selectedImage={ selectedImage } + mediaPlaceholder={ mediaPlaceholder } + onMoveBackward={ onMoveBackward } + onMoveForward={ onMoveForward } + onRemoveImage={ onRemoveImage } + onSelectImage={ onSelectImage } + onDeselectImage={ onDeselectImage } + onSetImageAttributes={ setImageAttributes } + blockProps={ blockProps } + // This prop is used by gallery.native.js. + onFocusGalleryCaption={ onFocusGalleryCaption } + /> + </> + ); +} + +export default compose( [ + withNotices, + withViewportMatch( { isNarrow: '< small' } ), +] )( GalleryEdit ); diff --git a/packages/block-library/src/gallery/v1/gallery-button.native.js b/packages/block-library/src/gallery/v1/gallery-button.native.js new file mode 100644 index 0000000000000..8804e99cf2e7e --- /dev/null +++ b/packages/block-library/src/gallery/v1/gallery-button.native.js @@ -0,0 +1,47 @@ +/** + * External dependencies + */ +import { StyleSheet, TouchableOpacity } from 'react-native'; + +/** + * WordPress dependencies + */ +import { Icon } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import style from './gallery-image-style.scss'; + +export function Button( props ) { + const { + icon, + iconSize = 24, + onClick, + disabled, + 'aria-disabled': ariaDisabled, + accessibilityLabel = 'button', + style: customStyle, + } = props; + + const buttonStyle = StyleSheet.compose( style.buttonActive, customStyle ); + + const isDisabled = disabled || ariaDisabled; + + const { fill } = isDisabled ? style.buttonDisabled : style.button; + + return ( + <TouchableOpacity + style={ buttonStyle } + activeOpacity={ 0.7 } + accessibilityLabel={ accessibilityLabel } + accessibilityRole={ 'button' } + onPress={ onClick } + disabled={ isDisabled } + > + <Icon icon={ icon } fill={ fill } size={ iconSize } /> + </TouchableOpacity> + ); +} + +export default Button; diff --git a/packages/block-library/src/gallery/v1/gallery-image-style.native.scss b/packages/block-library/src/gallery/v1/gallery-image-style.native.scss new file mode 100644 index 0000000000000..9b221a56981f6 --- /dev/null +++ b/packages/block-library/src/gallery/v1/gallery-image-style.native.scss @@ -0,0 +1,109 @@ +$gallery-image-container-height: 150px; +$overlay-border-width: 2px; +$caption-background-color: rgba(0, 0, 0, 0.4); + +.galleryImageContainer { + flex: 1; + height: $gallery-image-container-height; + overflow: hidden; + background-color: $gray-lighten-30; +} + +.galleryImageContainerDark { + background-color: $gray-90; +} + +.image { + height: 100%; +} + +.button { + fill: $gray-0; + width: 30px; +} + +.buttonDisabled { + fill: $gray-30; +} + +.buttonActive { + flex-direction: row; + justify-content: center; + align-items: center; + border-radius: 6px; + border-color: $gray-70; + background-color: $gray-70; +} + +.moverButtonContainer { + flex-direction: row; + align-items: center; + border-radius: 3px; + background-color: $gray-70; +} + +.separator { + border-right-color: $gray-30; + border-right-width: 1px; + height: 20px; +} + +.toolbarContainer { + position: absolute; +} + +.toolbar { + padding: 5px; + flex-direction: row; + justify-content: space-between; +} + +.captionContainer { + flex: 1; + flex-direction: row; + align-items: flex-end; + position: absolute; + bottom: $overlay-border-width; + left: $overlay-border-width; + right: $overlay-border-width; + top: 45; +} + +@mixin caption-shared { + font-size: 12px; + background-color: #0000; + color: #fff; + font-family: $default-regular-font; + text-align: center; +} + +.caption { + @include caption-shared; + background-color: $caption-background-color; + padding-top: $grid-unit; + padding-bottom: $grid-unit; +} + +.captionPlaceholder { + color: #ccc; +} + +// expand caption container to compensate for overlay border +.captionExpandedContainer { + // constrain height to gallery image height for caption scroll + max-height: $gallery-image-container-height; + position: absolute; + bottom: 0; + left: 0; + right: 0; + padding-top: $grid-unit; + padding-bottom: $overlay-border-width + $grid-unit; + padding-left: $overlay-border-width; + padding-right: $overlay-border-width; + // use caption background color on container when expanded + background-color: $caption-background-color; +} + +.captionExpanded { + @include caption-shared; +} diff --git a/packages/block-library/src/gallery/v1/gallery-image.js b/packages/block-library/src/gallery/v1/gallery-image.js new file mode 100644 index 0000000000000..9520a5ba76349 --- /dev/null +++ b/packages/block-library/src/gallery/v1/gallery-image.js @@ -0,0 +1,282 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; +import { get, omit } from 'lodash'; + +/** + * WordPress dependencies + */ +import { Component } from '@wordpress/element'; +import { Button, Spinner, ButtonGroup } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { BACKSPACE, DELETE } from '@wordpress/keycodes'; +import { withSelect, withDispatch } from '@wordpress/data'; +import { + RichText, + MediaPlaceholder, + store as blockEditorStore, + __experimentalGetElementClassName, +} from '@wordpress/block-editor'; +import { isBlobURL } from '@wordpress/blob'; +import { compose } from '@wordpress/compose'; +import { + closeSmall, + chevronLeft, + chevronRight, + edit, + image as imageIcon, +} from '@wordpress/icons'; +import { store as coreStore } from '@wordpress/core-data'; + +/** + * Internal dependencies + */ +import { pickRelevantMediaFiles } from './shared'; +import { + LINK_DESTINATION_ATTACHMENT, + LINK_DESTINATION_MEDIA, +} from './constants'; + +const isTemporaryImage = ( id, url ) => ! id && isBlobURL( url ); + +class GalleryImage extends Component { + constructor() { + super( ...arguments ); + + this.onSelectImage = this.onSelectImage.bind( this ); + this.onRemoveImage = this.onRemoveImage.bind( this ); + this.bindContainer = this.bindContainer.bind( this ); + this.onEdit = this.onEdit.bind( this ); + this.onSelectImageFromLibrary = + this.onSelectImageFromLibrary.bind( this ); + this.onSelectCustomURL = this.onSelectCustomURL.bind( this ); + this.state = { + isEditing: false, + }; + } + + bindContainer( ref ) { + this.container = ref; + } + + onSelectImage() { + if ( ! this.props.isSelected ) { + this.props.onSelect(); + } + } + + onRemoveImage( event ) { + if ( + this.container === this.container.ownerDocument.activeElement && + this.props.isSelected && + [ BACKSPACE, DELETE ].indexOf( event.keyCode ) !== -1 + ) { + event.preventDefault(); + this.props.onRemove(); + } + } + + onEdit() { + this.setState( { + isEditing: true, + } ); + } + + componentDidUpdate() { + const { image, url, __unstableMarkNextChangeAsNotPersistent } = + this.props; + if ( image && ! url ) { + __unstableMarkNextChangeAsNotPersistent(); + this.props.setAttributes( { + url: image.source_url, + alt: image.alt_text, + } ); + } + } + + deselectOnBlur() { + this.props.onDeselect(); + } + + onSelectImageFromLibrary( media ) { + const { setAttributes, id, url, alt, caption, sizeSlug } = this.props; + if ( ! media || ! media.url ) { + return; + } + + let mediaAttributes = pickRelevantMediaFiles( media, sizeSlug ); + + // If the current image is temporary but an alt text was meanwhile + // written by the user, make sure the text is not overwritten. + if ( isTemporaryImage( id, url ) ) { + if ( alt ) { + mediaAttributes = omit( mediaAttributes, [ 'alt' ] ); + } + } + + // If a caption text was meanwhile written by the user, + // make sure the text is not overwritten by empty captions. + if ( caption && ! get( mediaAttributes, [ 'caption' ] ) ) { + mediaAttributes = omit( mediaAttributes, [ 'caption' ] ); + } + + setAttributes( mediaAttributes ); + this.setState( { + isEditing: false, + } ); + } + + onSelectCustomURL( newURL ) { + const { setAttributes, url } = this.props; + if ( newURL !== url ) { + setAttributes( { + url: newURL, + id: undefined, + } ); + this.setState( { + isEditing: false, + } ); + } + } + + render() { + const { + url, + alt, + id, + linkTo, + link, + isFirstItem, + isLastItem, + isSelected, + caption, + onRemove, + onMoveForward, + onMoveBackward, + setAttributes, + 'aria-label': ariaLabel, + } = this.props; + const { isEditing } = this.state; + + let href; + + switch ( linkTo ) { + case LINK_DESTINATION_MEDIA: + href = url; + break; + case LINK_DESTINATION_ATTACHMENT: + href = link; + break; + } + + const img = ( + // Disable reason: Image itself is not meant to be interactive, but should + // direct image selection and unfocus caption fields. + /* eslint-disable jsx-a11y/no-noninteractive-element-interactions */ + <> + <img + src={ url } + alt={ alt } + data-id={ id } + onKeyDown={ this.onRemoveImage } + tabIndex="0" + aria-label={ ariaLabel } + ref={ this.bindContainer } + /> + { isBlobURL( url ) && <Spinner /> } + </> + /* eslint-enable jsx-a11y/no-noninteractive-element-interactions */ + ); + + const className = classnames( { + 'is-selected': isSelected, + 'is-transient': isBlobURL( url ), + } ); + + return ( + // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions + <figure + className={ className } + onClick={ this.onSelectImage } + onFocus={ this.onSelectImage } + > + { ! isEditing && ( href ? <a href={ href }>{ img }</a> : img ) } + { isEditing && ( + <MediaPlaceholder + labels={ { title: __( 'Edit gallery image' ) } } + icon={ imageIcon } + onSelect={ this.onSelectImageFromLibrary } + onSelectURL={ this.onSelectCustomURL } + accept="image/*" + allowedTypes={ [ 'image' ] } + value={ { id, src: url } } + /> + ) } + <ButtonGroup className="block-library-gallery-item__inline-menu is-left"> + <Button + icon={ chevronLeft } + onClick={ isFirstItem ? undefined : onMoveBackward } + label={ __( 'Move image backward' ) } + aria-disabled={ isFirstItem } + disabled={ ! isSelected } + /> + <Button + icon={ chevronRight } + onClick={ isLastItem ? undefined : onMoveForward } + label={ __( 'Move image forward' ) } + aria-disabled={ isLastItem } + disabled={ ! isSelected } + /> + </ButtonGroup> + <ButtonGroup className="block-library-gallery-item__inline-menu is-right"> + <Button + icon={ edit } + onClick={ this.onEdit } + label={ __( 'Replace image' ) } + disabled={ ! isSelected } + /> + <Button + icon={ closeSmall } + onClick={ onRemove } + label={ __( 'Remove image' ) } + disabled={ ! isSelected } + /> + </ButtonGroup> + { ! isEditing && ( isSelected || caption ) && ( + <RichText + tagName="figcaption" + className={ __experimentalGetElementClassName( + 'caption' + ) } + aria-label={ __( 'Image caption text' ) } + placeholder={ isSelected ? __( 'Add caption' ) : null } + value={ caption } + onChange={ ( newCaption ) => + setAttributes( { caption: newCaption } ) + } + inlineToolbar + /> + ) } + </figure> + ); + } +} + +export default compose( [ + withSelect( ( select, ownProps ) => { + const { getMedia } = select( coreStore ); + const { id } = ownProps; + + return { + image: id ? getMedia( parseInt( id, 10 ) ) : null, + }; + } ), + withDispatch( ( dispatch ) => { + const { __unstableMarkNextChangeAsNotPersistent } = + dispatch( blockEditorStore ); + return { + __unstableMarkNextChangeAsNotPersistent, + }; + } ), +] )( GalleryImage ); diff --git a/packages/block-library/src/gallery/v1/gallery-image.native.js b/packages/block-library/src/gallery/v1/gallery-image.native.js new file mode 100644 index 0000000000000..7fa6ab4ab7ea7 --- /dev/null +++ b/packages/block-library/src/gallery/v1/gallery-image.native.js @@ -0,0 +1,349 @@ +/** + * External dependencies + */ +import { + StyleSheet, + View, + ScrollView, + TouchableWithoutFeedback, +} from 'react-native'; +import { isEmpty } from 'lodash'; + +/** + * WordPress dependencies + */ +import { + requestImageFailedRetryDialog, + requestImageUploadCancelDialog, + requestImageFullscreenPreview, +} from '@wordpress/react-native-bridge'; +import { Component } from '@wordpress/element'; +import { Image } from '@wordpress/components'; +import { __, sprintf } from '@wordpress/i18n'; +import { Caption, MediaUploadProgress } from '@wordpress/block-editor'; +import { getProtocol } from '@wordpress/url'; +import { withPreferredColorScheme } from '@wordpress/compose'; +import { arrowLeft, arrowRight, warning } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import Button from './gallery-button'; +import style from './gallery-image-style.scss'; + +const { compose } = StyleSheet; + +const separatorStyle = compose( style.separator, { + borderRightWidth: StyleSheet.hairlineWidth, +} ); +const buttonStyle = compose( style.button, { aspectRatio: 1 } ); +const ICON_SIZE_ARROW = 15; + +class GalleryImage extends Component { + constructor() { + super( ...arguments ); + + this.onSelectImage = this.onSelectImage.bind( this ); + this.onSelectCaption = this.onSelectCaption.bind( this ); + this.onMediaPressed = this.onMediaPressed.bind( this ); + this.onCaptionChange = this.onCaptionChange.bind( this ); + this.onSelectMedia = this.onSelectMedia.bind( this ); + + this.updateMediaProgress = this.updateMediaProgress.bind( this ); + this.finishMediaUploadWithSuccess = + this.finishMediaUploadWithSuccess.bind( this ); + this.finishMediaUploadWithFailure = + this.finishMediaUploadWithFailure.bind( this ); + this.renderContent = this.renderContent.bind( this ); + + this.state = { + captionSelected: false, + isUploadInProgress: false, + didUploadFail: false, + }; + } + + onSelectCaption() { + if ( ! this.state.captionSelected ) { + this.setState( { + captionSelected: true, + } ); + } + + if ( ! this.props.isSelected ) { + this.props.onSelect(); + } + } + + onMediaPressed() { + const { id, url, isSelected } = this.props; + const { captionSelected, isUploadInProgress, didUploadFail } = + this.state; + + this.onSelectImage(); + + if ( isUploadInProgress ) { + requestImageUploadCancelDialog( id ); + } else if ( + didUploadFail || + ( id && getProtocol( url ) === 'file:' ) + ) { + requestImageFailedRetryDialog( id ); + } else if ( isSelected && ! captionSelected ) { + requestImageFullscreenPreview( url ); + } + } + + onSelectImage() { + if ( ! this.props.isBlockSelected ) { + this.props.onSelectBlock(); + } + + if ( ! this.props.isSelected ) { + this.props.onSelect(); + } + + if ( this.state.captionSelected ) { + this.setState( { + captionSelected: false, + } ); + } + } + + onSelectMedia( media ) { + const { setAttributes } = this.props; + setAttributes( media ); + } + + onCaptionChange( caption ) { + const { setAttributes } = this.props; + setAttributes( { caption } ); + } + + componentDidUpdate( prevProps ) { + const { isSelected, image, url } = this.props; + if ( image && ! url ) { + this.props.setAttributes( { + url: image.source_url, + alt: image.alt_text, + } ); + } + + // Unselect the caption so when the user selects other image and comeback + // the caption is not immediately selected. + if ( + this.state.captionSelected && + ! isSelected && + prevProps.isSelected + ) { + this.setState( { + captionSelected: false, + } ); + } + } + + updateMediaProgress() { + if ( ! this.state.isUploadInProgress ) { + this.setState( { isUploadInProgress: true } ); + } + } + + finishMediaUploadWithSuccess( payload ) { + this.setState( { + isUploadInProgress: false, + didUploadFail: false, + } ); + + this.props.setAttributes( { + id: payload.mediaServerId, + url: payload.mediaUrl, + } ); + } + + finishMediaUploadWithFailure() { + this.setState( { + isUploadInProgress: false, + didUploadFail: true, + } ); + } + + renderContent( params ) { + const { + url, + isFirstItem, + isLastItem, + isSelected, + caption, + onRemove, + onMoveForward, + onMoveBackward, + 'aria-label': ariaLabel, + isCropped, + getStylesFromColorScheme, + isRTL, + } = this.props; + + const { isUploadInProgress, captionSelected } = this.state; + const { isUploadFailed, retryMessage } = params; + const resizeMode = isCropped ? 'cover' : 'contain'; + + const captionPlaceholderStyle = getStylesFromColorScheme( + style.captionPlaceholder, + style.captionPlaceholderDark + ); + + const shouldShowCaptionEditable = ! isUploadFailed && isSelected; + const shouldShowCaptionExpanded = + ! isUploadFailed && ! isSelected && !! caption; + + const captionContainerStyle = shouldShowCaptionExpanded + ? style.captionExpandedContainer + : style.captionContainer; + + const captionStyle = shouldShowCaptionExpanded + ? style.captionExpanded + : style.caption; + + const mediaPickerOptions = [ + { + destructiveButton: true, + id: 'removeImage', + label: __( 'Remove' ), + onPress: onRemove, + separated: true, + value: 'removeImage', + }, + ]; + + return ( + <> + <Image + alt={ ariaLabel } + height={ style.image.height } + isSelected={ isSelected } + isUploadFailed={ isUploadFailed } + isUploadInProgress={ isUploadInProgress } + mediaPickerOptions={ mediaPickerOptions } + onSelectMediaUploadOption={ this.onSelectMedia } + resizeMode={ resizeMode } + url={ url } + retryMessage={ retryMessage } + retryIcon={ warning } + /> + + { ! isUploadInProgress && isSelected && ( + <View style={ style.toolbarContainer }> + <View style={ style.toolbar }> + <View style={ style.moverButtonContainer }> + <Button + style={ buttonStyle } + icon={ isRTL ? arrowRight : arrowLeft } + iconSize={ ICON_SIZE_ARROW } + onClick={ + isFirstItem ? undefined : onMoveBackward + } + accessibilityLabel={ __( + 'Move Image Backward' + ) } + aria-disabled={ isFirstItem } + disabled={ ! isSelected } + /> + <View style={ separatorStyle } /> + <Button + style={ buttonStyle } + icon={ isRTL ? arrowLeft : arrowRight } + iconSize={ ICON_SIZE_ARROW } + onClick={ + isLastItem ? undefined : onMoveForward + } + accessibilityLabel={ __( + 'Move Image Forward' + ) } + aria-disabled={ isLastItem } + disabled={ ! isSelected } + /> + </View> + </View> + </View> + ) } + + { ! isUploadInProgress && + ( shouldShowCaptionEditable || + shouldShowCaptionExpanded ) && ( + <View style={ captionContainerStyle }> + <ScrollView + nestedScrollEnabled + keyboardShouldPersistTaps="handled" + bounces={ false } + > + <Caption + inlineToolbar + isSelected={ isSelected && captionSelected } + onChange={ this.onCaptionChange } + onFocus={ this.onSelectCaption } + placeholder={ + isSelected ? __( 'Add caption' ) : null + } + placeholderTextColor={ + captionPlaceholderStyle.color + } + style={ captionStyle } + value={ caption } + /> + </ScrollView> + </View> + ) } + </> + ); + } + + render() { + const { id, onRemove, getStylesFromColorScheme, isSelected } = + this.props; + + const containerStyle = getStylesFromColorScheme( + style.galleryImageContainer, + style.galleryImageContainerDark + ); + + return ( + <TouchableWithoutFeedback + onPress={ this.onMediaPressed } + accessible={ ! isSelected } // We need only child views to be accessible after the selection. + accessibilityLabel={ this.accessibilityLabelImageContainer() } // if we don't set this explicitly it reads system provided accessibilityLabels of all child components and those include pretty technical words which don't make sense + accessibilityRole={ 'imagebutton' } // this makes VoiceOver to read a description of image provided by system on iOS and lets user know this is a button which conveys the message of tappablity + > + <View style={ containerStyle }> + <MediaUploadProgress + mediaId={ id } + onUpdateMediaProgress={ this.updateMediaProgress } + onFinishMediaUploadWithSuccess={ + this.finishMediaUploadWithSuccess + } + onFinishMediaUploadWithFailure={ + this.finishMediaUploadWithFailure + } + onMediaUploadStateReset={ onRemove } + renderContent={ this.renderContent } + /> + </View> + </TouchableWithoutFeedback> + ); + } + + accessibilityLabelImageContainer() { + const { caption, 'aria-label': ariaLabel } = this.props; + + return isEmpty( caption ) + ? ariaLabel + : ariaLabel + + '. ' + + sprintf( + /* translators: accessibility text. %s: image caption. */ + __( 'Image caption. %s' ), + caption + ); + } +} + +export default withPreferredColorScheme( GalleryImage ); diff --git a/packages/block-library/src/gallery/v1/gallery-styles.native.scss b/packages/block-library/src/gallery/v1/gallery-styles.native.scss new file mode 100644 index 0000000000000..9b3169da048b2 --- /dev/null +++ b/packages/block-library/src/gallery/v1/gallery-styles.native.scss @@ -0,0 +1,8 @@ +.galleryTilesContainerSelected { + margin-bottom: 16px; +} + +.fullWidth { + margin-left: $block-edge-to-content; + margin-right: $block-edge-to-content; +} diff --git a/packages/block-library/src/gallery/v1/gallery.js b/packages/block-library/src/gallery/v1/gallery.js new file mode 100644 index 0000000000000..7fc587f27911a --- /dev/null +++ b/packages/block-library/src/gallery/v1/gallery.js @@ -0,0 +1,125 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { + RichText, + __experimentalGetElementClassName, +} from '@wordpress/block-editor'; +import { VisuallyHidden } from '@wordpress/components'; +import { __, sprintf } from '@wordpress/i18n'; +import { createBlock, getDefaultBlockName } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import GalleryImage from './gallery-image'; +import { defaultColumnsNumberV1 } from '../deprecated'; + +export const Gallery = ( props ) => { + const { + attributes, + isSelected, + setAttributes, + selectedImage, + mediaPlaceholder, + onMoveBackward, + onMoveForward, + onRemoveImage, + onSelectImage, + onDeselectImage, + onSetImageAttributes, + insertBlocksAfter, + blockProps, + } = props; + + const { + align, + columns = defaultColumnsNumberV1( attributes ), + caption, + imageCrop, + images, + } = attributes; + + return ( + <figure + { ...blockProps } + className={ classnames( blockProps.className, { + [ `align${ align }` ]: align, + [ `columns-${ columns }` ]: columns, + 'is-cropped': imageCrop, + } ) } + > + <ul className="blocks-gallery-grid"> + { images.map( ( img, index ) => { + const ariaLabel = sprintf( + /* translators: 1: the order number of the image. 2: the total number of images. */ + __( 'image %1$d of %2$d in gallery' ), + index + 1, + images.length + ); + + return ( + <li + className="blocks-gallery-item" + key={ img.id ? `${ img.id }-${ index }` : img.url } + > + <GalleryImage + url={ img.url } + alt={ img.alt } + id={ img.id } + isFirstItem={ index === 0 } + isLastItem={ index + 1 === images.length } + isSelected={ + isSelected && selectedImage === index + } + onMoveBackward={ onMoveBackward( index ) } + onMoveForward={ onMoveForward( index ) } + onRemove={ onRemoveImage( index ) } + onSelect={ onSelectImage( index ) } + onDeselect={ onDeselectImage( index ) } + setAttributes={ ( attrs ) => + onSetImageAttributes( index, attrs ) + } + caption={ img.caption } + aria-label={ ariaLabel } + sizeSlug={ attributes.sizeSlug } + /> + </li> + ); + } ) } + </ul> + { mediaPlaceholder } + <RichTextVisibilityHelper + isHidden={ ! isSelected && RichText.isEmpty( caption ) } + tagName="figcaption" + className={ classnames( + 'blocks-gallery-caption', + __experimentalGetElementClassName( 'caption' ) + ) } + aria-label={ __( 'Gallery caption text' ) } + placeholder={ __( 'Write gallery caption…' ) } + value={ caption } + onChange={ ( value ) => setAttributes( { caption: value } ) } + inlineToolbar + __unstableOnSplitAtEnd={ () => + insertBlocksAfter( createBlock( getDefaultBlockName() ) ) + } + /> + </figure> + ); +}; + +function RichTextVisibilityHelper( { isHidden, ...richTextProps } ) { + return isHidden ? ( + <VisuallyHidden as={ RichText } { ...richTextProps } /> + ) : ( + <RichText { ...richTextProps } /> + ); +} + +export default Gallery; diff --git a/packages/block-library/src/gallery/v1/gallery.native.js b/packages/block-library/src/gallery/v1/gallery.native.js new file mode 100644 index 0000000000000..7908d17988a1a --- /dev/null +++ b/packages/block-library/src/gallery/v1/gallery.native.js @@ -0,0 +1,162 @@ +/** + * External dependencies + */ +import { View } from 'react-native'; +import { isEmpty } from 'lodash'; + +/** + * Internal dependencies + */ +import GalleryImage from './gallery-image'; +import { defaultColumnsNumberV1 } from '../deprecated'; +import styles from './gallery-styles.scss'; +import Tiles from './tiles'; + +/** + * WordPress dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; +import { + BlockCaption, + store as blockEditorStore, +} from '@wordpress/block-editor'; +import { useState, useEffect } from '@wordpress/element'; +import { mediaUploadSync } from '@wordpress/react-native-bridge'; +import { useSelect } from '@wordpress/data'; +import { alignmentHelpers } from '@wordpress/components'; + +const TILE_SPACING = 15; + +// we must limit displayed columns since readable content max-width is 580px +const MAX_DISPLAYED_COLUMNS = 4; +const MAX_DISPLAYED_COLUMNS_NARROW = 2; + +const { isFullWidth } = alignmentHelpers; + +export const Gallery = ( props ) => { + const [ isCaptionSelected, setIsCaptionSelected ] = useState( false ); + useEffect( mediaUploadSync, [] ); + + const isRTL = useSelect( ( select ) => { + return !! select( blockEditorStore ).getSettings().isRTL; + }, [] ); + + const { + clientId, + selectedImage, + mediaPlaceholder, + onBlur, + onMoveBackward, + onMoveForward, + onRemoveImage, + onSelectImage, + onSetImageAttributes, + onFocusGalleryCaption, + attributes, + isSelected, + isNarrow, + onFocus, + insertBlocksAfter, + } = props; + + const { + align, + columns = defaultColumnsNumberV1( attributes ), + imageCrop, + images, + } = attributes; + + // limit displayed columns when isNarrow is true (i.e. when viewport width is + // less than "small", where small = 600) + const displayedColumns = isNarrow + ? Math.min( columns, MAX_DISPLAYED_COLUMNS_NARROW ) + : Math.min( columns, MAX_DISPLAYED_COLUMNS ); + + const selectImage = ( index ) => { + return () => { + if ( isCaptionSelected ) { + setIsCaptionSelected( false ); + } + // We need to fully invoke the curried function here. + onSelectImage( index )(); + }; + }; + + const focusGalleryCaption = () => { + if ( ! isCaptionSelected ) { + setIsCaptionSelected( true ); + } + onFocusGalleryCaption(); + }; + + return ( + <View style={ { flex: 1 } }> + <Tiles + columns={ displayedColumns } + spacing={ TILE_SPACING } + style={ + isSelected + ? styles.galleryTilesContainerSelected + : undefined + } + > + { images.map( ( img, index ) => { + const ariaLabel = sprintf( + /* translators: 1: the order number of the image. 2: the total number of images. */ + __( 'image %1$d of %2$d in gallery' ), + index + 1, + images.length + ); + + return ( + <GalleryImage + key={ img.id ? `${ img.id }-${ index }` : img.url } + url={ img.url } + alt={ img.alt } + id={ parseInt( img.id, 10 ) } // make id an integer explicitly + isCropped={ imageCrop } + isFirstItem={ index === 0 } + isLastItem={ index + 1 === images.length } + isSelected={ isSelected && selectedImage === index } + isBlockSelected={ isSelected } + onMoveBackward={ onMoveBackward( index ) } + onMoveForward={ onMoveForward( index ) } + onRemove={ onRemoveImage( index ) } + onSelect={ selectImage( index ) } + onSelectBlock={ onFocus } + setAttributes={ ( attrs ) => + onSetImageAttributes( index, attrs ) + } + caption={ img.caption } + aria-label={ ariaLabel } + isRTL={ isRTL } + /> + ); + } ) } + </Tiles> + <View style={ isFullWidth( align ) && styles.fullWidth }> + { mediaPlaceholder } + </View> + <BlockCaption + clientId={ clientId } + isSelected={ isCaptionSelected } + accessible={ true } + accessibilityLabelCreator={ ( caption ) => + isEmpty( caption ) + ? /* translators: accessibility text. Empty gallery caption. */ + 'Gallery caption. Empty' + : sprintf( + /* translators: accessibility text. %s: gallery caption. */ + __( 'Gallery caption. %s' ), + caption + ) + } + onFocus={ focusGalleryCaption } + onBlur={ onBlur } // Always assign onBlur as props. + insertBlocksAfter={ insertBlocksAfter } + /> + </View> + ); +}; + +export default Gallery; diff --git a/packages/block-library/src/gallery/v1/save.js b/packages/block-library/src/gallery/v1/save.js new file mode 100644 index 0000000000000..bf8be9ba413b4 --- /dev/null +++ b/packages/block-library/src/gallery/v1/save.js @@ -0,0 +1,98 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { + RichText, + useBlockProps, + __experimentalGetElementClassName, +} from '@wordpress/block-editor'; + +/** + * Internal dependencies + */ +import { defaultColumnsNumberV1 } from '../deprecated'; +import { + LINK_DESTINATION_ATTACHMENT, + LINK_DESTINATION_MEDIA, +} from './constants'; + +export default function saveV1( { attributes } ) { + const { + images, + columns = defaultColumnsNumberV1( attributes ), + imageCrop, + caption, + linkTo, + } = attributes; + const className = `columns-${ columns } ${ imageCrop ? 'is-cropped' : '' }`; + + return ( + <figure { ...useBlockProps.save( { className } ) }> + <ul className="blocks-gallery-grid"> + { images.map( ( image ) => { + let href; + + switch ( linkTo ) { + case LINK_DESTINATION_MEDIA: + href = image.fullUrl || image.url; + break; + case LINK_DESTINATION_ATTACHMENT: + href = image.link; + break; + } + + const img = ( + <img + src={ image.url } + alt={ image.alt } + data-id={ image.id } + data-full-url={ image.fullUrl } + data-link={ image.link } + className={ + image.id ? `wp-image-${ image.id }` : null + } + /> + ); + + return ( + <li + key={ image.id || image.url } + className="blocks-gallery-item" + > + <figure> + { href ? <a href={ href }>{ img }</a> : img } + { ! RichText.isEmpty( image.caption ) && ( + <RichText.Content + tagName="figcaption" + className={ classnames( + 'blocks-gallery-item', + __experimentalGetElementClassName( + 'caption' + ) + ) } + value={ image.caption } + /> + ) } + </figure> + </li> + ); + } ) } + </ul> + { ! RichText.isEmpty( caption ) && ( + <RichText.Content + tagName="figcaption" + className={ classnames( + 'blocks-gallery-caption', + __experimentalGetElementClassName( 'caption' ) + ) } + value={ caption } + /> + ) } + </figure> + ); +} diff --git a/packages/block-library/src/gallery/v1/shared.js b/packages/block-library/src/gallery/v1/shared.js new file mode 100644 index 0000000000000..484020cb9d58c --- /dev/null +++ b/packages/block-library/src/gallery/v1/shared.js @@ -0,0 +1,19 @@ +/** + * External dependencies + */ +import { get, pick } from 'lodash'; + +export const pickRelevantMediaFiles = ( image, sizeSlug = 'large' ) => { + const imageProps = pick( image, [ 'alt', 'id', 'link', 'caption' ] ); + imageProps.url = + get( image, [ 'sizes', sizeSlug, 'url' ] ) || + get( image, [ 'media_details', 'sizes', sizeSlug, 'source_url' ] ) || + image.url; + const fullUrl = + get( image, [ 'sizes', 'full', 'url' ] ) || + get( image, [ 'media_details', 'sizes', 'full', 'source_url' ] ); + if ( fullUrl ) { + imageProps.fullUrl = fullUrl; + } + return imageProps; +}; diff --git a/packages/block-library/src/gallery/v1/tiles-styles.native.scss b/packages/block-library/src/gallery/v1/tiles-styles.native.scss new file mode 100644 index 0000000000000..c2e3d4bad7979 --- /dev/null +++ b/packages/block-library/src/gallery/v1/tiles-styles.native.scss @@ -0,0 +1,11 @@ +.containerStyle { + flex-direction: row; + flex-wrap: wrap; +} + +.tileStyle { + overflow: hidden; + flex-direction: row; + align-items: center; + border-color: transparent; +} diff --git a/packages/block-library/src/gallery/v1/tiles.native.js b/packages/block-library/src/gallery/v1/tiles.native.js new file mode 100644 index 0000000000000..30126ce872593 --- /dev/null +++ b/packages/block-library/src/gallery/v1/tiles.native.js @@ -0,0 +1,79 @@ +/** + * External dependencies + */ +import { View, StyleSheet } from 'react-native'; + +/** + * WordPress dependencies + */ +import { Children } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import styles from './tiles-styles.scss'; + +function Tiles( props ) { + const { columns, children, spacing = 10, style } = props; + + const { compose } = StyleSheet; + + const tileCount = Children.count( children ); + const lastTile = tileCount - 1; + const lastRow = Math.floor( lastTile / columns ); + + const wrappedChildren = Children.map( children, ( child, index ) => { + /** + * Since we don't have `calc()`, we must calculate our spacings here in + * order to preserve even spacing between tiles and equal width for tiles + * in a given row. + * + * In order to ensure equal sizing of tile contents, we distribute the + * spacing such that each tile has an equal "share" of the fixed spacing. To + * keep the tiles properly aligned within their rows, we calculate the left + * and right paddings based on the tile's relative position within the row. + * + * Note: we use padding instead of margins so that the fixed spacing is + * included within the relative spacing (i.e. width percentage), and + * wrapping behavior is preserved. + * + * - The left most tile in a row must have left padding of zero. + * - The right most tile in a row must have a right padding of zero. + * + * The values of these left and right paddings are interpolated for tiles in + * between. The right padding is complementary with the left padding of the + * next tile (i.e. the right padding of [tile n] + the left padding of + * [tile n + 1] will be equal for all tiles except the last one in a given + * row). + */ + + const row = Math.floor( index / columns ); + const rowLength = + row === lastRow ? ( lastTile % columns ) + 1 : columns; + const indexInRow = index % columns; + + return ( + <View + style={ [ + styles.tileStyle, + { + width: `${ 100 / rowLength }%`, + paddingLeft: spacing * ( indexInRow / rowLength ), + paddingRight: + spacing * ( 1 - ( indexInRow + 1 ) / rowLength ), + paddingTop: row === 0 ? 0 : spacing / 2, + paddingBottom: row === lastRow ? 0 : spacing / 2, + }, + ] } + > + { child } + </View> + ); + } ); + + const containerStyle = compose( styles.containerStyle, style ); + + return <View style={ containerStyle }>{ wrappedChildren }</View>; +} + +export default Tiles; From ae1cfe358558e14b91cb1dde8e760094ca9aac02 Mon Sep 17 00:00:00 2001 From: ntsekouras <ntsekouras@outlook.com> Date: Wed, 29 Jun 2022 11:25:35 +0300 Subject: [PATCH 07/13] maybe keep gallery for 6.0? --- lib/compat/wordpress-6.0/block-gallery.php | 57 ++++++++++++++++++++++ lib/load.php | 1 + 2 files changed, 58 insertions(+) create mode 100644 lib/compat/wordpress-6.0/block-gallery.php diff --git a/lib/compat/wordpress-6.0/block-gallery.php b/lib/compat/wordpress-6.0/block-gallery.php new file mode 100644 index 0000000000000..53c3a0c2247fd --- /dev/null +++ b/lib/compat/wordpress-6.0/block-gallery.php @@ -0,0 +1,57 @@ +<?php +/** + * Gallery block modifications. + * + * @package gutenberg + */ + +/** + * The new gallery block format is not compatible with the use_BalanceTags option + * in WP versions <= 5.8 https://core.trac.wordpress.org/ticket/54130. + * This method adds a variable to the wp namespace to indicate if the new gallery block + * format can be enabled or not. It needs to be added this early and to the wp namespace + * as it needs to be available when the initial block parsing runs on editor load, and most of + * the editor store and standard flags are not loaded yet at that point + * + * @since 12.1.0 + * @todo This should be removed when the minimum required WP version is >= 6.0. + * + * @return void. + */ +function gutenberg_check_gallery_block_v2_compatibility() { + $use_balance_tags = (int) get_option( 'use_balanceTags' ); + $v2_gallery_enabled = boolval( 1 !== $use_balance_tags || is_wp_version_compatible( '6.0' ) ) ? 'true' : 'false'; + + wp_add_inline_script( + 'wp-dom-ready', + 'wp.galleryBlockV2Enabled = ' . $v2_gallery_enabled . ';', + 'after' + ); +} +add_action( 'init', 'gutenberg_check_gallery_block_v2_compatibility' ); + +/** + * Prevent use_balanceTags being enabled on WordPress 5.8 or earlier as it breaks + * the layout of the new Gallery block. + * + * @since 12.1.0 + * @todo This should be removed when the minimum required WP version is >= 6.0. + * + * @param int $new_value The new value for use_balanceTags. + */ +function gutenberg_use_balancetags_check( $new_value ) { + global $wp_version; + + if ( 1 === (int) $new_value && version_compare( $wp_version, '6.0', '<' ) ) { + /* translators: %s: Minimum required version */ + $message = sprintf( __( 'Gutenberg requires WordPress %s or later in order to enable the “Correct invalidly nested XHTML automatically” option. Please upgrade WordPress before enabling.', 'gutenberg' ), '6.0' ); + add_settings_error( 'gutenberg_use_balancetags_check', 'gutenberg_use_balancetags_check', $message, 'error' ); + if ( class_exists( 'WP_CLI' ) ) { + WP_CLI::error( $message ); + } + return 0; + } + + return $new_value; +} +add_filter( 'pre_update_option_use_balanceTags', 'gutenberg_use_balancetags_check' ); diff --git a/lib/load.php b/lib/load.php index 61d7bedba5b86..cac1d1430bc36 100644 --- a/lib/load.php +++ b/lib/load.php @@ -59,6 +59,7 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/experimental/editor-settings.php'; // WordPress 6.0 compat. +require __DIR__ . '/compat/wordpress-6.0/block-gallery.php'; require __DIR__ . '/compat/wordpress-6.0/block-editor-settings.php'; require __DIR__ . '/compat/wordpress-6.0/get-global-styles-and-settings.php'; require __DIR__ . '/compat/wordpress-6.0/render-svg-filters.php'; From 9fd854d2d74acdc9180b509e0f215d0f4ac64ba5 Mon Sep 17 00:00:00 2001 From: ntsekouras <ntsekouras@outlook.com> Date: Wed, 29 Jun 2022 14:17:03 +0300 Subject: [PATCH 08/13] php tests --- phpunit/class-wp-theme-json-test.php | 2838 ++------------------------ phpunit/global-styles-test.php | 28 - phpunit/navigation-test.php | 94 - 3 files changed, 170 insertions(+), 2790 deletions(-) delete mode 100644 phpunit/global-styles-test.php delete mode 100644 phpunit/navigation-test.php diff --git a/phpunit/class-wp-theme-json-test.php b/phpunit/class-wp-theme-json-test.php index 36f28873fc0ac..4ba9b65aa4f5b 100644 --- a/phpunit/class-wp-theme-json-test.php +++ b/phpunit/class-wp-theme-json-test.php @@ -8,33 +8,32 @@ class WP_Theme_JSON_Gutenberg_Test extends WP_UnitTestCase { - function test_get_settings() { + function test_get_stylesheet_handles_whitelisted_element_pseudo_selectors() { $theme_json = new WP_Theme_JSON_Gutenberg( array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'settings' => array( - 'color' => array( - 'custom' => false, - ), - 'layout' => array( - 'contentSize' => 'value', - 'invalid/key' => 'value', - ), - 'invalid/key' => 'value', - 'blocks' => array( - 'core/group' => array( - 'color' => array( - 'custom' => false, - ), - 'invalid/key' => 'value', - ), - ), - ), - 'styles' => array( + 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, + 'styles' => array( 'elements' => array( 'link' => array( - 'color' => array( - 'text' => '#111', + 'color' => array( + 'text' => 'green', + 'background' => 'red', + ), + ':hover' => array( + 'color' => array( + 'text' => 'red', + 'background' => 'green', + ), + 'typography' => array( + 'textTransform' => 'uppercase', + 'fontSize' => '10em', + ), + ), + ':focus' => array( + 'color' => array( + 'text' => 'yellow', + 'background' => 'black', + ), ), ), ), @@ -42,78 +41,37 @@ function test_get_settings() { ) ); - $actual = $theme_json->get_settings(); + $base_styles = 'body { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }'; + + $element_styles = 'a{background-color: red;color: green;}a:hover{background-color: green;color: red;font-size: 10em;text-transform: uppercase;}a:focus{background-color: black;color: yellow;}'; - $expected = array( - 'color' => array( - 'custom' => false, - ), - 'layout' => array( - 'contentSize' => 'value', - ), - 'blocks' => array( - 'core/group' => array( - 'color' => array( - 'custom' => false, - ), - ), - ), - ); + $expected = $base_styles . $element_styles; - $this->assertEqualSetsWithIndex( $expected, $actual ); + $this->assertEquals( $expected, $theme_json->get_stylesheet() ); + $this->assertEquals( $expected, $theme_json->get_stylesheet( array( 'styles' ) ) ); } - function test_get_settings_presets_are_keyed_by_origin() { - $default_origin = new WP_Theme_JSON_Gutenberg( + function test_get_stylesheet_handles_only_psuedo_selector_rules_for_given_property() { + $theme_json = new WP_Theme_JSON_Gutenberg( array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'settings' => array( - 'color' => array( - 'palette' => array( - array( - 'slug' => 'white', - 'color' => 'white', - ), - ), - ), - 'invalid/key' => 'value', - 'blocks' => array( - 'core/group' => array( - 'color' => array( - 'palette' => array( - array( - 'slug' => 'white', - 'color' => 'white', - ), + 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, + 'styles' => array( + 'elements' => array( + 'link' => array( + ':hover' => array( + 'color' => array( + 'text' => 'red', + 'background' => 'green', + ), + 'typography' => array( + 'textTransform' => 'uppercase', + 'fontSize' => '10em', ), ), - ), - ), - ), - ), - 'default' - ); - $no_origin = new WP_Theme_JSON_Gutenberg( - array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'settings' => array( - 'color' => array( - 'palette' => array( - array( - 'slug' => 'black', - 'color' => 'black', - ), - ), - ), - 'invalid/key' => 'value', - 'blocks' => array( - 'core/group' => array( - 'color' => array( - 'palette' => array( - array( - 'slug' => 'black', - 'color' => 'black', - ), + ':focus' => array( + 'color' => array( + 'text' => 'yellow', + 'background' => 'black', ), ), ), @@ -122,94 +80,77 @@ function test_get_settings_presets_are_keyed_by_origin() { ) ); - $actual_default = $default_origin->get_raw_data(); - $actual_no_origin = $no_origin->get_raw_data(); + $base_styles = 'body { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }'; + + $element_styles = 'a:hover{background-color: green;color: red;font-size: 10em;text-transform: uppercase;}a:focus{background-color: black;color: yellow;}'; + + $expected = $base_styles . $element_styles; + + $this->assertEquals( $expected, $theme_json->get_stylesheet() ); + $this->assertEquals( $expected, $theme_json->get_stylesheet( array( 'styles' ) ) ); + } - $expected_default = array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'settings' => array( - 'color' => array( - 'palette' => array( - 'default' => array( - array( - 'slug' => 'white', - 'color' => 'white', + function test_get_stylesheet_ignores_pseudo_selectors_on_non_whitelisted_elements() { + $theme_json = new WP_Theme_JSON_Gutenberg( + array( + 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, + 'styles' => array( + 'elements' => array( + 'h4' => array( + 'color' => array( + 'text' => 'green', + 'background' => 'red', ), - ), - ), - ), - 'blocks' => array( - 'core/group' => array( - 'color' => array( - 'palette' => array( - 'default' => array( - array( - 'slug' => 'white', - 'color' => 'white', - ), + ':hover' => array( + 'color' => array( + 'text' => 'red', + 'background' => 'green', ), ), - ), - ), - ), - ), - ); - $expected_no_origin = array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'settings' => array( - 'color' => array( - 'palette' => array( - 'theme' => array( - array( - 'slug' => 'black', - 'color' => 'black', - ), - ), - ), - ), - 'blocks' => array( - 'core/group' => array( - 'color' => array( - 'palette' => array( - 'theme' => array( - array( - 'slug' => 'black', - 'color' => 'black', - ), + ':focus' => array( + 'color' => array( + 'text' => 'yellow', + 'background' => 'black', ), ), ), ), ), - ), + ) ); - $this->assertEqualSetsWithIndex( $expected_default, $actual_default ); - $this->assertEqualSetsWithIndex( $expected_no_origin, $actual_no_origin ); + $base_styles = 'body { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }'; + + $element_styles = 'h4{background-color: red;color: green;}'; + + $expected = $base_styles . $element_styles; + + $this->assertEquals( $expected, $theme_json->get_stylesheet() ); + $this->assertEquals( $expected, $theme_json->get_stylesheet( array( 'styles' ) ) ); } - function test_get_settings_appearance_true_opts_in() { + function test_get_stylesheet_ignores_non_whitelisted_pseudo_selectors() { $theme_json = new WP_Theme_JSON_Gutenberg( array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'settings' => array( - 'appearanceTools' => true, - 'spacing' => array( - 'blockGap' => false, // This should override appearanceTools. - ), - 'blocks' => array( - 'core/paragraph' => array( - 'typography' => array( - 'lineHeight' => false, + 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, + 'styles' => array( + 'elements' => array( + 'link' => array( + 'color' => array( + 'text' => 'green', + 'background' => 'red', ), - ), - 'core/group' => array( - 'appearanceTools' => true, - 'typography' => array( - 'lineHeight' => false, // This should override appearanceTools. + ':hover' => array( + 'color' => array( + 'text' => 'red', + 'background' => 'green', + ), ), - 'spacing' => array( - 'blockGap' => null, + ':levitate' => array( + 'color' => array( + 'text' => 'yellow', + 'background' => 'black', + ), ), ), ), @@ -217,74 +158,47 @@ function test_get_settings_appearance_true_opts_in() { ) ); - $actual = $theme_json->get_settings(); - $expected = array( - 'border' => array( - 'width' => true, - 'style' => true, - 'radius' => true, - 'color' => true, - ), - 'color' => array( - 'link' => true, - ), - 'spacing' => array( - 'blockGap' => false, - 'margin' => true, - 'padding' => true, - ), - 'typography' => array( - 'lineHeight' => true, - ), - 'blocks' => array( - 'core/paragraph' => array( - 'typography' => array( - 'lineHeight' => false, - ), - ), - 'core/group' => array( - 'border' => array( - 'width' => true, - 'style' => true, - 'radius' => true, - 'color' => true, - ), - 'color' => array( - 'link' => true, - ), - 'spacing' => array( - 'blockGap' => false, - 'margin' => true, - 'padding' => true, - ), - 'typography' => array( - 'lineHeight' => false, - ), - ), - ), - ); + $base_styles = 'body { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }'; - $this->assertEqualSetsWithIndex( $expected, $actual ); + $element_styles = 'a{background-color: red;color: green;}a:hover{background-color: green;color: red;}'; + + $expected = $base_styles . $element_styles; + + $this->assertEquals( $expected, $theme_json->get_stylesheet() ); + $this->assertEquals( $expected, $theme_json->get_stylesheet( array( 'styles' ) ) ); + $this->assertStringNotContainsString( 'a:levitate{', $theme_json->get_stylesheet( array( 'styles' ) ) ); } - function test_get_settings_appearance_false_does_not_opt_in() { + function test_get_stylesheet_handles_priority_of_elements_vs_block_elements_pseudo_selectors() { $theme_json = new WP_Theme_JSON_Gutenberg( array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'settings' => array( - 'appearanceTools' => false, - 'border' => array( - 'width' => true, - ), - 'blocks' => array( - 'core/paragraph' => array( - 'typography' => array( - 'lineHeight' => false, - ), - ), - 'core/group' => array( - 'typography' => array( - 'lineHeight' => false, + 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, + 'styles' => array( + 'blocks' => array( + 'core/group' => array( + 'elements' => array( + 'link' => array( + 'color' => array( + 'text' => 'green', + 'background' => 'red', + ), + ':hover' => array( + 'color' => array( + 'text' => 'red', + 'background' => 'green', + ), + 'typography' => array( + 'textTransform' => 'uppercase', + 'fontSize' => '10em', + ), + ), + ':focus' => array( + 'color' => array( + 'text' => 'yellow', + 'background' => 'black', + ), + ), + ), ), ), ), @@ -292,57 +206,45 @@ function test_get_settings_appearance_false_does_not_opt_in() { ) ); - $actual = $theme_json->get_settings(); - $expected = array( - 'appearanceTools' => false, - 'border' => array( - 'width' => true, - ), - 'blocks' => array( - 'core/paragraph' => array( - 'typography' => array( - 'lineHeight' => false, - ), - ), - 'core/group' => array( - 'typography' => array( - 'lineHeight' => false, - ), - ), - ), - ); + $base_styles = 'body { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }'; - $this->assertEqualSetsWithIndex( $expected, $actual ); + $element_styles = '.wp-block-group a{background-color: red;color: green;}.wp-block-group a:hover{background-color: green;color: red;font-size: 10em;text-transform: uppercase;}.wp-block-group a:focus{background-color: black;color: yellow;}'; + + $expected = $base_styles . $element_styles; + + $this->assertEquals( $expected, $theme_json->get_stylesheet() ); + $this->assertEquals( $expected, $theme_json->get_stylesheet( array( 'styles' ) ) ); } - function test_get_stylesheet_support_for_shorthand_and_longhand_values() { + function test_get_stylesheet_handles_whitelisted_block_level_element_pseudo_selectors() { $theme_json = new WP_Theme_JSON_Gutenberg( array( 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, 'styles' => array( - 'blocks' => array( - 'core/group' => array( - 'border' => array( - 'radius' => '10px', - ), - 'spacing' => array( - 'padding' => '24px', - 'margin' => '1em', + 'elements' => array( + 'link' => array( + 'color' => array( + 'text' => 'green', + 'background' => 'red', ), - ), - 'core/image' => array( - 'border' => array( - 'radius' => array( - 'topLeft' => '10px', - 'bottomRight' => '1em', + ':hover' => array( + 'color' => array( + 'text' => 'red', + 'background' => 'green', ), ), - 'spacing' => array( - 'padding' => array( - 'top' => '15px', - ), - 'margin' => array( - 'bottom' => '30px', + ), + ), + 'blocks' => array( + 'core/group' => array( + 'elements' => array( + 'link' => array( + ':hover' => array( + 'color' => array( + 'text' => 'yellow', + 'background' => 'black', + ), + ), ), ), ), @@ -351,1926 +253,14 @@ function test_get_stylesheet_support_for_shorthand_and_longhand_values() { ) ); - $styles = 'body { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }.wp-block-group{border-radius: 10px;margin: 1em;padding: 24px;}.wp-block-image{border-top-left-radius: 10px;border-bottom-right-radius: 1em;margin-bottom: 30px;padding-top: 15px;}'; - $this->assertEquals( $styles, $theme_json->get_stylesheet() ); - $this->assertEquals( $styles, $theme_json->get_stylesheet( array( 'styles' ) ) ); - } + $base_styles = 'body { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }'; - function test_get_stylesheet_skips_disabled_protected_properties() { - $theme_json = new WP_Theme_JSON_Gutenberg( - array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'settings' => array( - 'spacing' => array( - 'blockGap' => null, - ), - ), - 'styles' => array( - 'spacing' => array( - 'blockGap' => '1em', - ), - 'blocks' => array( - 'core/columns' => array( - 'spacing' => array( - 'blockGap' => '24px', - ), - ), - ), - ), - ) - ); - - $expected = 'body { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }'; - $this->assertEquals( $expected, $theme_json->get_stylesheet() ); - $this->assertEquals( $expected, $theme_json->get_stylesheet( array( 'styles' ) ) ); - } - - function test_get_stylesheet_renders_enabled_protected_properties() { - $theme_json = new WP_Theme_JSON_Gutenberg( - array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'settings' => array( - 'spacing' => array( - 'blockGap' => true, - ), - ), - 'styles' => array( - 'spacing' => array( - 'blockGap' => '1em', - ), - ), - ) - ); - - $expected = 'body { margin: 0; }body{--wp--style--block-gap: 1em;}.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }.wp-site-blocks > * { margin-block-start: 0; margin-block-end: 0; }.wp-site-blocks > * + * { margin-block-start: var( --wp--style--block-gap ); }'; - $this->assertEquals( $expected, $theme_json->get_stylesheet() ); - $this->assertEquals( $expected, $theme_json->get_stylesheet( array( 'styles' ) ) ); - } - - function test_get_stylesheet() { - $theme_json = new WP_Theme_JSON_Gutenberg( - array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'settings' => array( - 'color' => array( - 'text' => 'value', - 'palette' => array( - array( - 'slug' => 'grey', - 'color' => 'grey', - ), - ), - ), - 'typography' => array( - 'fontFamilies' => array( - array( - 'slug' => 'small', - 'fontFamily' => '14px', - ), - array( - 'slug' => 'big', - 'fontFamily' => '41px', - ), - ), - ), - 'spacing' => array( - 'blockGap' => false, - ), - 'misc' => 'value', - 'blocks' => array( - 'core/group' => array( - 'custom' => array( - 'base-font' => 16, - 'line-height' => array( - 'small' => 1.2, - 'medium' => 1.4, - 'large' => 1.8, - ), - ), - ), - ), - ), - 'styles' => array( - 'color' => array( - 'text' => 'var:preset|color|grey', - ), - 'misc' => 'value', - 'elements' => array( - 'link' => array( - 'color' => array( - 'text' => '#111', - 'background' => '#333', - ), - ), - ), - 'blocks' => array( - 'core/group' => array( - 'border' => array( - 'radius' => '10px', - ), - 'elements' => array( - 'link' => array( - 'color' => array( - 'text' => '#111', - ), - ), - ), - 'spacing' => array( - 'padding' => '24px', - ), - ), - 'core/heading' => array( - 'color' => array( - 'text' => '#123456', - ), - 'elements' => array( - 'link' => array( - 'color' => array( - 'text' => '#111', - 'background' => '#333', - ), - 'typography' => array( - 'fontSize' => '60px', - ), - ), - ), - ), - 'core/post-date' => array( - 'color' => array( - 'text' => '#123456', - ), - 'elements' => array( - 'link' => array( - 'color' => array( - 'background' => '#777', - 'text' => '#555', - ), - ), - ), - ), - 'core/image' => array( - 'border' => array( - 'radius' => array( - 'topLeft' => '10px', - 'bottomRight' => '1em', - ), - ), - 'spacing' => array( - 'margin' => array( - 'bottom' => '30px', - ), - ), - ), - ), - ), - 'misc' => 'value', - ) - ); - - $variables = 'body{--wp--preset--color--grey: grey;--wp--preset--font-family--small: 14px;--wp--preset--font-family--big: 41px;}.wp-block-group{--wp--custom--base-font: 16;--wp--custom--line-height--small: 1.2;--wp--custom--line-height--medium: 1.4;--wp--custom--line-height--large: 1.8;}'; - $styles = 'body { margin: 0; }body{color: var(--wp--preset--color--grey);}.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }.wp-site-blocks > * { margin-block-start: 0; margin-block-end: 0; }.wp-site-blocks > * + * { margin-block-start: var( --wp--style--block-gap ); }a{background-color: #333;color: #111;}.wp-block-group{border-radius: 10px;padding: 24px;}.wp-block-group a{color: #111;}h1,h2,h3,h4,h5,h6{color: #123456;}h1 a,h2 a,h3 a,h4 a,h5 a,h6 a{background-color: #333;color: #111;font-size: 60px;}.wp-block-post-date{color: #123456;}.wp-block-post-date a{background-color: #777;color: #555;}.wp-block-image{border-top-left-radius: 10px;border-bottom-right-radius: 1em;margin-bottom: 30px;}'; - $presets = '.has-grey-color{color: var(--wp--preset--color--grey) !important;}.has-grey-background-color{background-color: var(--wp--preset--color--grey) !important;}.has-grey-border-color{border-color: var(--wp--preset--color--grey) !important;}.has-small-font-family{font-family: var(--wp--preset--font-family--small) !important;}.has-big-font-family{font-family: var(--wp--preset--font-family--big) !important;}'; - $all = $variables . $styles . $presets; - $this->assertEquals( $all, $theme_json->get_stylesheet() ); - $this->assertEquals( $styles, $theme_json->get_stylesheet( array( 'styles' ) ) ); - $this->assertEquals( $presets, $theme_json->get_stylesheet( array( 'presets' ) ) ); - $this->assertEquals( $variables, $theme_json->get_stylesheet( array( 'variables' ) ) ); - } - - function test_get_stylesheet_preset_classes_work_with_compounded_selectors() { - $theme_json = new WP_Theme_JSON_Gutenberg( - array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'settings' => array( - 'blocks' => array( - 'core/heading' => array( - 'color' => array( - 'palette' => array( - array( - 'slug' => 'white', - 'color' => '#fff', - ), - ), - ), - ), - ), - ), - ) - ); - - $this->assertEquals( - 'h1.has-white-color,h2.has-white-color,h3.has-white-color,h4.has-white-color,h5.has-white-color,h6.has-white-color{color: var(--wp--preset--color--white) !important;}h1.has-white-background-color,h2.has-white-background-color,h3.has-white-background-color,h4.has-white-background-color,h5.has-white-background-color,h6.has-white-background-color{background-color: var(--wp--preset--color--white) !important;}h1.has-white-border-color,h2.has-white-border-color,h3.has-white-border-color,h4.has-white-border-color,h5.has-white-border-color,h6.has-white-border-color{border-color: var(--wp--preset--color--white) !important;}', - $theme_json->get_stylesheet( array( 'presets' ) ) - ); - } - - function test_get_stylesheet_preset_rules_come_after_block_rules() { - $theme_json = new WP_Theme_JSON_Gutenberg( - array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'settings' => array( - 'blocks' => array( - 'core/group' => array( - 'color' => array( - 'palette' => array( - array( - 'slug' => 'grey', - 'color' => 'grey', - ), - ), - ), - ), - ), - ), - 'styles' => array( - 'blocks' => array( - 'core/group' => array( - 'color' => array( - 'text' => 'red', - ), - ), - ), - ), - ) - ); - - $styles = 'body { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }.wp-block-group{color: red;}'; - $presets = '.wp-block-group.has-grey-color{color: var(--wp--preset--color--grey) !important;}.wp-block-group.has-grey-background-color{background-color: var(--wp--preset--color--grey) !important;}.wp-block-group.has-grey-border-color{border-color: var(--wp--preset--color--grey) !important;}'; - $variables = '.wp-block-group{--wp--preset--color--grey: grey;}'; - $all = $variables . $styles . $presets; - $this->assertEquals( $all, $theme_json->get_stylesheet() ); - $this->assertEquals( $styles, $theme_json->get_stylesheet( array( 'styles' ) ) ); - $this->assertEquals( $presets, $theme_json->get_stylesheet( array( 'presets' ) ) ); - $this->assertEquals( $variables, $theme_json->get_stylesheet( array( 'variables' ) ) ); - } - - function test_get_stylesheet_generates_proper_classes_and_css_vars_from_slugs() { - $theme_json = new WP_Theme_JSON_Gutenberg( - array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'settings' => array( - 'color' => array( - 'palette' => array( - array( - 'slug' => 'grey', - 'color' => 'grey', - ), - array( - 'slug' => 'dark grey', - 'color' => 'grey', - ), - array( - 'slug' => 'light-grey', - 'color' => 'grey', - ), - array( - 'slug' => 'white2black', - 'color' => 'grey', - ), - ), - ), - 'custom' => array( - 'white2black' => 'value', - ), - ), - ) - ); - - $this->assertEquals( - '.has-grey-color{color: var(--wp--preset--color--grey) !important;}.has-dark-grey-color{color: var(--wp--preset--color--dark-grey) !important;}.has-light-grey-color{color: var(--wp--preset--color--light-grey) !important;}.has-white-2-black-color{color: var(--wp--preset--color--white-2-black) !important;}.has-grey-background-color{background-color: var(--wp--preset--color--grey) !important;}.has-dark-grey-background-color{background-color: var(--wp--preset--color--dark-grey) !important;}.has-light-grey-background-color{background-color: var(--wp--preset--color--light-grey) !important;}.has-white-2-black-background-color{background-color: var(--wp--preset--color--white-2-black) !important;}.has-grey-border-color{border-color: var(--wp--preset--color--grey) !important;}.has-dark-grey-border-color{border-color: var(--wp--preset--color--dark-grey) !important;}.has-light-grey-border-color{border-color: var(--wp--preset--color--light-grey) !important;}.has-white-2-black-border-color{border-color: var(--wp--preset--color--white-2-black) !important;}', - $theme_json->get_stylesheet( array( 'presets' ) ) - ); - $this->assertEquals( - 'body{--wp--preset--color--grey: grey;--wp--preset--color--dark-grey: grey;--wp--preset--color--light-grey: grey;--wp--preset--color--white-2-black: grey;--wp--custom--white-2-black: value;}', - $theme_json->get_stylesheet( array( 'variables' ) ) - ); - - } - - public function test_get_stylesheet_preset_values_are_marked_as_important() { - $theme_json = new WP_Theme_JSON_Gutenberg( - array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'settings' => array( - 'color' => array( - 'palette' => array( - array( - 'slug' => 'grey', - 'color' => 'grey', - ), - ), - ), - ), - 'styles' => array( - 'blocks' => array( - 'core/paragraph' => array( - 'color' => array( - 'text' => 'red', - 'background' => 'blue', - ), - 'typography' => array( - 'fontSize' => '12px', - 'lineHeight' => '1.3', - ), - ), - ), - ), - ), - 'default' - ); - - $this->assertEquals( - 'body{--wp--preset--color--grey: grey;}body { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }p{background-color: blue;color: red;font-size: 12px;line-height: 1.3;}.has-grey-color{color: var(--wp--preset--color--grey) !important;}.has-grey-background-color{background-color: var(--wp--preset--color--grey) !important;}.has-grey-border-color{border-color: var(--wp--preset--color--grey) !important;}', - $theme_json->get_stylesheet() - ); - } - - function test_get_stylesheet_handles_whitelisted_element_pseudo_selectors() { - $theme_json = new WP_Theme_JSON_Gutenberg( - array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'styles' => array( - 'elements' => array( - 'link' => array( - 'color' => array( - 'text' => 'green', - 'background' => 'red', - ), - ':hover' => array( - 'color' => array( - 'text' => 'red', - 'background' => 'green', - ), - 'typography' => array( - 'textTransform' => 'uppercase', - 'fontSize' => '10em', - ), - ), - ':focus' => array( - 'color' => array( - 'text' => 'yellow', - 'background' => 'black', - ), - ), - ), - ), - ), - ) - ); - - $base_styles = 'body { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }'; - - $element_styles = 'a{background-color: red;color: green;}a:hover{background-color: green;color: red;font-size: 10em;text-transform: uppercase;}a:focus{background-color: black;color: yellow;}'; - - $expected = $base_styles . $element_styles; - - $this->assertEquals( $expected, $theme_json->get_stylesheet() ); - $this->assertEquals( $expected, $theme_json->get_stylesheet( array( 'styles' ) ) ); - } - - function test_get_stylesheet_handles_only_psuedo_selector_rules_for_given_property() { - $theme_json = new WP_Theme_JSON_Gutenberg( - array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'styles' => array( - 'elements' => array( - 'link' => array( - ':hover' => array( - 'color' => array( - 'text' => 'red', - 'background' => 'green', - ), - 'typography' => array( - 'textTransform' => 'uppercase', - 'fontSize' => '10em', - ), - ), - ':focus' => array( - 'color' => array( - 'text' => 'yellow', - 'background' => 'black', - ), - ), - ), - ), - ), - ) - ); - - $base_styles = 'body { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }'; - - $element_styles = 'a:hover{background-color: green;color: red;font-size: 10em;text-transform: uppercase;}a:focus{background-color: black;color: yellow;}'; - - $expected = $base_styles . $element_styles; - - $this->assertEquals( $expected, $theme_json->get_stylesheet() ); - $this->assertEquals( $expected, $theme_json->get_stylesheet( array( 'styles' ) ) ); - } - - function test_get_stylesheet_ignores_pseudo_selectors_on_non_whitelisted_elements() { - $theme_json = new WP_Theme_JSON_Gutenberg( - array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'styles' => array( - 'elements' => array( - 'h4' => array( - 'color' => array( - 'text' => 'green', - 'background' => 'red', - ), - ':hover' => array( - 'color' => array( - 'text' => 'red', - 'background' => 'green', - ), - ), - ':focus' => array( - 'color' => array( - 'text' => 'yellow', - 'background' => 'black', - ), - ), - ), - ), - ), - ) - ); - - $base_styles = 'body { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }'; - - $element_styles = 'h4{background-color: red;color: green;}'; - - $expected = $base_styles . $element_styles; - - $this->assertEquals( $expected, $theme_json->get_stylesheet() ); - $this->assertEquals( $expected, $theme_json->get_stylesheet( array( 'styles' ) ) ); - } - - function test_get_stylesheet_ignores_non_whitelisted_pseudo_selectors() { - $theme_json = new WP_Theme_JSON_Gutenberg( - array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'styles' => array( - 'elements' => array( - 'link' => array( - 'color' => array( - 'text' => 'green', - 'background' => 'red', - ), - ':hover' => array( - 'color' => array( - 'text' => 'red', - 'background' => 'green', - ), - ), - ':levitate' => array( - 'color' => array( - 'text' => 'yellow', - 'background' => 'black', - ), - ), - ), - ), - ), - ) - ); - - $base_styles = 'body { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }'; - - $element_styles = 'a{background-color: red;color: green;}a:hover{background-color: green;color: red;}'; - - $expected = $base_styles . $element_styles; - - $this->assertEquals( $expected, $theme_json->get_stylesheet() ); - $this->assertEquals( $expected, $theme_json->get_stylesheet( array( 'styles' ) ) ); - $this->assertStringNotContainsString( 'a:levitate{', $theme_json->get_stylesheet( array( 'styles' ) ) ); - } - - function test_get_stylesheet_handles_priority_of_elements_vs_block_elements_pseudo_selectors() { - $theme_json = new WP_Theme_JSON_Gutenberg( - array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'styles' => array( - 'blocks' => array( - 'core/group' => array( - 'elements' => array( - 'link' => array( - 'color' => array( - 'text' => 'green', - 'background' => 'red', - ), - ':hover' => array( - 'color' => array( - 'text' => 'red', - 'background' => 'green', - ), - 'typography' => array( - 'textTransform' => 'uppercase', - 'fontSize' => '10em', - ), - ), - ':focus' => array( - 'color' => array( - 'text' => 'yellow', - 'background' => 'black', - ), - ), - ), - ), - ), - ), - ), - ) - ); - - $base_styles = 'body { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }'; - - $element_styles = '.wp-block-group a{background-color: red;color: green;}.wp-block-group a:hover{background-color: green;color: red;font-size: 10em;text-transform: uppercase;}.wp-block-group a:focus{background-color: black;color: yellow;}'; - - $expected = $base_styles . $element_styles; - - $this->assertEquals( $expected, $theme_json->get_stylesheet() ); - $this->assertEquals( $expected, $theme_json->get_stylesheet( array( 'styles' ) ) ); - } - - function test_get_stylesheet_handles_whitelisted_block_level_element_pseudo_selectors() { - $theme_json = new WP_Theme_JSON_Gutenberg( - array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'styles' => array( - 'elements' => array( - 'link' => array( - 'color' => array( - 'text' => 'green', - 'background' => 'red', - ), - ':hover' => array( - 'color' => array( - 'text' => 'red', - 'background' => 'green', - ), - ), - ), - ), - 'blocks' => array( - 'core/group' => array( - 'elements' => array( - 'link' => array( - ':hover' => array( - 'color' => array( - 'text' => 'yellow', - 'background' => 'black', - ), - ), - ), - ), - ), - ), - ), - ) - ); - - $base_styles = 'body { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }'; - - $element_styles = 'a{background-color: red;color: green;}a:hover{background-color: green;color: red;}.wp-block-group a:hover{background-color: black;color: yellow;}'; - - $expected = $base_styles . $element_styles; - - $this->assertEquals( $expected, $theme_json->get_stylesheet() ); - $this->assertEquals( $expected, $theme_json->get_stylesheet( array( 'styles' ) ) ); - } - - public function test_merge_incoming_data() { - $theme_json = new WP_Theme_JSON_Gutenberg( - array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'settings' => array( - 'color' => array( - 'custom' => false, - 'palette' => array( - array( - 'slug' => 'red', - 'color' => 'red', - ), - array( - 'slug' => 'green', - 'color' => 'green', - ), - ), - ), - 'blocks' => array( - 'core/paragraph' => array( - 'color' => array( - 'custom' => false, - ), - ), - ), - ), - 'styles' => array( - 'typography' => array( - 'fontSize' => '12', - ), - ), - ) - ); - - $add_new_block = array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'settings' => array( - 'blocks' => array( - 'core/list' => array( - 'color' => array( - 'custom' => false, - ), - ), - ), - ), - 'styles' => array( - 'blocks' => array( - 'core/list' => array( - 'typography' => array( - 'fontSize' => '12', - ), - 'color' => array( - 'background' => 'brown', - ), - ), - ), - ), - ); - - $add_key_in_settings = array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'settings' => array( - 'color' => array( - 'customGradient' => true, - ), - ), - ); - - $update_key_in_settings = array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'settings' => array( - 'color' => array( - 'custom' => true, - ), - ), - ); - - $add_styles = array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'styles' => array( - 'blocks' => array( - 'core/group' => array( - 'spacing' => array( - 'padding' => array( - 'top' => '12px', - ), - ), - ), - ), - ), - ); - - $add_key_in_styles = array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'styles' => array( - 'blocks' => array( - 'core/group' => array( - 'spacing' => array( - 'padding' => array( - 'bottom' => '12px', - ), - ), - ), - ), - ), - ); - - $add_invalid_context = array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'styles' => array( - 'blocks' => array( - 'core/para' => array( - 'typography' => array( - 'lineHeight' => '12', - ), - ), - ), - ), - ); - - $update_presets = array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'settings' => array( - 'color' => array( - 'palette' => array( - array( - 'slug' => 'blue', - 'color' => 'blue', - ), - ), - 'gradients' => array( - array( - 'slug' => 'gradient', - 'gradient' => 'gradient', - ), - ), - ), - 'typography' => array( - 'fontSizes' => array( - array( - 'slug' => 'fontSize', - 'size' => 'fontSize', - ), - ), - 'fontFamilies' => array( - array( - 'slug' => 'fontFamily', - 'fontFamily' => 'fontFamily', - ), - ), - ), - ), - ); - - $expected = array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'settings' => array( - 'color' => array( - 'custom' => true, - 'customGradient' => true, - 'palette' => array( - 'theme' => array( - array( - 'slug' => 'blue', - 'color' => 'blue', - ), - ), - ), - 'gradients' => array( - 'theme' => array( - array( - 'slug' => 'gradient', - 'gradient' => 'gradient', - ), - ), - ), - ), - 'typography' => array( - 'fontSizes' => array( - 'theme' => array( - array( - 'slug' => 'fontSize', - 'size' => 'fontSize', - ), - ), - ), - 'fontFamilies' => array( - 'theme' => array( - array( - 'slug' => 'fontFamily', - 'fontFamily' => 'fontFamily', - ), - ), - ), - ), - 'blocks' => array( - 'core/paragraph' => array( - 'color' => array( - 'custom' => false, - ), - ), - 'core/list' => array( - 'color' => array( - 'custom' => false, - ), - ), - ), - ), - 'styles' => array( - 'typography' => array( - 'fontSize' => '12', - ), - 'blocks' => array( - 'core/group' => array( - 'spacing' => array( - 'padding' => array( - 'top' => '12px', - 'bottom' => '12px', - ), - ), - ), - 'core/list' => array( - 'typography' => array( - 'fontSize' => '12', - ), - 'color' => array( - 'background' => 'brown', - ), - ), - ), - ), - ); - - $theme_json->merge( new WP_Theme_JSON_Gutenberg( $add_new_block ) ); - $theme_json->merge( new WP_Theme_JSON_Gutenberg( $add_key_in_settings ) ); - $theme_json->merge( new WP_Theme_JSON_Gutenberg( $update_key_in_settings ) ); - $theme_json->merge( new WP_Theme_JSON_Gutenberg( $add_styles ) ); - $theme_json->merge( new WP_Theme_JSON_Gutenberg( $add_key_in_styles ) ); - $theme_json->merge( new WP_Theme_JSON_Gutenberg( $add_invalid_context ) ); - $theme_json->merge( new WP_Theme_JSON_Gutenberg( $update_presets ) ); - $actual = $theme_json->get_raw_data(); - - $this->assertEqualSetsWithIndex( $expected, $actual ); - } - - public function test_merge_incoming_data_empty_presets() { - $theme_json = new WP_Theme_JSON_Gutenberg( - array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'settings' => array( - 'color' => array( - 'duotone' => array( - array( - 'slug' => 'value', - 'colors' => array( 'red', 'green' ), - ), - ), - 'gradients' => array( - array( - 'slug' => 'gradient', - 'gradient' => 'gradient', - ), - ), - 'palette' => array( - array( - 'slug' => 'red', - 'color' => 'red', - ), - ), - ), - 'spacing' => array( - 'units' => array( 'px', 'em' ), - ), - 'typography' => array( - 'fontSizes' => array( - array( - 'slug' => 'size', - 'value' => 'size', - ), - ), - ), - ), - ) - ); - - $theme_json->merge( - new WP_Theme_JSON_Gutenberg( - array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'settings' => array( - 'color' => array( - 'duotone' => array(), - 'gradients' => array(), - 'palette' => array(), - ), - 'spacing' => array( - 'units' => array(), - ), - 'typography' => array( - 'fontSizes' => array(), - ), - ), - ) - ) - ); - - $actual = $theme_json->get_raw_data(); - $expected = array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'settings' => array( - 'color' => array( - 'duotone' => array( - 'theme' => array(), - ), - 'gradients' => array( - 'theme' => array(), - ), - 'palette' => array( - 'theme' => array(), - ), - ), - 'spacing' => array( - 'units' => array(), - ), - 'typography' => array( - 'fontSizes' => array( - 'theme' => array(), - ), - ), - ), - ); - - $this->assertEqualSetsWithIndex( $expected, $actual ); - } - - public function test_merge_incoming_data_null_presets() { - $theme_json = new WP_Theme_JSON_Gutenberg( - array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'settings' => array( - 'color' => array( - 'duotone' => array( - array( - 'slug' => 'value', - 'colors' => array( 'red', 'green' ), - ), - ), - 'gradients' => array( - array( - 'slug' => 'gradient', - 'gradient' => 'gradient', - ), - ), - 'palette' => array( - array( - 'slug' => 'red', - 'color' => 'red', - ), - ), - ), - 'spacing' => array( - 'units' => array( 'px', 'em' ), - ), - 'typography' => array( - 'fontSizes' => array( - array( - 'slug' => 'size', - 'value' => 'size', - ), - ), - ), - ), - ) - ); - - $theme_json->merge( - new WP_Theme_JSON_Gutenberg( - array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'settings' => array( - 'color' => array( - 'custom' => false, - ), - 'spacing' => array( - 'margin' => false, - ), - 'typography' => array( - 'lineHeight' => false, - ), - ), - ) - ) - ); - - $actual = $theme_json->get_raw_data(); - $expected = array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'settings' => array( - 'color' => array( - 'custom' => false, - 'duotone' => array( - 'theme' => array( - array( - 'slug' => 'value', - 'colors' => array( 'red', 'green' ), - ), - ), - ), - 'gradients' => array( - 'theme' => array( - array( - 'slug' => 'gradient', - 'gradient' => 'gradient', - ), - ), - ), - 'palette' => array( - 'theme' => array( - array( - 'slug' => 'red', - 'color' => 'red', - ), - ), - ), - ), - 'spacing' => array( - 'margin' => false, - 'units' => array( 'px', 'em' ), - ), - 'typography' => array( - 'lineHeight' => false, - 'fontSizes' => array( - 'theme' => array( - array( - 'slug' => 'size', - 'value' => 'size', - ), - ), - ), - ), - ), - ); - - $this->assertEqualSetsWithIndex( $expected, $actual ); - } - - public function test_merge_incoming_data_color_presets_with_same_slugs_as_default_are_removed() { - $defaults = new WP_Theme_JSON_Gutenberg( - array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'settings' => array( - 'color' => array( - 'defaultPalette' => true, - 'palette' => array( - array( - 'slug' => 'red', - 'color' => 'red', - 'name' => 'Red', - ), - array( - 'slug' => 'green', - 'color' => 'green', - 'name' => 'Green', - ), - ), - ), - 'blocks' => array( - 'core/paragraph' => array( - 'color' => array( - 'palette' => array( - array( - 'slug' => 'blue', - 'color' => 'blue', - 'name' => 'Blue', - ), - ), - ), - ), - ), - ), - ), - 'default' - ); - $theme = new WP_Theme_JSON_Gutenberg( - array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'settings' => array( - 'color' => array( - 'palette' => array( - array( - 'slug' => 'pink', - 'color' => 'pink', - 'name' => 'Pink', - ), - array( - 'slug' => 'green', - 'color' => 'green', - 'name' => 'Greenish', - ), - ), - ), - 'blocks' => array( - 'core/paragraph' => array( - 'color' => array( - 'palette' => array( - array( - 'slug' => 'blue', - 'color' => 'blue', - 'name' => 'Bluish', - ), - array( - 'slug' => 'yellow', - 'color' => 'yellow', - 'name' => 'Yellow', - ), - array( - 'slug' => 'green', - 'color' => 'green', - 'name' => 'Block Green', - ), - ), - ), - ), - ), - ), - ) - ); - - $expected = array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'settings' => array( - 'color' => array( - 'palette' => array( - 'default' => array( - array( - 'slug' => 'red', - 'color' => 'red', - 'name' => 'Red', - ), - array( - 'slug' => 'green', - 'color' => 'green', - 'name' => 'Green', - ), - ), - 'theme' => array( - array( - 'slug' => 'pink', - 'color' => 'pink', - 'name' => 'Pink', - ), - ), - ), - 'defaultPalette' => true, - ), - 'blocks' => array( - 'core/paragraph' => array( - 'color' => array( - 'palette' => array( - 'default' => array( - array( - 'slug' => 'blue', - 'color' => 'blue', - 'name' => 'Blue', - ), - ), - 'theme' => array( - array( - 'slug' => 'yellow', - 'color' => 'yellow', - 'name' => 'Yellow', - ), - ), - ), - ), - ), - ), - ), - ); - - $defaults->merge( $theme ); - $actual = $defaults->get_raw_data(); - - $this->assertEqualSetsWithIndex( $expected, $actual ); - } - - public function test_merge_incoming_data_color_presets_with_same_slugs_as_default_are_not_removed_if_defaults_are_disabled() { - $defaults = new WP_Theme_JSON_Gutenberg( - array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'settings' => array( - 'color' => array( - 'defaultPalette' => true, // Emulate the defaults from core theme.json. - 'palette' => array( - array( - 'slug' => 'red', - 'color' => 'red', - 'name' => 'Red', - ), - array( - 'slug' => 'green', - 'color' => 'green', - 'name' => 'Green', - ), - ), - ), - 'blocks' => array( - 'core/paragraph' => array( - 'color' => array( - 'palette' => array( - array( - 'slug' => 'blue', - 'color' => 'blue', - 'name' => 'Blue', - ), - ), - ), - ), - ), - ), - ), - 'default' - ); - $theme = new WP_Theme_JSON_Gutenberg( - array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'settings' => array( - 'color' => array( - 'defaultPalette' => false, - 'palette' => array( - array( - 'slug' => 'pink', - 'color' => 'pink', - 'name' => 'Pink', - ), - array( - 'slug' => 'green', - 'color' => 'green', - 'name' => 'Greenish', - ), - ), - ), - 'blocks' => array( - 'core/paragraph' => array( - 'color' => array( - 'palette' => array( - array( - 'slug' => 'blue', - 'color' => 'blue', - 'name' => 'Bluish', - ), - array( - 'slug' => 'yellow', - 'color' => 'yellow', - 'name' => 'Yellow', - ), - array( - 'slug' => 'green', - 'color' => 'green', - 'name' => 'Block Green', - ), - ), - ), - ), - ), - ), - ) - ); - - $expected = array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'settings' => array( - 'color' => array( - 'defaultPalette' => false, - 'palette' => array( - 'default' => array( - array( - 'slug' => 'red', - 'color' => 'red', - 'name' => 'Red', - ), - array( - 'slug' => 'green', - 'color' => 'green', - 'name' => 'Green', - ), - ), - 'theme' => array( - array( - 'slug' => 'pink', - 'color' => 'pink', - 'name' => 'Pink', - ), - array( - 'slug' => 'green', - 'color' => 'green', - 'name' => 'Greenish', - ), - ), - ), - ), - 'blocks' => array( - 'core/paragraph' => array( - 'color' => array( - 'palette' => array( - 'default' => array( - array( - 'slug' => 'blue', - 'color' => 'blue', - 'name' => 'Blue', - ), - ), - 'theme' => array( - array( - 'slug' => 'blue', - 'color' => 'blue', - 'name' => 'Bluish', - ), - array( - 'slug' => 'yellow', - 'color' => 'yellow', - 'name' => 'Yellow', - ), - array( - 'slug' => 'green', - 'color' => 'green', - 'name' => 'Block Green', - ), - ), - ), - ), - ), - ), - ), - ); - - $defaults->merge( $theme ); - $actual = $defaults->get_raw_data(); - - $this->assertEqualSetsWithIndex( $expected, $actual ); - } - - public function test_merge_incoming_data_presets_use_default_names() { - $defaults = new WP_Theme_JSON_Gutenberg( - array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'settings' => array( - 'typography' => array( - 'fontSizes' => array( - array( - 'name' => 'Small', - 'slug' => 'small', - 'size' => '12px', - ), - array( - 'name' => 'Large', - 'slug' => 'large', - 'size' => '20px', - ), - ), - ), - ), - ), - 'default' - ); - $theme_json = new WP_Theme_JSON_Gutenberg( - array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'settings' => array( - 'typography' => array( - 'fontSizes' => array( - array( - 'slug' => 'small', - 'size' => '1.1rem', - ), - array( - 'slug' => 'large', - 'size' => '1.75rem', - ), - array( - 'name' => 'Huge', - 'slug' => 'huge', - 'size' => '3rem', - ), - ), - ), - ), - ), - 'theme' - ); - $expected = array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'settings' => array( - 'typography' => array( - 'fontSizes' => array( - 'theme' => array( - array( - 'name' => 'Small', - 'slug' => 'small', - 'size' => '1.1rem', - ), - array( - 'name' => 'Large', - 'slug' => 'large', - 'size' => '1.75rem', - ), - array( - 'name' => 'Huge', - 'slug' => 'huge', - 'size' => '3rem', - ), - ), - 'default' => array( - array( - 'name' => 'Small', - 'slug' => 'small', - 'size' => '12px', - ), - array( - 'name' => 'Large', - 'slug' => 'large', - 'size' => '20px', - ), - ), - ), - ), - ), - ); - $defaults->merge( $theme_json ); - $actual = $defaults->get_raw_data(); - $this->assertEqualSetsWithIndex( $expected, $actual ); - } - - - - function test_remove_insecure_properties_removes_unsafe_styles() { - $actual = WP_Theme_JSON_Gutenberg::remove_insecure_properties( - array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'styles' => array( - 'color' => array( - 'gradient' => 'url(\'data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHdpZHRoPScxMCcgaGVpZ2h0PScxMCc+PHNjcmlwdD5hbGVydCgnb2snKTwvc2NyaXB0PjxsaW5lYXJHcmFkaWVudCBpZD0nZ3JhZGllbnQnPjxzdG9wIG9mZnNldD0nMTAlJyBzdG9wLWNvbG9yPScjRjAwJy8+PHN0b3Agb2Zmc2V0PSc5MCUnIHN0b3AtY29sb3I9JyNmY2MnLz4gPC9saW5lYXJHcmFkaWVudD48cmVjdCBmaWxsPSd1cmwoI2dyYWRpZW50KScgeD0nMCcgeT0nMCcgd2lkdGg9JzEwMCUnIGhlaWdodD0nMTAwJScvPjwvc3ZnPg==\')', - 'text' => 'var:preset|color|dark-red', - ), - 'elements' => array( - 'link' => array( - 'color' => array( - 'gradient' => 'url(\'data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHdpZHRoPScxMCcgaGVpZ2h0PScxMCc+PHNjcmlwdD5hbGVydCgnb2snKTwvc2NyaXB0PjxsaW5lYXJHcmFkaWVudCBpZD0nZ3JhZGllbnQnPjxzdG9wIG9mZnNldD0nMTAlJyBzdG9wLWNvbG9yPScjRjAwJy8+PHN0b3Agb2Zmc2V0PSc5MCUnIHN0b3AtY29sb3I9JyNmY2MnLz4gPC9saW5lYXJHcmFkaWVudD48cmVjdCBmaWxsPSd1cmwoI2dyYWRpZW50KScgeD0nMCcgeT0nMCcgd2lkdGg9JzEwMCUnIGhlaWdodD0nMTAwJScvPjwvc3ZnPg==\')', - 'text' => 'var:preset|color|dark-pink', - 'background' => 'var:preset|color|dark-red', - ), - ), - ), - 'blocks' => array( - 'core/image' => array( - 'filter' => array( - 'duotone' => 'var:preset|duotone|blue-red', - ), - ), - 'core/cover' => array( - 'filter' => array( - 'duotone' => 'var(--wp--preset--duotone--blue-red, var(--fallback-unsafe))', - ), - ), - 'core/group' => array( - 'color' => array( - 'gradient' => 'url(\'data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHdpZHRoPScxMCcgaGVpZ2h0PScxMCc+PHNjcmlwdD5hbGVydCgnb2snKTwvc2NyaXB0PjxsaW5lYXJHcmFkaWVudCBpZD0nZ3JhZGllbnQnPjxzdG9wIG9mZnNldD0nMTAlJyBzdG9wLWNvbG9yPScjRjAwJy8+PHN0b3Agb2Zmc2V0PSc5MCUnIHN0b3AtY29sb3I9JyNmY2MnLz4gPC9saW5lYXJHcmFkaWVudD48cmVjdCBmaWxsPSd1cmwoI2dyYWRpZW50KScgeD0nMCcgeT0nMCcgd2lkdGg9JzEwMCUnIGhlaWdodD0nMTAwJScvPjwvc3ZnPg==\')', - 'text' => 'var:preset|color|dark-gray', - ), - 'elements' => array( - 'link' => array( - 'color' => array( - 'gradient' => 'url(\'data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHdpZHRoPScxMCcgaGVpZ2h0PScxMCc+PHNjcmlwdD5hbGVydCgnb2snKTwvc2NyaXB0PjxsaW5lYXJHcmFkaWVudCBpZD0nZ3JhZGllbnQnPjxzdG9wIG9mZnNldD0nMTAlJyBzdG9wLWNvbG9yPScjRjAwJy8+PHN0b3Agb2Zmc2V0PSc5MCUnIHN0b3AtY29sb3I9JyNmY2MnLz4gPC9saW5lYXJHcmFkaWVudD48cmVjdCBmaWxsPSd1cmwoI2dyYWRpZW50KScgeD0nMCcgeT0nMCcgd2lkdGg9JzEwMCUnIGhlaWdodD0nMTAwJScvPjwvc3ZnPg==\')', - 'text' => 'var:preset|color|dark-pink', - ), - ), - ), - ), - 'invalid/key' => array( - 'background' => 'green', - ), - ), - ), - ) - ); - - $expected = array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'styles' => array( - 'color' => array( - 'text' => 'var:preset|color|dark-red', - ), - 'elements' => array( - 'link' => array( - 'color' => array( - 'text' => 'var:preset|color|dark-pink', - 'background' => 'var:preset|color|dark-red', - ), - ), - ), - 'blocks' => array( - 'core/image' => array( - 'filter' => array( - 'duotone' => 'var:preset|duotone|blue-red', - ), - ), - 'core/group' => array( - 'color' => array( - 'text' => 'var:preset|color|dark-gray', - ), - 'elements' => array( - 'link' => array( - 'color' => array( - 'text' => 'var:preset|color|dark-pink', - ), - ), - ), - ), - ), - ), - ); - $this->assertEqualSetsWithIndex( $expected, $actual ); - } - - function test_remove_insecure_properties_removes_unsafe_styles_sub_properties() { - $actual = WP_Theme_JSON_Gutenberg::remove_insecure_properties( - array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'styles' => array( - 'border' => array( - 'radius' => array( - 'topLeft' => '6px', - 'topRight' => 'var(--top-right, var(--unsafe-fallback))', - 'bottomRight' => '6px', - 'bottomLeft' => '6px', - ), - ), - 'spacing' => array( - 'padding' => array( - 'top' => '1px', - 'right' => '1px', - 'bottom' => 'var(--bottom, var(--unsafe-fallback))', - 'left' => '1px', - ), - ), - 'elements' => array( - 'link' => array( - 'spacing' => array( - 'padding' => array( - 'top' => '2px', - 'right' => '2px', - 'bottom' => 'var(--bottom, var(--unsafe-fallback))', - 'left' => '2px', - ), - ), - ), - ), - 'blocks' => array( - 'core/group' => array( - 'border' => array( - 'radius' => array( - 'topLeft' => '5px', - 'topRight' => 'var(--top-right, var(--unsafe-fallback))', - 'bottomRight' => '5px', - 'bottomLeft' => '5px', - ), - ), - 'spacing' => array( - 'padding' => array( - 'top' => '3px', - 'right' => '3px', - 'bottom' => 'var(bottom, var(--unsafe-fallback))', - 'left' => '3px', - ), - ), - 'elements' => array( - 'link' => array( - 'spacing' => array( - 'padding' => array( - 'top' => '4px', - 'right' => '4px', - 'bottom' => 'var(--bottom, var(--unsafe-fallback))', - 'left' => '4px', - ), - ), - ), - ), - ), - ), - ), - ), - true - ); - - $expected = array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'styles' => array( - 'border' => array( - 'radius' => array( - 'topLeft' => '6px', - 'bottomRight' => '6px', - 'bottomLeft' => '6px', - ), - ), - 'spacing' => array( - 'padding' => array( - 'top' => '1px', - 'right' => '1px', - 'left' => '1px', - ), - ), - 'elements' => array( - 'link' => array( - 'spacing' => array( - 'padding' => array( - 'top' => '2px', - 'right' => '2px', - 'left' => '2px', - ), - ), - ), - ), - 'blocks' => array( - 'core/group' => array( - 'border' => array( - 'radius' => array( - 'topLeft' => '5px', - 'bottomRight' => '5px', - 'bottomLeft' => '5px', - ), - ), - 'spacing' => array( - 'padding' => array( - 'top' => '3px', - 'right' => '3px', - 'left' => '3px', - ), - ), - 'elements' => array( - 'link' => array( - 'spacing' => array( - 'padding' => array( - 'top' => '4px', - 'right' => '4px', - 'left' => '4px', - ), - ), - ), - ), - ), - ), - ), - ); - $this->assertEqualSetsWithIndex( $expected, $actual ); - } - - function test_remove_insecure_properties_removes_non_preset_settings() { - $actual = WP_Theme_JSON_Gutenberg::remove_insecure_properties( - array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'settings' => array( - 'color' => array( - 'custom' => true, - 'palette' => array( - 'custom' => array( - array( - 'name' => 'Red', - 'slug' => 'red', - 'color' => '#ff0000', - ), - array( - 'name' => 'Green', - 'slug' => 'green', - 'color' => '#00ff00', - ), - array( - 'name' => 'Blue', - 'slug' => 'blue', - 'color' => '#0000ff', - ), - ), - ), - ), - 'spacing' => array( - 'padding' => false, - ), - 'blocks' => array( - 'core/group' => array( - 'color' => array( - 'custom' => true, - 'palette' => array( - 'custom' => array( - array( - 'name' => 'Yellow', - 'slug' => 'yellow', - 'color' => '#ff0000', - ), - array( - 'name' => 'Pink', - 'slug' => 'pink', - 'color' => '#00ff00', - ), - array( - 'name' => 'Orange', - 'slug' => 'orange', - 'color' => '#0000ff', - ), - ), - ), - ), - 'spacing' => array( - 'padding' => false, - ), - ), - ), - ), - ) - ); - - $expected = array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'settings' => array( - 'color' => array( - 'palette' => array( - 'custom' => array( - array( - 'name' => 'Red', - 'slug' => 'red', - 'color' => '#ff0000', - ), - array( - 'name' => 'Green', - 'slug' => 'green', - 'color' => '#00ff00', - ), - array( - 'name' => 'Blue', - 'slug' => 'blue', - 'color' => '#0000ff', - ), - ), - ), - ), - 'blocks' => array( - 'core/group' => array( - 'color' => array( - 'palette' => array( - 'custom' => array( - array( - 'name' => 'Yellow', - 'slug' => 'yellow', - 'color' => '#ff0000', - ), - array( - 'name' => 'Pink', - 'slug' => 'pink', - 'color' => '#00ff00', - ), - array( - 'name' => 'Orange', - 'slug' => 'orange', - 'color' => '#0000ff', - ), - ), - ), - ), - ), - ), - ), - ); - $this->assertEqualSetsWithIndex( $expected, $actual ); - } - - function test_remove_insecure_properties_removes_unsafe_preset_settings() { - $actual = WP_Theme_JSON_Gutenberg::remove_insecure_properties( - array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'settings' => array( - 'color' => array( - 'palette' => array( - 'custom' => array( - array( - 'name' => 'Red/><b>ok</ok>', - 'slug' => 'red', - 'color' => '#ff0000', - ), - array( - 'name' => 'Green', - 'slug' => 'a" attr', - 'color' => '#00ff00', - ), - array( - 'name' => 'Blue', - 'slug' => 'blue', - 'color' => 'var(--color, var(--unsafe-fallback))', - ), - array( - 'name' => 'Pink', - 'slug' => 'pink', - 'color' => '#FFC0CB', - ), - ), - ), - ), - 'typography' => array( - 'fontFamilies' => array( - 'custom' => array( - array( - 'name' => 'Helvetica Arial/><b>test</b>', - 'slug' => 'helvetica-arial', - 'fontFamily' => 'Helvetica Neue, Helvetica, Arial, sans-serif', - ), - array( - 'name' => 'Geneva', - 'slug' => 'geneva#asa', - 'fontFamily' => 'Geneva, Tahoma, Verdana, sans-serif', - ), - array( - 'name' => 'Cambria', - 'slug' => 'cambria', - 'fontFamily' => 'Cambria, Georgia, serif', - ), - array( - 'name' => 'Helvetica Arial', - 'slug' => 'helvetica-arial', - 'fontFamily' => 'var(--fontFamily, var(--unsafe-fallback))', - ), - ), - ), - ), - 'blocks' => array( - 'core/group' => array( - 'color' => array( - 'palette' => array( - 'custom' => array( - array( - 'name' => 'Red/><b>ok</ok>', - 'slug' => 'red', - 'color' => '#ff0000', - ), - array( - 'name' => 'Green', - 'slug' => 'a" attr', - 'color' => '#00ff00', - ), - array( - 'name' => 'Blue', - 'slug' => 'blue', - 'color' => 'var(--color, var(--unsafe--fallback))', - ), - array( - 'name' => 'Pink', - 'slug' => 'pink', - 'color' => '#FFC0CB', - ), - ), - ), - ), - ), - ), - ), - ) - ); - - $expected = array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'settings' => array( - 'color' => array( - 'palette' => array( - 'custom' => array( - array( - 'name' => 'Pink', - 'slug' => 'pink', - 'color' => '#FFC0CB', - ), - ), - ), - ), - 'typography' => array( - 'fontFamilies' => array( - 'custom' => array( - array( - 'name' => 'Cambria', - 'slug' => 'cambria', - 'fontFamily' => 'Cambria, Georgia, serif', - ), - ), - ), - ), - 'blocks' => array( - 'core/group' => array( - 'color' => array( - 'palette' => array( - 'custom' => array( - array( - 'name' => 'Pink', - 'slug' => 'pink', - 'color' => '#FFC0CB', - ), - ), - ), - ), - ), - ), - ), - ); - $this->assertEqualSetsWithIndex( $expected, $actual ); - } + $element_styles = 'a{background-color: red;color: green;}a:hover{background-color: green;color: red;}.wp-block-group a:hover{background-color: black;color: yellow;}'; - function test_remove_insecure_properties_applies_safe_styles() { - $actual = WP_Theme_JSON_Gutenberg::remove_insecure_properties( - array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'styles' => array( - 'color' => array( - 'text' => '#abcabc ', // Trailing space. - ), - ), - ), - true - ); + $expected = $base_styles . $element_styles; - $expected = array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'styles' => array( - 'color' => array( - 'text' => '#abcabc ', - ), - ), - ); - $this->assertEqualSetsWithIndex( $expected, $actual ); + $this->assertEquals( $expected, $theme_json->get_stylesheet() ); + $this->assertEquals( $expected, $theme_json->get_stylesheet( array( 'styles' ) ) ); } function test_remove_invalid_element_pseudo_selectors() { @@ -2320,494 +310,6 @@ function test_remove_invalid_element_pseudo_selectors() { $this->assertEqualSetsWithIndex( $expected, $actual ); } - function test_get_custom_templates() { - $theme_json = new WP_Theme_JSON_Gutenberg( - array( - 'customTemplates' => array( - array( - 'name' => 'page-home', - 'title' => 'Homepage template', - ), - ), - ) - ); - - $page_templates = $theme_json->get_custom_templates(); - - $this->assertEqualSetsWithIndex( - $page_templates, - array( - 'page-home' => array( - 'title' => 'Homepage template', - 'postTypes' => array( 'page' ), - ), - ) - ); - } - - function test_get_template_parts() { - $theme_json = new WP_Theme_JSON_Gutenberg( - array( - 'templateParts' => array( - array( - 'name' => 'small-header', - 'title' => 'Small Header', - 'area' => 'header', - ), - ), - ) - ); - - $template_parts = $theme_json->get_template_parts(); - - $this->assertEqualSetsWithIndex( - $template_parts, - array( - 'small-header' => array( - 'title' => 'Small Header', - 'area' => 'header', - ), - ) - ); - } - - function test_get_from_editor_settings() { - $input = array( - 'disableCustomColors' => true, - 'disableCustomGradients' => true, - 'disableCustomFontSizes' => true, - 'enableCustomLineHeight' => true, - 'enableCustomUnits' => true, - 'colors' => array( - array( - 'slug' => 'color-slug', - 'name' => 'Color Name', - 'color' => 'colorvalue', - ), - ), - 'gradients' => array( - array( - 'slug' => 'gradient-slug', - 'name' => 'Gradient Name', - 'gradient' => 'gradientvalue', - ), - ), - 'fontSizes' => array( - array( - 'slug' => 'size-slug', - 'name' => 'Size Name', - 'size' => 'sizevalue', - ), - ), - ); - - $expected = array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'settings' => array( - 'color' => array( - 'custom' => false, - 'customGradient' => false, - 'gradients' => array( - array( - 'slug' => 'gradient-slug', - 'name' => 'Gradient Name', - 'gradient' => 'gradientvalue', - ), - ), - 'palette' => array( - array( - 'slug' => 'color-slug', - 'name' => 'Color Name', - 'color' => 'colorvalue', - ), - ), - ), - 'spacing' => array( - 'units' => array( 'px', 'em', 'rem', 'vh', 'vw', '%' ), - ), - 'typography' => array( - 'customFontSize' => false, - 'lineHeight' => true, - 'fontSizes' => array( - array( - 'slug' => 'size-slug', - 'name' => 'Size Name', - 'size' => 'sizevalue', - ), - ), - ), - ), - ); - - $actual = WP_Theme_JSON_Gutenberg::get_from_editor_settings( $input ); - - $this->assertEqualSetsWithIndex( $expected, $actual ); - } - - function test_get_editor_settings_no_theme_support() { - $input = array( - '__unstableEnableFullSiteEditingBlocks' => false, - 'disableCustomColors' => false, - 'disableCustomFontSizes' => false, - 'disableCustomGradients' => false, - 'enableCustomLineHeight' => false, - 'enableCustomUnits' => false, - 'imageSizes' => array( - array( - 'slug' => 'thumbnail', - 'name' => 'Thumbnail', - ), - array( - 'slug' => 'medium', - 'name' => 'Medium', - ), - array( - 'slug' => 'large', - 'name' => 'Large', - ), - array( - 'slug' => 'full', - 'name' => 'Full Size', - ), - ), - 'isRTL' => false, - 'maxUploadFileSize' => 123, - ); - - $expected = array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'settings' => array( - 'color' => array( - 'custom' => true, - 'customGradient' => true, - ), - 'spacing' => array( - 'units' => false, - ), - 'typography' => array( - 'customFontSize' => true, - 'lineHeight' => false, - ), - ), - ); - - $actual = WP_Theme_JSON_Gutenberg::get_from_editor_settings( $input ); - - $this->assertEqualSetsWithIndex( $expected, $actual ); - } - - function test_get_editor_settings_blank() { - $expected = array( - 'version' => WP_Theme_JSON_Gutenberg::LATEST_SCHEMA, - 'settings' => array(), - ); - $actual = WP_Theme_JSON_Gutenberg::get_from_editor_settings( array() ); - - $this->assertEqualSetsWithIndex( $expected, $actual ); - } - - function test_get_editor_settings_custom_units_can_be_disabled() { - add_theme_support( 'custom-units', array() ); - $actual = WP_Theme_JSON_Gutenberg::get_from_editor_settings( get_default_block_editor_settings() ); - remove_theme_support( 'custom-units' ); - - $expected = array( - 'units' => array( array() ), - 'padding' => false, - ); - - $this->assertEqualSetsWithIndex( $expected, $actual['settings']['spacing'] ); - } - - function test_get_editor_settings_custom_units_can_be_enabled() { - add_theme_support( 'custom-units' ); - $actual = WP_Theme_JSON_Gutenberg::get_from_editor_settings( get_default_block_editor_settings() ); - remove_theme_support( 'custom-units' ); - - $expected = array( - 'units' => array( 'px', 'em', 'rem', 'vh', 'vw', '%' ), - 'padding' => false, - ); - - $this->assertEqualSetsWithIndex( $expected, $actual['settings']['spacing'] ); - } - - function test_get_editor_settings_custom_units_can_be_filtered() { - add_theme_support( 'custom-units', 'rem', 'em' ); - $actual = WP_Theme_JSON_Gutenberg::get_from_editor_settings( get_default_block_editor_settings() ); - remove_theme_support( 'custom-units' ); - - $expected = array( - 'units' => array( 'rem', 'em' ), - 'padding' => false, - ); - - $this->assertEqualSetsWithIndex( $expected, $actual['settings']['spacing'] ); - } - - function test_sanitization() { - $theme_json = new WP_Theme_JSON_Gutenberg( - array( - 'version' => 2, - 'styles' => array( - 'spacing' => array( - 'blockGap' => 'valid value', - ), - 'blocks' => array( - 'core/group' => array( - 'spacing' => array( - 'margin' => 'valid value', - 'blockGap' => 'invalid value', - ), - ), - ), - ), - ) - ); - - $actual = $theme_json->get_raw_data(); - $expected = array( - 'version' => 2, - 'styles' => array( - 'spacing' => array( - 'blockGap' => 'valid value', - ), - 'blocks' => array( - 'core/group' => array( - 'spacing' => array( - 'margin' => 'valid value', - ), - ), - ), - ), - ); - - $this->assertEqualSetsWithIndex( $expected, $actual ); - } - - function test_export_data() { - $theme = new WP_Theme_JSON_Gutenberg( - array( - 'version' => 2, - 'settings' => array( - 'color' => array( - 'palette' => array( - array( - 'slug' => 'white', - 'color' => 'white', - 'label' => 'White', - ), - array( - 'slug' => 'black', - 'color' => 'black', - 'label' => 'Black', - ), - ), - ), - ), - ) - ); - $user = new WP_Theme_JSON_Gutenberg( - array( - 'version' => 2, - 'settings' => array( - 'color' => array( - 'palette' => array( - array( - 'slug' => 'white', - 'color' => '#fff', - 'label' => 'User White', - ), - array( - 'slug' => 'hotpink', - 'color' => 'hotpink', - 'label' => 'hotpink', - ), - ), - ), - ), - ), - 'custom' - ); - - $theme->merge( $user ); - $actual = $theme->get_data(); - $expected = array( - 'version' => 2, - 'settings' => array( - 'color' => array( - 'palette' => array( - array( - 'slug' => 'white', - 'color' => '#fff', - 'label' => 'User White', - ), - array( - 'slug' => 'black', - 'color' => 'black', - 'label' => 'Black', - ), - array( - 'slug' => 'hotpink', - 'color' => 'hotpink', - 'label' => 'hotpink', - ), - ), - ), - ), - ); - - $this->assertEqualSetsWithIndex( $expected, $actual ); - } - - function test_export_data_deals_with_empty_user_data() { - $theme = new WP_Theme_JSON_Gutenberg( - array( - 'version' => 2, - 'settings' => array( - 'color' => array( - 'palette' => array( - array( - 'slug' => 'white', - 'color' => 'white', - 'label' => 'White', - ), - array( - 'slug' => 'black', - 'color' => 'black', - 'label' => 'Black', - ), - ), - ), - ), - ) - ); - - $actual = $theme->get_data(); - $expected = array( - 'version' => 2, - 'settings' => array( - 'color' => array( - 'palette' => array( - array( - 'slug' => 'white', - 'color' => 'white', - 'label' => 'White', - ), - array( - 'slug' => 'black', - 'color' => 'black', - 'label' => 'Black', - ), - ), - ), - ), - ); - - $this->assertEqualSetsWithIndex( $expected, $actual ); - } - - function test_export_data_deals_with_empty_theme_data() { - $user = new WP_Theme_JSON_Gutenberg( - array( - 'version' => 2, - 'settings' => array( - 'color' => array( - 'palette' => array( - array( - 'slug' => 'white', - 'color' => '#fff', - 'label' => 'User White', - ), - array( - 'slug' => 'hotpink', - 'color' => 'hotpink', - 'label' => 'hotpink', - ), - ), - ), - ), - ), - 'custom' - ); - - $actual = $user->get_data(); - $expected = array( - 'version' => 2, - 'settings' => array( - 'color' => array( - 'palette' => array( - array( - 'slug' => 'white', - 'color' => '#fff', - 'label' => 'User White', - ), - array( - 'slug' => 'hotpink', - 'color' => 'hotpink', - 'label' => 'hotpink', - ), - ), - ), - ), - ); - - $this->assertEqualSetsWithIndex( $expected, $actual ); - } - - function test_export_data_deals_with_empty_data() { - $theme_v2 = new WP_Theme_JSON_Gutenberg( - array( - 'version' => 2, - ), - 'theme' - ); - $actual_v2 = $theme_v2->get_data(); - $expected_v2 = array( 'version' => 2 ); - $this->assertEqualSetsWithIndex( $expected_v2, $actual_v2 ); - - $theme_v1 = new WP_Theme_JSON_Gutenberg( - array( - 'version' => 1, - ), - 'theme' - ); - $actual_v1 = $theme_v1->get_data(); - $expected_v1 = array( 'version' => 2 ); - $this->assertEqualSetsWithIndex( $expected_v1, $actual_v1 ); - } - - function test_export_data_sets_appearance_tools() { - $theme = new WP_Theme_JSON_Gutenberg( - array( - 'version' => 2, - 'settings' => array( - 'appearanceTools' => true, - 'blocks' => array( - 'core/paragraph' => array( - 'appearanceTools' => true, - ), - ), - ), - ) - ); - - $actual = $theme->get_data(); - $expected = array( - 'version' => 2, - 'settings' => array( - 'appearanceTools' => true, - 'blocks' => array( - 'core/paragraph' => array( - 'appearanceTools' => true, - ), - ), - ), - ); - - $this->assertEqualSetsWithIndex( $expected, $actual ); - } - function test_get_element_class_name_button() { $expected = 'wp-element-button'; $actual = WP_Theme_JSON_Gutenberg::get_element_class_name( 'button' ); diff --git a/phpunit/global-styles-test.php b/phpunit/global-styles-test.php deleted file mode 100644 index 39712522293db..0000000000000 --- a/phpunit/global-styles-test.php +++ /dev/null @@ -1,28 +0,0 @@ -<?php - -/** - * Test the Global Styles lib - * - * @package Gutenberg - */ - -class WP_Global_Styles_Test extends WP_UnitTestCase { - function test_gutenberg_global_styles_filter_post() { - $user_theme_json = '{ - "isGlobalStylesUserThemeJSON": 1, - "version": 1, - "settings": { - "typography": { - "fontFamilies": { - "fontFamily": "\"DM Sans\", sans-serif", - "slug": "dm-sans", - "name": "DM Sans", - } - } - } - }'; - - $filtered_user_theme_json = gutenberg_global_styles_filter_post( $user_theme_json ); - $this->assertEquals( $user_theme_json, $filtered_user_theme_json ); - } -} diff --git a/phpunit/navigation-test.php b/phpunit/navigation-test.php deleted file mode 100644 index d10f8cd24b835..0000000000000 --- a/phpunit/navigation-test.php +++ /dev/null @@ -1,94 +0,0 @@ -<?php -/** - * This class tests the functionality of block navigation - * - * @package Gutenberg - */ - -class WP_Navigation_Test extends WP_UnitTestCase { - const NAVIGATION_POST_TYPE = 'wp_navigation'; - const NON_NAVIGATION_POST_TYPE = 'wp_non_navigation'; - - public function setUp() { - $this->enable_editor_support(); - } - - public function tearDown() { - $this->enable_editor_support(); - } - - public function test_it_doesnt_disable_block_editor_for_non_navigation_post_types() { - $filtered_result = gutenberg_disable_block_editor_for_navigation_post_type( true, static::NON_NAVIGATION_POST_TYPE ); - $this->assertTrue( $filtered_result ); - } - - public function test_it_disables_block_editor_for_navigation_post_types() { - $filtered_result = gutenberg_disable_block_editor_for_navigation_post_type( true, static::NAVIGATION_POST_TYPE ); - $this->assertFalse( $filtered_result ); - } - - public function test_it_doesnt_disable_content_editor_for_non_navigation_type_posts() { - $post = $this->create_non_navigation_post(); - $this->assertTrue( $this->supports_block_editor() ); - - gutenberg_disable_content_editor_for_navigation_post_type( $post ); - - $this->assertTrue( $this->supports_block_editor() ); - } - - public function test_it_disables_content_editor_for_navigation_type_posts() { - $post = $this->create_navigation_post(); - $this->assertTrue( $this->supports_block_editor() ); - - gutenberg_disable_content_editor_for_navigation_post_type( $post ); - - $this->assertFalse( $this->supports_block_editor() ); - } - - public function test_it_enables_content_editor_for_non_navigation_type_posts_after_the_content_editor_form() { - $this->disable_editor_support(); - $post = $this->create_navigation_post(); - $this->assertFalse( $this->supports_block_editor() ); - - gutenberg_enable_content_editor_for_navigation_post_type( $post ); - - $this->assertTrue( $this->supports_block_editor() ); - } - - public function test_it_doesnt_enable_content_editor_for_non_navigation_type_posts_after_the_content_editor_form() { - $this->disable_editor_support(); - $post = $this->create_non_navigation_post(); - $this->assertFalse( $this->supports_block_editor() ); - - gutenberg_enable_content_editor_for_navigation_post_type( $post ); - - $this->assertFalse( $this->supports_block_editor() ); - } - - private function create_post( $type ) { - $post = new WP_Post( new StdClass() ); - $post->post_type = $type; - $post->filter = 'raw'; - return $post; - } - - private function create_non_navigation_post() { - return $this->create_post( static::NON_NAVIGATION_POST_TYPE ); - } - - private function create_navigation_post() { - return $this->create_post( static::NAVIGATION_POST_TYPE ); - } - - private function supports_block_editor() { - return post_type_supports( static::NAVIGATION_POST_TYPE, 'editor' ); - } - - private function enable_editor_support() { - add_post_type_support( static::NAVIGATION_POST_TYPE, 'editor' ); - } - - private function disable_editor_support() { - remove_post_type_support( static::NAVIGATION_POST_TYPE, 'editor' ); - } -} From 4073ce91a73b659877f7202ab0e3ad15497c9aa8 Mon Sep 17 00:00:00 2001 From: ntsekouras <ntsekouras@outlook.com> Date: Wed, 29 Jun 2022 15:24:12 +0300 Subject: [PATCH 09/13] remove more tests that have been back ported --- phpunit/class-elements-test.php | 107 ------------------ phpunit/functions-test.php | 91 --------------- .../translate-settings-using-i18n-schema.php | 97 ---------------- 3 files changed, 295 deletions(-) delete mode 100644 phpunit/class-elements-test.php delete mode 100644 phpunit/functions-test.php delete mode 100644 phpunit/translate-settings-using-i18n-schema.php diff --git a/phpunit/class-elements-test.php b/phpunit/class-elements-test.php deleted file mode 100644 index 0d0c11c55d5ed..0000000000000 --- a/phpunit/class-elements-test.php +++ /dev/null @@ -1,107 +0,0 @@ -<?php -/** - * Test Elements Hook. - * - * @package Gutenberg - */ -class Gutenberg_Elements_Test extends WP_UnitTestCase { - /** - * Given a string containing a class prefixed by "wp-elements-" followed by a unique id, - * this function returns a string where the id is one instead of being unique. - * - * @param string $string String containing unique id classes. - * @return string String where the unique id classes were replaced with "wp-elements-1". - */ - private static function make_unique_id_one( $string ) { - return preg_replace( '/wp-elements-[a-zA-Z0-9]+/', 'wp-elements-1', $string ); - } - - /** - * Test gutenberg_render_elements_support() with a simple paragraph and link color preset. - */ - public function test_simple_paragraph_link_color() { - $result = self::make_unique_id_one( - gutenberg_render_elements_support( - '<p>Hello <a href="http://www.wordpress.org/">WordPress</a>!</p>', - array( - 'blockName' => 'core/paragraph', - 'attrs' => array( - 'style' => array( - 'elements' => array( - 'link' => array( - 'color' => array( - 'text' => 'var:preset|color|subtle-background', - ), - ), - ), - ), - ), - ) - ) - ); - $this->assertSame( - $result, - '<p class="wp-elements-1">Hello <a href="http://www.wordpress.org/">WordPress</a>!</p>' - ); - } - - /** - * Test gutenberg_render_elements_support() with a paragraph containing a class. - */ - public function test_class_paragraph_link_color() { - $result = self::make_unique_id_one( - gutenberg_render_elements_support( - '<p class="has-dark-gray-background-color has-background">Hello <a href="http://www.wordpress.org/">WordPress</a>!</p>', - array( - 'blockName' => 'core/paragraph', - 'attrs' => array( - 'style' => array( - 'elements' => array( - 'link' => array( - 'color' => array( - 'text' => 'red', - ), - ), - ), - ), - 'backgroundColor' => 'dark-gray', - ), - ) - ) - ); - $this->assertSame( - $result, - '<p class="wp-elements-1 has-dark-gray-background-color has-background">Hello <a href="http://www.wordpress.org/">WordPress</a>!</p>' - ); - } - - /** - * Test gutenberg_render_elements_support() with a paragraph containing a anchor. - */ - public function test_anchor_paragraph_link_color() { - $result = self::make_unique_id_one( - gutenberg_render_elements_support( - '<p id="anchor">Hello <a href="http://www.wordpress.org/">WordPress</a>!</p>', - array( - 'blockName' => 'core/paragraph', - 'attrs' => array( - 'style' => array( - 'elements' => array( - 'link' => array( - 'color' => array( - 'text' => '#fff000', - ), - ), - ), - ), - ), - ) - ) - ); - $this->assertSame( - $result, - '<p id="anchor" class="wp-elements-1">Hello <a href="http://www.wordpress.org/">WordPress</a>!</p>' - ); - } - -} diff --git a/phpunit/functions-test.php b/phpunit/functions-test.php deleted file mode 100644 index 87dcf0c7e77ee..0000000000000 --- a/phpunit/functions-test.php +++ /dev/null @@ -1,91 +0,0 @@ -<?php - -/** - * Test functions.php - * - * @package Gutenberg - */ - -class WP_Functions_Test extends WP_UnitTestCase { - function test_wp_recursive_ksort() { - // Create an array to test. - $theme_json = array( - 'version' => 1, - 'settings' => array( - 'typography' => array( - 'fontFamilies' => array( - 'fontFamily' => 'DM Sans, sans-serif', - 'slug' => 'dm-sans', - 'name' => 'DM Sans', - ), - ), - 'color' => array( - 'palette' => array( - array( - 'slug' => 'foreground', - 'color' => '#242321', - 'name' => 'Foreground', - ), - array( - 'slug' => 'background', - 'color' => '#FCFBF8', - 'name' => 'Background', - ), - array( - 'slug' => 'primary', - 'color' => '#71706E', - 'name' => 'Primary', - ), - array( - 'slug' => 'tertiary', - 'color' => '#CFCFCF', - 'name' => 'Tertiary', - ), - ), - ), - ), - ); - - // Sort the array. - wp_recursive_ksort( $theme_json ); - - // Expected result. - $expected_theme_json = array( - 'settings' => array( - 'color' => array( - 'palette' => array( - array( - 'color' => '#242321', - 'name' => 'Foreground', - 'slug' => 'foreground', - ), - array( - 'color' => '#FCFBF8', - 'name' => 'Background', - 'slug' => 'background', - ), - array( - 'color' => '#71706E', - 'name' => 'Primary', - 'slug' => 'primary', - ), - array( - 'color' => '#CFCFCF', - 'name' => 'Tertiary', - 'slug' => 'tertiary', - ), - ), - ), - 'typography' => array( - 'fontFamilies' => array( - 'fontFamily' => 'DM Sans, sans-serif', - 'name' => 'DM Sans', - 'slug' => 'dm-sans', - ), - ), - ), - 'version' => 1, - ); - $this->assertEquals( $theme_json, $expected_theme_json ); - } -} diff --git a/phpunit/translate-settings-using-i18n-schema.php b/phpunit/translate-settings-using-i18n-schema.php deleted file mode 100644 index f91cd84d6dad8..0000000000000 --- a/phpunit/translate-settings-using-i18n-schema.php +++ /dev/null @@ -1,97 +0,0 @@ -<?php -/** - * Test Tests_L10n_TranslateSettingsUsingI18nSchema class. - * - * @package Gutenberg - */ - -class Tests_L10n_TranslateSettingsUsingI18nSchema extends WP_UnitTestCase { - /** - * Returns Polish locale string. - * - * @return string - */ - function filter_set_locale_to_polish() { - return 'pl_PL'; - } - - function test_gutenberg_translate_settings_using_i18n_schema() { - $textdomain = 'notice'; - - add_filter( 'locale', array( $this, 'filter_set_locale_to_polish' ) ); - load_textdomain( $textdomain, realpath( __DIR__ . '/data/languages/plugins/notice-pl_PL.po' ) ); - - $i18n_schema = (object) array( - 'title' => 'block title', - 'keywords' => array( 'block keyword' ), - 'styles' => array( - (object) array( 'label' => 'block style label' ), - ), - 'context' => (object) array( - '*' => (object) array( - 'variations' => array( - (object) array( - 'title' => 'block variation title', - 'description' => 'block variation description', - 'keywords' => array( 'block variation keyword' ), - ), - ), - ), - ), - ); - $settings = array( - 'title' => 'Notice', - 'keywords' => array( - 'alert', - 'message', - ), - 'styles' => array( - array( 'label' => 'Default' ), - array( 'label' => 'Other' ), - ), - 'context' => array( - 'namespace' => array( - 'variations' => array( - array( - 'title' => 'Error', - 'description' => 'Shows error.', - 'keywords' => array( 'failure' ), - ), - ), - ), - ), - ); - $result = translate_settings_using_i18n_schema( - $i18n_schema, - $settings, - $textdomain - ); - - unload_textdomain( $textdomain ); - remove_filter( 'locale', array( $this, 'filter_set_locale_to_polish' ) ); - - $this->assertSame( 'Powiadomienie', $result['title'] ); - $this->assertSameSets( array( 'ostrzeżenie', 'wiadomość' ), $result['keywords'] ); - $this->assertSame( - array( - array( - 'label' => 'Domyślny', - ), - array( - 'label' => 'Inny', - ), - ), - $result['styles'] - ); - $this->assertSame( - array( - array( - 'title' => 'Błąd', - 'description' => 'Wyświetla błąd.', - 'keywords' => array( 'niepowodzenie' ), - ), - ), - $result['context']['namespace']['variations'] - ); - } -} From 04499011299e6591b3acf09f7810f66aee76b670 Mon Sep 17 00:00:00 2001 From: Ben Dwyer <ben@scruffian.com> Date: Thu, 30 Jun 2022 12:21:29 +0100 Subject: [PATCH 10/13] Add a class variable for core data on the 6.1 resolver --- .../wordpress-6.1/class-wp-theme-json-resolver-6-1.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/compat/wordpress-6.1/class-wp-theme-json-resolver-6-1.php b/lib/compat/wordpress-6.1/class-wp-theme-json-resolver-6-1.php index efb2025d6f4dd..310889c04e26f 100644 --- a/lib/compat/wordpress-6.1/class-wp-theme-json-resolver-6-1.php +++ b/lib/compat/wordpress-6.1/class-wp-theme-json-resolver-6-1.php @@ -16,6 +16,15 @@ * @access private */ class WP_Theme_JSON_Resolver_6_1 extends WP_Theme_JSON_Resolver_6_0 { + + /** + * Container for data coming from core. + * + * @since 5.8.0 + * @var WP_Theme_JSON + */ + public static $core = null; + /** * Given a theme.json structure modifies it in place * to update certain values by its translated strings From cad0e84155868652c2a8e1e260abe6403cffab3f Mon Sep 17 00:00:00 2001 From: ntsekouras <ntsekouras@outlook.com> Date: Thu, 30 Jun 2022 14:32:55 +0300 Subject: [PATCH 11/13] tweaks --- .../class-wp-theme-json-resolver-6-0.php | 17 ----------------- .../class-wp-theme-json-resolver-6-1.php | 2 +- 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/lib/compat/wordpress-6.0/class-wp-theme-json-resolver-6-0.php b/lib/compat/wordpress-6.0/class-wp-theme-json-resolver-6-0.php index 13e00171ce082..b85e8e335037a 100644 --- a/lib/compat/wordpress-6.0/class-wp-theme-json-resolver-6-0.php +++ b/lib/compat/wordpress-6.0/class-wp-theme-json-resolver-6-0.php @@ -38,23 +38,6 @@ protected static function translate( $theme_json, $domain = 'default' ) { return translate_settings_using_i18n_schema( static::$i18n_schema, $theme_json, $domain ); } - /** - * Return core's origin config. - * - * @return WP_Theme_JSON_Gutenberg Entity that holds core data. - */ - public static function get_core_data() { - if ( null !== static::$core ) { - return static::$core; - } - - $config = static::read_json_file( __DIR__ . '/theme.json' ); - $config = static::translate( $config ); - static::$core = new WP_Theme_JSON_Gutenberg( $config, 'default' ); - - return static::$core; - } - /** * Returns the theme's data. * diff --git a/lib/compat/wordpress-6.1/class-wp-theme-json-resolver-6-1.php b/lib/compat/wordpress-6.1/class-wp-theme-json-resolver-6-1.php index 310889c04e26f..f704daaffafb1 100644 --- a/lib/compat/wordpress-6.1/class-wp-theme-json-resolver-6-1.php +++ b/lib/compat/wordpress-6.1/class-wp-theme-json-resolver-6-1.php @@ -23,7 +23,7 @@ class WP_Theme_JSON_Resolver_6_1 extends WP_Theme_JSON_Resolver_6_0 { * @since 5.8.0 * @var WP_Theme_JSON */ - public static $core = null; + protected static $core = null; /** * Given a theme.json structure modifies it in place From 5992cdc61ecc9b6c0b3f78d086c29114557bd477 Mon Sep 17 00:00:00 2001 From: ntsekouras <ntsekouras@outlook.com> Date: Fri, 1 Jul 2022 10:41:29 +0300 Subject: [PATCH 12/13] remove navigation test --- .../specs/editor/blocks/navigation.test.js | 36 ------------------- 1 file changed, 36 deletions(-) diff --git a/packages/e2e-tests/specs/editor/blocks/navigation.test.js b/packages/e2e-tests/specs/editor/blocks/navigation.test.js index 63af9e346d78c..ee75374afdf53 100644 --- a/packages/e2e-tests/specs/editor/blocks/navigation.test.js +++ b/packages/e2e-tests/specs/editor/blocks/navigation.test.js @@ -1317,42 +1317,6 @@ Expected mock function not to be called but it was called with: ["POST", "http:/ await switchUserToAdmin(); } ); - it( 'shows a warning if user does not have permission to edit or update navigation menus', async () => { - await createNewPost(); - await insertBlock( 'Navigation' ); - - const startEmptyButton = await page.waitForXPath( - START_EMPTY_XPATH - ); - - // This creates an empty Navigation post type entity. - await startEmptyButton.click(); - - // Publishing the Post ensures the Navigation entity is saved. - // The Post itself is irrelevant. - await publishPost(); - - // Switch to a Contributor role user - they should not have - // permission to update Navigation menus. - await loginUser( contributorUsername, contributorPassword ); - - await createNewPost(); - - // At this point the block will automatically pick the first Navigation Menu - // which will be the one created by the Admin User. - await insertBlock( 'Navigation' ); - - // Make sure the snackbar error shows up. - await page.waitForXPath( - `//*[contains(@class, 'components-snackbar__content')][ text()="You do not have permission to edit this Menu. Any changes made will not be saved." ]` - ); - - // Expect a console 403 for requests to: - // * /wp/v2/settings?_locale=user - // * /wp/v2/templates?context=edit&post_type=post&per_page=100&_locale=user - expect( console ).toHaveErrored(); - } ); - it( 'shows a warning if user does not have permission to create navigation menus', async () => { const noticeText = 'You do not have permission to create Navigation Menus.'; From 051807dc6f7319fa301930e28ad586b52f945da9 Mon Sep 17 00:00:00 2001 From: ntsekouras <ntsekouras@outlook.com> Date: Fri, 1 Jul 2022 11:45:43 +0300 Subject: [PATCH 13/13] in 5.9 we default to `0` for site-logo setting in REST --- packages/block-library/src/site-logo/edit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-library/src/site-logo/edit.js b/packages/block-library/src/site-logo/edit.js index a48e9e28546ff..c7a97219983f7 100644 --- a/packages/block-library/src/site-logo/edit.js +++ b/packages/block-library/src/site-logo/edit.js @@ -533,7 +533,7 @@ export default function LogoEdit( { { !! logoUrl && logoImage } { ! logoUrl && ! canUserEdit && ( <Placeholder className="site-logo_placeholder"> - { isLoading && ( + { !! isLoading && ( <span className="components-placeholder__preview"> <Spinner /> </span>