rebase on oct-10-2023

This commit is contained in:
Rachit Bhargava
2023-10-10 17:23:21 -04:00
parent d37566ffb6
commit d096058d7d
4789 changed files with 254611 additions and 307223 deletions

View File

@@ -18,6 +18,34 @@ class Api {
*/
private $inline_scripts = [];
/**
* Determines if caching is enabled for script data.
*
* @var boolean
*/
private $disable_cache = false;
/**
* Stores loaded script data for the current request
*
* @var array|null
*/
private $script_data = null;
/**
* Stores the hash for the script data, made up of the site url, plugin version and package path.
*
* @var string
*/
private $script_data_hash;
/**
* Stores the transient key used to cache the script data. This will change if the site is accessed via HTTPS or HTTP.
*
* @var string
*/
private $script_data_transient_key = 'woocommerce_blocks_asset_api_script_data';
/**
* Reference to the Package instance
*
@@ -31,7 +59,18 @@ class Api {
* @param Package $package An instance of Package.
*/
public function __construct( Package $package ) {
$this->package = $package;
$this->package = $package;
$this->disable_cache = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) || ! $this->package->feature()->is_production_environment();
// If the site is accessed via HTTPS, change the transient key. This is to prevent the script URLs being cached
// with the first scheme they are accessed on after cache expiry.
if ( is_ssl() ) {
$this->script_data_transient_key .= '_ssl';
}
if ( ! $this->disable_cache ) {
$this->script_data_hash = $this->get_script_data_hash();
}
add_action( 'shutdown', array( $this, 'update_script_data_cache' ), 20 );
}
/**
@@ -76,6 +115,64 @@ class Api {
return $path_to_metadata_from_plugin_root;
}
/**
* Generates a hash containing the site url, plugin version and package path.
*
* Moving the plugin, changing the version, or changing the site url will result in a new hash and the cache will be invalidated.
*
* @return string The generated hash.
*/
private function get_script_data_hash() {
return md5( get_option( 'siteurl', '' ) . $this->package->get_version() . $this->package->get_path() );
}
/**
* Initialize and load cached script data from the transient cache.
*
* @return array
*/
private function get_cached_script_data() {
if ( $this->disable_cache ) {
return [];
}
$transient_value = json_decode( (string) get_transient( $this->script_data_transient_key ), true );
if (
json_last_error() !== JSON_ERROR_NONE ||
empty( $transient_value ) ||
empty( $transient_value['script_data'] ) ||
empty( $transient_value['version'] ) ||
$transient_value['version'] !== $this->package->get_version() ||
empty( $transient_value['hash'] ) ||
$transient_value['hash'] !== $this->script_data_hash
) {
return [];
}
return (array) ( $transient_value['script_data'] ?? [] );
}
/**
* Store all cached script data in the transient cache.
*/
public function update_script_data_cache() {
if ( is_null( $this->script_data ) || $this->disable_cache ) {
return;
}
set_transient(
$this->script_data_transient_key,
wp_json_encode(
array(
'script_data' => $this->script_data,
'version' => $this->package->get_version(),
'hash' => $this->script_data_hash,
)
),
DAY_IN_SECONDS * 30
);
}
/**
* Get src, version and dependencies given a script relative src.
*
@@ -85,29 +182,37 @@ class Api {
* @return array src, version and dependencies of the script.
*/
public function get_script_data( $relative_src, $dependencies = [] ) {
$src = '';
$version = '1';
if ( $relative_src ) {
$src = $this->get_asset_url( $relative_src );
$asset_path = $this->package->get_path(
str_replace( '.js', '.asset.php', $relative_src )
if ( ! $relative_src ) {
return array(
'src' => '',
'version' => '1',
'dependencies' => $dependencies,
);
if ( file_exists( $asset_path ) ) {
$asset = require $asset_path;
$dependencies = isset( $asset['dependencies'] ) ? array_merge( $asset['dependencies'], $dependencies ) : $dependencies;
$version = ! empty( $asset['version'] ) ? $asset['version'] : $this->get_file_version( $relative_src );
} else {
$version = $this->get_file_version( $relative_src );
}
}
return array(
'src' => $src,
'version' => $version,
'dependencies' => $dependencies,
);
if ( is_null( $this->script_data ) ) {
$this->script_data = $this->get_cached_script_data();
}
if ( empty( $this->script_data[ $relative_src ] ) ) {
$asset_path = $this->package->get_path( str_replace( '.js', '.asset.php', $relative_src ) );
// The following require is safe because we are checking if the file exists and it is not a user input.
// nosemgrep audit.php.lang.security.file.inclusion-arg.
$asset = file_exists( $asset_path ) ? require $asset_path : [];
$this->script_data[ $relative_src ] = array(
'src' => $this->get_asset_url( $relative_src ),
'version' => ! empty( $asset['version'] ) ? $asset['version'] : $this->get_file_version( $relative_src ),
'dependencies' => ! empty( $asset['dependencies'] ) ? $asset['dependencies'] : [],
);
}
// Return asset details as well as the requested dependencies array.
return [
'src' => $this->script_data[ $relative_src ]['src'],
'version' => $this->script_data[ $relative_src ]['version'],
'dependencies' => array_merge( $this->script_data[ $relative_src ]['dependencies'], $dependencies ),
];
}
/**
@@ -171,17 +276,22 @@ class Api {
* @since 2.5.0
* @since 2.6.0 Change src to be relative source.
*
* @param string $handle Name of the stylesheet. Should be unique.
* @param string $relative_src Relative source of the stylesheet to the plugin path.
* @param array $deps Optional. An array of registered stylesheet handles this stylesheet depends on. Default empty array.
* @param string $media Optional. The media for which this stylesheet has been defined. Default 'all'. Accepts media types like
* 'all', 'print' and 'screen', or media queries like '(orientation: portrait)' and '(max-width: 640px)'.
* @param string $handle Name of the stylesheet. Should be unique.
* @param string $relative_src Relative source of the stylesheet to the plugin path.
* @param array $deps Optional. An array of registered stylesheet handles this stylesheet depends on. Default empty array.
* @param string $media Optional. The media for which this stylesheet has been defined. Default 'all'. Accepts media types like
* 'all', 'print' and 'screen', or media queries like '(orientation: portrait)' and '(max-width: 640px)'.
* @param boolean $rtl Optional. Whether or not to register RTL styles.
*/
public function register_style( $handle, $relative_src, $deps = [], $media = 'all' ) {
public function register_style( $handle, $relative_src, $deps = [], $media = 'all', $rtl = false ) {
$filename = str_replace( plugins_url( '/', __DIR__ ), '', $relative_src );
$src = $this->get_asset_url( $relative_src );
$ver = $this->get_file_version( $filename );
wp_register_style( $handle, $src, $deps, $ver, $media );
if ( $rtl ) {
wp_style_add_data( $handle, 'rtl', 'replace' );
}
}
/**

View File

@@ -2,7 +2,7 @@
namespace Automattic\WooCommerce\Blocks\Assets;
use Automattic\WooCommerce\Blocks\Package;
use Automattic\WooCommerce\Blocks\Domain\Services\Hydration;
use Exception;
use InvalidArgumentException;
@@ -66,23 +66,8 @@ class AssetDataRegistry {
* Hook into WP asset registration for enqueueing asset data.
*/
protected function init() {
if ( $this->is_site_editor() ) {
add_action( 'enqueue_block_editor_assets', array( $this, 'register_data_script' ) );
} else {
add_action( 'init', array( $this, 'register_data_script' ) );
}
add_action( 'wp_print_footer_scripts', array( $this, 'enqueue_asset_data' ), 2 );
add_action( 'admin_print_footer_scripts', array( $this, 'enqueue_asset_data' ), 2 );
}
/**
* Checks if the current URL is the Site Editor.
*
* @return boolean
*/
protected function is_site_editor() {
$url_path = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
return str_contains( $url_path, 'site-editor.php' );
add_action( 'init', array( $this, 'register_data_script' ) );
add_action( is_admin() ? 'admin_print_footer_scripts' : 'wp_print_footer_scripts', array( $this, 'enqueue_asset_data' ), 1 );
}
/**
@@ -308,9 +293,9 @@ class AssetDataRegistry {
* You can only register data that is not already in the registry identified by the given key. If there is a
* duplicate found, unless $ignore_duplicates is true, an exception will be thrown.
*
* @param string $key The key used to reference the data being registered.
* @param mixed $data If not a function, registered to the registry as is. If a function, then the
* callback is invoked right before output to the screen.
* @param string $key The key used to reference the data being registered. This should use camelCase.
* @param mixed $data If not a function, registered to the registry as is. If a function, then the
* callback is invoked right before output to the screen.
* @param boolean $check_key_exists If set to true, duplicate data will be ignored if the key exists.
* If false, duplicate data will cause an exception.
*
@@ -332,16 +317,40 @@ class AssetDataRegistry {
}
/**
* Hydrate from API.
* Hydrate from the API.
*
* @param string $path REST API path to preload.
*/
public function hydrate_api_request( $path ) {
if ( ! isset( $this->preloaded_api_requests[ $path ] ) ) {
$this->preloaded_api_requests = rest_preload_api_request( $this->preloaded_api_requests, $path );
$this->preloaded_api_requests[ $path ] = Package::container()->get( Hydration::class )->get_rest_api_response_data( $path );
}
}
/**
* Hydrate some data from the API.
*
* @param string $key The key used to reference the data being registered.
* @param string $path REST API path to preload.
* @param boolean $check_key_exists If set to true, duplicate data will be ignored if the key exists.
* If false, duplicate data will cause an exception.
*
* @throws InvalidArgumentException Only throws when site is in debug mode. Always logs the error.
*/
public function hydrate_data_from_api_request( $key, $path, $check_key_exists = false ) {
$this->add(
$key,
function() use ( $path ) {
if ( isset( $this->preloaded_api_requests[ $path ], $this->preloaded_api_requests[ $path ]['body'] ) ) {
return $this->preloaded_api_requests[ $path ]['body'];
}
$response = Package::container()->get( Hydration::class )->get_rest_api_response_data( $path );
return $response['body'] ?? '';
},
$check_key_exists
);
}
/**
* Adds a page permalink to the data registry.
*

View File

@@ -1,9 +1,7 @@
<?php
namespace Automattic\WooCommerce\Blocks;
use Automattic\WooCommerce\Blocks\Package;
use Automattic\WooCommerce\Blocks\Assets\Api as AssetApi;
use Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry as AssetDataRegistry;
/**
* AssetsController class.
@@ -35,6 +33,7 @@ final class AssetsController {
*/
protected function init() {
add_action( 'init', array( $this, 'register_assets' ) );
add_action( 'enqueue_block_editor_assets', array( $this, 'register_and_enqueue_site_editor_assets' ) );
add_filter( 'wp_resource_hints', array( $this, 'add_resource_hints' ), 10, 2 );
add_action( 'body_class', array( $this, 'add_theme_body_class' ), 1 );
add_action( 'admin_body_class', array( $this, 'add_theme_body_class' ), 1 );
@@ -47,16 +46,23 @@ final class AssetsController {
* Register block scripts & styles.
*/
public function register_assets() {
$this->register_style( 'wc-blocks-vendors-style', plugins_url( $this->api->get_block_asset_build_path( 'wc-blocks-vendors-style', 'css' ), __DIR__ ) );
$this->register_style( 'wc-blocks-editor-style', plugins_url( $this->api->get_block_asset_build_path( 'wc-blocks-editor-style', 'css' ), __DIR__ ), [ 'wp-edit-blocks' ], 'all', true );
$this->register_style( 'wc-blocks-style', plugins_url( $this->api->get_block_asset_build_path( 'wc-blocks-style', 'css' ), __DIR__ ), [ 'wc-blocks-vendors-style' ], 'all', true );
if ( wc_current_theme_is_fse_theme() ) {
$this->register_style( 'wc-blocks-packages-style', plugins_url( $this->api->get_block_asset_build_path( 'packages-style', 'css' ), __DIR__ ), [], 'all', true );
$this->register_style( 'wc-blocks-style', plugins_url( $this->api->get_block_asset_build_path( 'wc-blocks', 'css' ), __DIR__ ), [], 'all', true );
} else {
$this->register_style( 'wc-blocks-vendors-style', plugins_url( $this->api->get_block_asset_build_path( 'wc-blocks-vendors-style', 'css' ), __DIR__ ) );
$this->register_style( 'wc-all-blocks-style', plugins_url( $this->api->get_block_asset_build_path( 'wc-all-blocks-style', 'css' ), __DIR__ ), [ 'wc-blocks-vendors-style' ], 'all', true );
}
$this->api->register_script( 'wc-blocks-middleware', 'build/wc-blocks-middleware.js', [], false );
$this->api->register_script( 'wc-blocks-data-store', 'build/wc-blocks-data.js', [ 'wc-blocks-middleware' ] );
$this->api->register_script( 'wc-blocks-vendors', $this->api->get_block_asset_build_path( 'wc-blocks-vendors' ), [], false );
$this->api->register_script( 'wc-blocks-registry', 'build/wc-blocks-registry.js', [], false );
$this->api->register_script( 'wc-blocks', $this->api->get_block_asset_build_path( 'wc-blocks' ), [ 'wc-blocks-vendors' ], false );
$this->api->register_script( 'wc-blocks-shared-context', 'build/wc-blocks-shared-context.js', [] );
$this->api->register_script( 'wc-blocks-shared-context', 'build/wc-blocks-shared-context.js' );
$this->api->register_script( 'wc-blocks-shared-hocs', 'build/wc-blocks-shared-hocs.js', [], false );
// The price package is shared externally so has no blocks prefix.
@@ -76,6 +82,20 @@ final class AssetsController {
);
}
/**
* Register and enqueue assets for exclusive usage within the Site Editor.
*/
public function register_and_enqueue_site_editor_assets() {
$this->api->register_script( 'wc-blocks-classic-template-revert-button', 'build/wc-blocks-classic-template-revert-button.js' );
$this->api->register_style( 'wc-blocks-classic-template-revert-button-style', 'build/wc-blocks-classic-template-revert-button-style.css' );
$current_screen = get_current_screen();
if ( $current_screen instanceof \WP_Screen && 'site-editor' === $current_screen->base ) {
wp_enqueue_script( 'wc-blocks-classic-template-revert-button' );
wp_enqueue_style( 'wc-blocks-classic-template-revert-button-style' );
}
}
/**
* Defines resource hints to help speed up the loading of some critical blocks.
*
@@ -93,7 +113,7 @@ final class AssetsController {
// We only need to prefetch when the cart has contents.
$cart = wc()->cart;
if ( ! $cart || ! $cart instanceof \WC_Cart || 0 === $cart->get_cart_contents_count() ) {
if ( ! $cart instanceof \WC_Cart || 0 === $cart->get_cart_contents_count() ) {
return $urls;
}
@@ -182,7 +202,7 @@ final class AssetsController {
$this->api->get_block_asset_build_path( $filename )
);
$resources = array_merge(
[ add_query_arg( 'ver', $script_data['version'], $script_data['src'] ) ],
[ esc_url( add_query_arg( 'ver', $script_data['version'], $script_data['src'] ) ) ],
$this->get_script_dependency_src_array( $script_data['dependencies'] )
);
return array_map(
@@ -208,7 +228,7 @@ final class AssetsController {
$dependencies,
function( $src, $handle ) use ( $wp_scripts ) {
if ( isset( $wp_scripts->registered[ $handle ] ) ) {
$src[] = add_query_arg( 'ver', $wp_scripts->registered[ $handle ]->ver, $this->get_absolute_url( $wp_scripts->registered[ $handle ]->src ) );
$src[] = esc_url( add_query_arg( 'ver', $wp_scripts->registered[ $handle ]->ver, $this->get_absolute_url( $wp_scripts->registered[ $handle ]->src ) ) );
$src = array_merge( $src, $this->get_script_dependency_src_array( $wp_scripts->registered[ $handle ]->deps ) );
}
return $src;

View File

@@ -1,10 +1,18 @@
<?php
namespace Automattic\WooCommerce\Blocks;
use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Blocks\Domain\Package;
use Automattic\WooCommerce\Blocks\Templates\CartTemplate;
use Automattic\WooCommerce\Blocks\Templates\CheckoutTemplate;
use Automattic\WooCommerce\Blocks\Templates\ProductAttributeTemplate;
use Automattic\WooCommerce\Blocks\Templates\ProductSearchResultsTemplate;
use Automattic\WooCommerce\Blocks\Templates\SingleProductTemplateCompatibility;
use Automattic\WooCommerce\Blocks\Utils\BlockTemplateUtils;
use Automattic\WooCommerce\Blocks\Templates\OrderConfirmationTemplate;
use Automattic\WooCommerce\Blocks\Utils\SettingsUtils;
use Automattic\WooCommerce\Blocks\Utils\BlockTemplateMigrationUtils;
use \WP_Post;
/**
* BlockTypesController class.
@@ -62,6 +70,7 @@ class BlockTemplatesController {
* Initialization method.
*/
protected function init() {
add_filter( 'default_wp_template_part_areas', array( $this, 'register_mini_cart_template_part_area' ), 10, 1 );
add_action( 'template_redirect', array( $this, 'render_block_template' ) );
add_filter( 'pre_get_block_template', array( $this, 'get_block_template_fallback' ), 10, 3 );
add_filter( 'pre_get_block_file_template', array( $this, 'get_block_file_template' ), 10, 3 );
@@ -69,12 +78,96 @@ class BlockTemplatesController {
add_filter( 'current_theme_supports-block-templates', array( $this, 'remove_block_template_support_for_shop_page' ) );
add_filter( 'taxonomy_template_hierarchy', array( $this, 'add_archive_product_to_eligible_for_fallback_templates' ), 10, 1 );
add_filter( 'post_type_archive_title', array( $this, 'update_product_archive_title' ), 10, 2 );
add_action( 'after_switch_theme', array( $this, 'check_should_use_blockified_product_grid_templates' ), 10, 2 );
if ( $this->package->is_experimental_build() ) {
add_action( 'after_switch_theme', array( $this, 'check_should_use_blockified_product_grid_templates' ), 10, 2 );
if ( wc_current_theme_is_fse_theme() ) {
add_action( 'init', array( $this, 'maybe_migrate_content' ) );
add_filter( 'woocommerce_settings_pages', array( $this, 'template_permalink_settings' ) );
add_filter( 'pre_update_option', array( $this, 'update_template_permalink' ), 10, 2 );
add_action( 'woocommerce_admin_field_permalink', array( SettingsUtils::class, 'permalink_input_field' ) );
// By default, the Template Part Block only supports template parts that are in the current theme directory.
// This render_callback wrapper allows us to add support for plugin-housed template parts.
add_filter(
'block_type_metadata_settings',
function( $settings, $metadata ) {
if (
isset( $metadata['name'], $settings['render_callback'] ) &&
'core/template-part' === $metadata['name'] &&
in_array( $settings['render_callback'], [ 'render_block_core_template_part', 'gutenberg_render_block_core_template_part' ], true )
) {
$settings['render_callback'] = [ $this, 'render_woocommerce_template_part' ];
}
return $settings;
},
10,
2
);
// Prevents shortcodes in templates having their HTML content broken by wpautop.
// @see https://core.trac.wordpress.org/ticket/58366 for more info.
add_filter(
'block_type_metadata_settings',
function( $settings, $metadata ) {
if (
isset( $metadata['name'], $settings['render_callback'] ) &&
'core/shortcode' === $metadata['name']
) {
$settings['original_render_callback'] = $settings['render_callback'];
$settings['render_callback'] = function( $attributes, $content ) use ( $settings ) {
// The shortcode has already been rendered, so look for the cart/checkout HTML.
if ( strstr( $content, 'woocommerce-cart-form' ) || strstr( $content, 'woocommerce-checkout-form' ) ) {
// Return early before wpautop runs again.
return $content;
}
$render_callback = $settings['original_render_callback'];
return $render_callback( $attributes, $content );
};
}
return $settings;
},
10,
2
);
}
}
/**
* Add Mini-Cart to the default template part areas.
*
* @param array $default_area_definitions An array of supported area objects.
* @return array The supported template part areas including the Mini-Cart one.
*/
public function register_mini_cart_template_part_area( $default_area_definitions ) {
$mini_cart_template_part_area = [
'area' => 'mini-cart',
'label' => __( 'Mini-Cart', 'woocommerce' ),
'description' => __( 'The Mini-Cart template allows shoppers to see their cart items and provides access to the Cart and Checkout pages.', 'woocommerce' ),
'icon' => 'mini-cart',
'area_tag' => 'mini-cart',
];
return array_merge( $default_area_definitions, [ $mini_cart_template_part_area ] );
}
/**
* Renders the `core/template-part` block on the server.
*
* @param array $attributes The block attributes.
* @return string The render.
*/
public function render_woocommerce_template_part( $attributes ) {
if ( 'woocommerce/woocommerce' === $attributes['theme'] ) {
$template_part = BlockTemplateUtils::get_block_template( $attributes['theme'] . '//' . $attributes['slug'], 'wp_template_part' );
if ( $template_part && ! empty( $template_part->content ) ) {
return do_blocks( $template_part->content );
}
}
return function_exists( '\gutenberg_render_block_core_template_part' ) ? \gutenberg_render_block_core_template_part( $attributes ) : \render_block_core_template_part( $attributes );
}
/**
* This function is used on the `pre_get_block_template` hook to return the fallback template from the db in case
* the template is eligible for it.
@@ -250,7 +343,7 @@ class BlockTemplatesController {
* @return array
*/
public function add_block_templates( $query_result, $query, $template_type ) {
if ( ! BlockTemplateUtils::supports_block_templates() ) {
if ( ! BlockTemplateUtils::supports_block_templates( $template_type ) ) {
return $query_result;
}
@@ -315,6 +408,33 @@ class BlockTemplatesController {
*/
$query_result = array_map(
function( $template ) {
if ( str_contains( $template->slug, 'single-product' ) ) {
// We don't want to add the compatibility layer on the Editor Side.
// The second condition is necessary to not apply the compatibility layer on the REST API. Gutenberg uses the REST API to clone the template.
// More details: https://github.com/woocommerce/woocommerce-blocks/issues/9662.
if ( ( ! is_admin() && ! ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) && ! BlockTemplateUtils::template_has_legacy_template_block( $template ) ) {
// Add the product class to the body. We should move this to a more appropriate place.
add_filter(
'body_class',
function( $classes ) {
return array_merge( $classes, wc_get_product_class() );
}
);
global $product;
if ( ! $product instanceof \WC_Product ) {
$product_id = get_the_ID();
if ( $product_id ) {
wc_setup_product_data( $product_id );
}
}
$new_content = SingleProductTemplateCompatibility::add_compatibility_layer( $template->content );
$template->content = $new_content;
}
}
if ( 'theme' === $template->origin && BlockTemplateUtils::template_has_title( $template ) ) {
return $template;
}
@@ -324,16 +444,6 @@ class BlockTemplatesController {
if ( ! $template->description ) {
$template->description = BlockTemplateUtils::get_block_template_description( $template->slug );
}
if ( str_contains( $template->slug, 'single-product' ) ) {
if ( ! is_admin() && ! BlockTemplateUtils::template_has_legacy_template_block( $template ) ) {
$new_content = SingleProductTemplateCompatibility::add_compatibility_layer( $template->content );
$template->content = $new_content;
}
return $template;
}
return $template;
},
$query_result
@@ -351,32 +461,8 @@ class BlockTemplatesController {
* @return int[]|\WP_Post[] An array of found templates.
*/
public function get_block_templates_from_db( $slugs = array(), $template_type = 'wp_template' ) {
$check_query_args = array(
'post_type' => $template_type,
'posts_per_page' => -1,
'no_found_rows' => true,
'tax_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
array(
'taxonomy' => 'wp_theme',
'field' => 'name',
'terms' => array( BlockTemplateUtils::DEPRECATED_PLUGIN_SLUG, BlockTemplateUtils::PLUGIN_SLUG, get_stylesheet() ),
),
),
);
if ( is_array( $slugs ) && count( $slugs ) > 0 ) {
$check_query_args['post_name__in'] = $slugs;
}
$check_query = new \WP_Query( $check_query_args );
$saved_woo_templates = $check_query->posts;
return array_map(
function( $saved_woo_template ) {
return BlockTemplateUtils::build_template_result_from_post( $saved_woo_template );
},
$saved_woo_templates
);
wc_deprecated_function( 'BlockTemplatesController::get_block_templates_from_db()', '7.8', '\Automattic\WooCommerce\Blocks\Utils\BlockTemplateUtils::get_block_templates_from_db()' );
return BlockTemplateUtils::get_block_templates_from_db( $slugs, $template_type );
}
/**
@@ -396,11 +482,7 @@ class BlockTemplatesController {
foreach ( $template_files as $template_file ) {
// Skip the template if it's blockified, and we should only use classic ones.
// Until the blockified Product Grid Block is implemented, we need to always skip the blockified templates.
// phpcs:ignore Squiz.PHP.CommentedOutCode
if ( // $this->package->is_experimental_build() &&
// ! BlockTemplateUtils::should_use_blockified_product_grid_templates() &&
strpos( $template_file, 'blockified' ) !== false ) {
if ( ! BlockTemplateUtils::should_use_blockified_product_grid_templates() && strpos( $template_file, 'blockified' ) !== false ) {
continue;
}
@@ -470,7 +552,7 @@ class BlockTemplatesController {
* @return array WP_Block_Template[] An array of block template objects.
*/
public function get_block_templates( $slugs = array(), $template_type = 'wp_template' ) {
$templates_from_db = $this->get_block_templates_from_db( $slugs, $template_type );
$templates_from_db = BlockTemplateUtils::get_block_templates_from_db( $slugs, $template_type );
$templates_from_woo = $this->get_block_templates_from_woocommerce( $slugs, $templates_from_db, $template_type );
$templates = array_merge( $templates_from_db, $templates_from_woo );
@@ -489,10 +571,9 @@ class BlockTemplatesController {
return $this->template_parts_directory;
}
// When the blockified Product Grid Block will be implemented, we need to use the blockified templates.
// if ( $this->package->is_experimental_build() && BlockTemplateUtils::should_use_blockified_product_grid_templates() ) {
// return $this->templates_directory . '/blockified';
// }.
if ( BlockTemplateUtils::should_use_blockified_product_grid_templates() ) {
return $this->templates_directory . '/blockified';
}
return $this->templates_directory;
}
@@ -539,7 +620,13 @@ class BlockTemplatesController {
if (
is_singular( 'product' ) && $this->block_template_is_available( 'single-product' )
) {
$templates = get_block_templates( array( 'slug__in' => array( 'single-product' ) ) );
global $post;
$valid_slugs = [ 'single-product' ];
if ( 'product' === $post->post_type && $post->post_name ) {
$valid_slugs[] = 'single-product-' . $post->post_name;
}
$templates = get_block_templates( array( 'slug__in' => $valid_slugs ) );
if ( isset( $templates[0] ) && BlockTemplateUtils::template_has_legacy_template_block( $templates[0] ) ) {
add_filter( 'woocommerce_disable_compatibility_layer', '__return_true' );
@@ -572,6 +659,16 @@ class BlockTemplatesController {
if ( ! BlockTemplateUtils::theme_has_template( 'taxonomy-product_tag' ) ) {
add_filter( 'woocommerce_has_block_template', '__return_true', 10, 0 );
}
} elseif ( is_post_type_archive( 'product' ) && is_search() ) {
$templates = get_block_templates( array( 'slug__in' => array( ProductSearchResultsTemplate::SLUG ) ) );
if ( isset( $templates[0] ) && BlockTemplateUtils::template_has_legacy_template_block( $templates[0] ) ) {
add_filter( 'woocommerce_disable_compatibility_layer', '__return_true' );
}
if ( ! BlockTemplateUtils::theme_has_template( ProductSearchResultsTemplate::SLUG ) ) {
add_filter( 'woocommerce_has_block_template', '__return_true', 10, 0 );
}
} elseif (
( is_post_type_archive( 'product' ) || is_page( wc_get_page_id( 'shop' ) ) ) && $this->block_template_is_available( 'archive-product' )
) {
@@ -584,6 +681,22 @@ class BlockTemplatesController {
if ( ! BlockTemplateUtils::theme_has_template( 'archive-product' ) ) {
add_filter( 'woocommerce_has_block_template', '__return_true', 10, 0 );
}
} elseif (
is_cart() &&
! BlockTemplateUtils::theme_has_template( CartTemplate::get_slug() ) && $this->block_template_is_available( CartTemplate::get_slug() )
) {
add_filter( 'woocommerce_has_block_template', '__return_true', 10, 0 );
} elseif (
is_checkout() &&
! BlockTemplateUtils::theme_has_template( CheckoutTemplate::get_slug() ) && $this->block_template_is_available( CheckoutTemplate::get_slug() )
) {
add_filter( 'woocommerce_has_block_template', '__return_true', 10, 0 );
} elseif (
is_wc_endpoint_url( 'order-received' )
&& ! BlockTemplateUtils::theme_has_template( OrderConfirmationTemplate::get_slug() )
&& $this->block_template_is_available( OrderConfirmationTemplate::get_slug() )
) {
add_filter( 'woocommerce_has_block_template', '__return_true', 10, 0 );
} else {
$queried_object = get_queried_object();
if ( is_null( $queried_object ) ) {
@@ -650,4 +763,120 @@ class BlockTemplatesController {
return $post_type_name;
}
/**
* Migrates page content to templates if needed.
*/
public function maybe_migrate_content() {
// Migration should occur on a normal request to ensure every requirement is met.
// We are postponing it if WP is in maintenance mode, installing, WC installing or if the request is part of a WP-CLI command.
if ( wp_is_maintenance_mode() || ! get_option( 'woocommerce_db_version', false ) || Constants::is_defined( 'WP_SETUP_CONFIG' ) || Constants::is_defined( 'WC_INSTALLING' ) || Constants::is_defined( 'WP_CLI' ) ) {
return;
}
if ( ! BlockTemplateMigrationUtils::has_migrated_page( 'cart' ) ) {
BlockTemplateMigrationUtils::migrate_page( 'cart', CartTemplate::get_placeholder_page() );
}
if ( ! BlockTemplateMigrationUtils::has_migrated_page( 'checkout' ) ) {
BlockTemplateMigrationUtils::migrate_page( 'checkout', CheckoutTemplate::get_placeholder_page() );
}
}
/**
* Replaces page settings in WooCommerce with text based permalinks which point to a template.
*
* @param array $settings Settings pages.
* @return array
*/
public function template_permalink_settings( $settings ) {
foreach ( $settings as $key => $setting ) {
if ( 'woocommerce_checkout_page_id' === $setting['id'] ) {
$checkout_page = CheckoutTemplate::get_placeholder_page();
$settings[ $key ] = [
'title' => __( 'Checkout page', 'woocommerce' ),
'desc' => sprintf(
// translators: %1$s: opening anchor tag, %2$s: closing anchor tag.
__( 'The checkout template can be %1$s edited here%2$s.', 'woocommerce' ),
'<a href="' . esc_url( admin_url( 'site-editor.php?postType=wp_template&postId=woocommerce%2Fwoocommerce%2F%2F' . CheckoutTemplate::get_slug() ) ) . '" target="_blank">',
'</a>'
),
'desc_tip' => __( 'This is the URL to the checkout page.', 'woocommerce' ),
'id' => 'woocommerce_checkout_page_endpoint',
'type' => 'permalink',
'default' => $checkout_page ? $checkout_page->post_name : CheckoutTemplate::get_slug(),
'autoload' => false,
];
}
if ( 'woocommerce_cart_page_id' === $setting['id'] ) {
$cart_page = CartTemplate::get_placeholder_page();
$settings[ $key ] = [
'title' => __( 'Cart page', 'woocommerce' ),
'desc' => sprintf(
// translators: %1$s: opening anchor tag, %2$s: closing anchor tag.
__( 'The cart template can be %1$s edited here%2$s.', 'woocommerce' ),
'<a href="' . esc_url( admin_url( 'site-editor.php?postType=wp_template&postId=woocommerce%2Fwoocommerce%2F%2F' . CartTemplate::get_slug() ) ) . '" target="_blank">',
'</a>'
),
'desc_tip' => __( 'This is the URL to the cart page.', 'woocommerce' ),
'id' => 'woocommerce_cart_page_endpoint',
'type' => 'permalink',
'default' => $cart_page ? $cart_page->post_name : CartTemplate::get_slug(),
'autoload' => false,
];
}
}
return $settings;
}
/**
* Syncs entered permalink with the pages and returns the correct value.
*
* @param string $value Value of the option.
* @param string $option Name of the option.
* @return string
*/
public function update_template_permalink( $value, $option ) {
if ( 'woocommerce_checkout_page_endpoint' === $option ) {
return $this->sync_endpoint_with_page( CheckoutTemplate::get_placeholder_page(), 'checkout', $value );
}
if ( 'woocommerce_cart_page_endpoint' === $option ) {
return $this->sync_endpoint_with_page( CartTemplate::get_placeholder_page(), 'cart', $value );
}
return $value;
}
/**
* Syncs the provided permalink with the actual WP page.
*
* @param WP_Post|null $page The page object, or null if it does not exist.
* @param string $page_slug The identifier for the page e.g. cart, checkout.
* @param string $permalink The new permalink to use.
* @return string THe actual permalink assigned to the page. May differ from $permalink if it was already taken.
*/
protected function sync_endpoint_with_page( $page, $page_slug, $permalink ) {
if ( ! $page ) {
$updated_page_id = wc_create_page(
esc_sql( $permalink ),
'woocommerce_' . $page_slug . '_page_id',
$page_slug,
'',
'',
'publish'
);
} else {
$updated_page_id = wp_update_post(
[
'ID' => $page->ID,
'post_name' => esc_sql( $permalink ),
]
);
}
// Get post again in case slug was updated with a suffix.
if ( $updated_page_id && ! is_wp_error( $updated_page_id ) ) {
return get_post( $updated_page_id )->post_name;
}
return $permalink;
}
}

View File

@@ -300,11 +300,15 @@ abstract class AbstractBlock {
/**
* Get the frontend style handle for this block type.
*
* @see $this->register_block_type()
* @return string|null
* @return string[]|null
*/
protected function get_block_type_style() {
return 'wc-blocks-style';
if ( wc_current_theme_is_fse_theme() ) {
$this->asset_api->register_style( 'wc-blocks-style-' . $this->block_name, $this->asset_api->get_block_asset_build_path( $this->block_name, 'css' ), [], 'all', true );
return [ 'wc-blocks-style', 'wc-blocks-style-' . $this->block_name ];
}
return [ 'wc-all-blocks-style' ];
}
/**

View File

@@ -54,4 +54,12 @@ abstract class AbstractInnerBlock extends AbstractBlock {
return parent::get_block_type_script( $key );
}
/**
* Get the frontend style handle for this block type.
*
* @return null
*/
protected function get_block_type_style() {
return null;
}
}

View File

@@ -513,10 +513,10 @@ abstract class AbstractProductGrid extends AbstractDynamicBlock {
'woocommerce_blocks_product_grid_item_html',
"<li class=\"wc-block-grid__product\">
<a href=\"{$data->permalink}\" class=\"wc-block-grid__product-link\">
{$data->badge}
{$data->image}
{$data->title}
</a>
{$data->badge}
{$data->price}
{$data->rating}
{$data->button}
@@ -678,12 +678,21 @@ abstract class AbstractProductGrid extends AbstractDynamicBlock {
*/
protected function enqueue_data( array $attributes = [] ) {
parent::enqueue_data( $attributes );
$this->asset_data_registry->add( 'min_columns', wc_get_theme_support( 'product_blocks::min_columns', 1 ), true );
$this->asset_data_registry->add( 'max_columns', wc_get_theme_support( 'product_blocks::max_columns', 6 ), true );
$this->asset_data_registry->add( 'default_columns', wc_get_theme_support( 'product_blocks::default_columns', 3 ), true );
$this->asset_data_registry->add( 'min_rows', wc_get_theme_support( 'product_blocks::min_rows', 1 ), true );
$this->asset_data_registry->add( 'max_rows', wc_get_theme_support( 'product_blocks::max_rows', 6 ), true );
$this->asset_data_registry->add( 'default_rows', wc_get_theme_support( 'product_blocks::default_rows', 3 ), true );
$this->asset_data_registry->add( 'stock_status_options', wc_get_product_stock_status_options(), true );
$this->asset_data_registry->add( 'minColumns', wc_get_theme_support( 'product_blocks::min_columns', 1 ), true );
$this->asset_data_registry->add( 'maxColumns', wc_get_theme_support( 'product_blocks::max_columns', 6 ), true );
$this->asset_data_registry->add( 'defaultColumns', wc_get_theme_support( 'product_blocks::default_columns', 3 ), true );
$this->asset_data_registry->add( 'minRows', wc_get_theme_support( 'product_blocks::min_rows', 1 ), true );
$this->asset_data_registry->add( 'maxRows', wc_get_theme_support( 'product_blocks::max_rows', 6 ), true );
$this->asset_data_registry->add( 'defaultRows', wc_get_theme_support( 'product_blocks::default_rows', 3 ), true );
}
/**
* Get the frontend style handle for this block type.
*
* @return string[]
*/
protected function get_block_type_style() {
// Currently these blocks rely on the styles from the All Products block.
return [ 'wc-blocks-style', 'wc-blocks-style-all-products' ];
}
}

View File

@@ -15,6 +15,38 @@ class AddToCartForm extends AbstractBlock {
*/
protected $block_name = 'add-to-cart-form';
/**
* Initializes the AddToCartForm block and hooks into the `wc_add_to_cart_message_html` filter
* to prevent displaying the Cart Notice when the block is inside the Single Product block
* and the Add to Cart button is clicked.
*
* It also hooks into the `woocommerce_add_to_cart_redirect` filter to prevent redirecting
* to another page when the block is inside the Single Product block and the Add to Cart button
* is clicked.
*
* @return void
*/
protected function initialize() {
parent::initialize();
add_filter( 'wc_add_to_cart_message_html', array( $this, 'add_to_cart_message_html_filter' ), 10, 2 );
add_filter( 'woocommerce_add_to_cart_redirect', array( $this, 'add_to_cart_redirect_filter' ), 10, 1 );
}
/**
* Get the block's attributes.
*
* @param array $attributes Block attributes. Default empty array.
* @return array Block attributes merged with defaults.
*/
private function parse_attributes( $attributes ) {
// These should match what's set in JS `registerBlockType`.
$defaults = array(
'isDescendentOfSingleProductBlock' => false,
);
return wp_parse_args( $attributes, $defaults );
}
/**
* Render the block.
*
@@ -33,11 +65,12 @@ class AddToCartForm extends AbstractBlock {
return '';
}
$previous_product = $product;
$product = wc_get_product( $post_id );
if ( ! $product instanceof \WC_Product ) {
$product = wc_get_product( $post_id );
if ( ! $product instanceof \WC_Product ) {
return '';
}
$product = $previous_product;
return '';
}
ob_start();
@@ -52,19 +85,87 @@ class AddToCartForm extends AbstractBlock {
$product = ob_get_clean();
if ( ! $product ) {
$product = $previous_product;
return '';
}
$parsed_attributes = $this->parse_attributes( $attributes );
$is_descendent_of_single_product_block = $parsed_attributes['isDescendentOfSingleProductBlock'];
$product = $this->add_is_descendent_of_single_product_block_hidden_input_to_product_form( $product, $is_descendent_of_single_product_block );
$classname = $attributes['className'] ?? '';
$classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes );
$product_classname = $is_descendent_of_single_product_block ? 'product' : '';
return sprintf(
'<div class="wp-block-add-to-cart-form %1$s %2$s" style="%3$s">%4$s</div>',
$form = sprintf(
'<div class="wp-block-add-to-cart-form %1$s %2$s %3$s" style="%4$s">%5$s</div>',
esc_attr( $classes_and_styles['classes'] ),
esc_attr( $classname ),
esc_attr( $product_classname ),
esc_attr( $classes_and_styles['styles'] ),
$product
);
$product = $previous_product;
return $form;
}
/**
* Add a hidden input to the Add to Cart form to indicate that it is a descendent of a Single Product block.
*
* @param string $product The Add to Cart Form HTML.
* @param string $is_descendent_of_single_product_block Indicates if block is descendent of Single Product block.
*
* @return string The Add to Cart Form HTML with the hidden input.
*/
protected function add_is_descendent_of_single_product_block_hidden_input_to_product_form( $product, $is_descendent_of_single_product_block ) {
$hidden_is_descendent_of_single_product_block_input = sprintf(
'<input type="hidden" name="is-descendent-of-single-product-block" value="%1$s">',
$is_descendent_of_single_product_block ? 'true' : 'false'
);
$regex_pattern = '/<button\s+type="submit"[^>]*>.*?<\/button>/i';
preg_match( $regex_pattern, $product, $input_matches );
if ( ! empty( $input_matches ) ) {
$product = preg_replace( $regex_pattern, $hidden_is_descendent_of_single_product_block_input . $input_matches[0], $product );
}
return $product;
}
/**
* Filter the add to cart message to prevent the Notice from being displayed when the Add to Cart form is a descendent of a Single Product block
* and the Add to Cart button is clicked.
*
* @param string $message Message to be displayed when product is added to the cart.
*/
public function add_to_cart_message_html_filter( $message ) {
// phpcs:ignore
if ( isset( $_POST['is-descendent-of-single-product-block'] ) && 'true' === $_POST['is-descendent-of-single-product-block'] ) {
return false;
}
return $message;
}
/**
* Hooks into the `woocommerce_add_to_cart_redirect` filter to prevent redirecting
* to another page when the block is inside the Single Product block and the Add to Cart button
* is clicked.
*
* @param string $url The URL to redirect to after the product is added to the cart.
* @return string The filtered redirect URL.
*/
public function add_to_cart_redirect_filter( $url ) {
// phpcs:ignore
if ( isset( $_POST['is-descendent-of-single-product-block'] ) && 'true' == $_POST['is-descendent-of-single-product-block'] ) {
return wp_validate_redirect( wp_get_referer(), $url );
}
return $url;
}
/**
@@ -76,6 +177,15 @@ class AddToCartForm extends AbstractBlock {
return null;
}
/**
* Get the frontend style handle for this block type.
*
* @return null
*/
protected function get_block_type_style() {
return array_merge( parent::get_block_type_style(), [ 'wc-blocks-packages-style' ] );
}
/**
* It isn't necessary register block assets because it is a server side block.
*/

View File

@@ -22,29 +22,20 @@ class AllProducts extends AbstractBlock {
protected function enqueue_data( array $attributes = [] ) {
parent::enqueue_data( $attributes );
// Set this so filter blocks being used as widgets know when to render.
$this->asset_data_registry->add( 'has_filterable_products', true, true );
$this->asset_data_registry->add( 'hasFilterableProducts', true, true );
$this->asset_data_registry->add( 'minColumns', wc_get_theme_support( 'product_blocks::min_columns', 1 ), true );
$this->asset_data_registry->add( 'maxColumns', wc_get_theme_support( 'product_blocks::max_columns', 6 ), true );
$this->asset_data_registry->add( 'defaultColumns', wc_get_theme_support( 'product_blocks::default_columns', 3 ), true );
$this->asset_data_registry->add( 'minRows', wc_get_theme_support( 'product_blocks::min_rows', 1 ), true );
$this->asset_data_registry->add( 'maxRows', wc_get_theme_support( 'product_blocks::max_rows', 6 ), true );
$this->asset_data_registry->add( 'defaultRows', wc_get_theme_support( 'product_blocks::default_rows', 3 ), true );
$this->asset_data_registry->add( 'min_columns', wc_get_theme_support( 'product_blocks::min_columns', 1 ), true );
$this->asset_data_registry->add( 'max_columns', wc_get_theme_support( 'product_blocks::max_columns', 6 ), true );
$this->asset_data_registry->add( 'default_columns', wc_get_theme_support( 'product_blocks::default_columns', 3 ), true );
$this->asset_data_registry->add( 'min_rows', wc_get_theme_support( 'product_blocks::min_rows', 1 ), true );
$this->asset_data_registry->add( 'max_rows', wc_get_theme_support( 'product_blocks::max_rows', 6 ), true );
$this->asset_data_registry->add( 'default_rows', wc_get_theme_support( 'product_blocks::default_rows', 3 ), true );
// Hydrate the following data depending on admin or frontend context.
// Hydrate the All Product block with data from the API. This is for the add to cart buttons which show current quantity in cart, and events.
if ( ! is_admin() && ! WC()->is_rest_api_request() ) {
$this->hydrate_from_api();
$this->asset_data_registry->hydrate_api_request( '/wc/store/v1/cart' );
}
}
/**
* Hydrate the All Product block with data from the API. This is for the add to cart buttons which show current
* quantity in cart, and events.
*/
protected function hydrate_from_api() {
$this->asset_data_registry->hydrate_api_request( '/wc/store/v1/cart' );
}
/**
* It is necessary to register and enqueue assets during the render phase because we want to load assets only if the block has the content.
*/

View File

@@ -25,4 +25,13 @@ class AttributeFilter extends AbstractBlock {
parent::enqueue_data( $attributes );
$this->asset_data_registry->add( 'attributes', array_values( wc_get_attribute_taxonomies() ), true );
}
/**
* Get the frontend style handle for this block type.
*
* @return string[]
*/
protected function get_block_type_style() {
return array_merge( parent::get_block_type_style(), [ 'wc-blocks-packages-style' ] );
}
}

View File

@@ -1,9 +1,7 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\Package;
use Automattic\WooCommerce\Blocks\Assets;
use Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry;
use Automattic\WooCommerce\Blocks\Utils\CartCheckoutUtils;
/**
* Cart class.
@@ -25,6 +23,60 @@ class Cart extends AbstractBlock {
*/
protected $chunks_folder = 'cart-blocks';
/**
* Initialize this block type.
*
* - Hook into WP lifecycle.
* - Register the block with WordPress.
*/
protected function initialize() {
parent::initialize();
add_action( 'wp_loaded', array( $this, 'register_patterns' ) );
}
/**
* Register block pattern for Empty Cart Message to make it translatable.
*/
public function register_patterns() {
$shop_permalink = wc_get_page_id( 'shop' ) ? get_permalink( wc_get_page_id( 'shop' ) ) : '';
register_block_pattern(
'woocommerce/cart-heading',
array(
'title' => '',
'inserter' => false,
'content' => '<!-- wp:heading {"align":"wide", "level":1} --><h1 class="wp-block-heading alignwide">' . esc_html__( 'Cart', 'woocommerce' ) . '</h1><!-- /wp:heading -->',
)
);
register_block_pattern(
'woocommerce/cart-cross-sells-message',
array(
'title' => '',
'inserter' => false,
'content' => '<!-- wp:heading {"fontSize":"large"} --><h2 class="wp-block-heading has-large-font-size">' . esc_html__( 'You may be interested in…', 'woocommerce' ) . '</h2><!-- /wp:heading -->',
)
);
register_block_pattern(
'woocommerce/cart-empty-message',
array(
'title' => '',
'inserter' => false,
'content' => '
<!-- wp:heading {"textAlign":"center","className":"with-empty-cart-icon wc-block-cart__empty-cart__title"} --><h2 class="wp-block-heading has-text-align-center with-empty-cart-icon wc-block-cart__empty-cart__title">' . esc_html__( 'Your cart is currently empty!', 'woocommerce' ) . '</h2><!-- /wp:heading -->
<!-- wp:paragraph {"align":"center"} --><p class="has-text-align-center"><a href="' . esc_attr( esc_url( $shop_permalink ) ) . '">' . esc_html__( 'Browse store', 'woocommerce' ) . '</a></p><!-- /wp:paragraph -->
',
)
);
register_block_pattern(
'woocommerce/cart-new-in-store-message',
array(
'title' => '',
'inserter' => false,
'content' => '<!-- wp:heading {"textAlign":"center"} --><h2 class="wp-block-heading has-text-align-center">' . esc_html__( 'New in store', 'woocommerce' ) . '</h2><!-- /wp:heading -->',
)
);
}
/**
* Get the editor script handle for this block type.
*
@@ -56,6 +108,15 @@ class Cart extends AbstractBlock {
return $key ? $script[ $key ] : $script;
}
/**
* Get the frontend style handle for this block type.
*
* @return string[]
*/
protected function get_block_type_style() {
return array_merge( parent::get_block_type_style(), [ 'wc-blocks-packages-style' ] );
}
/**
* Enqueue frontend assets for this block, just in time for rendering.
*
@@ -160,40 +221,8 @@ class Cart extends AbstractBlock {
*/
protected function enqueue_data( array $attributes = [] ) {
parent::enqueue_data( $attributes );
if ( wc_shipping_enabled() ) {
$this->asset_data_registry->add(
'shippingCountries',
function() {
return $this->deep_sort_with_accents( WC()->countries->get_shipping_countries() );
},
true
);
$this->asset_data_registry->add(
'shippingStates',
function() {
return $this->deep_sort_with_accents( WC()->countries->get_shipping_country_states() );
},
true
);
}
$this->asset_data_registry->add(
'countryLocale',
function() {
// Merge country and state data to work around https://github.com/woocommerce/woocommerce/issues/28944.
$country_locale = wc()->countries->get_country_locale();
$states = wc()->countries->get_states();
foreach ( $states as $country => $states ) {
if ( empty( $states ) ) {
$country_locale[ $country ]['state']['required'] = false;
$country_locale[ $country ]['state']['hidden'] = true;
}
}
return $country_locale;
},
true
);
$this->asset_data_registry->add( 'countryData', CartCheckoutUtils::get_country_data(), true );
$this->asset_data_registry->add( 'baseLocation', wc_get_base_location(), true );
$this->asset_data_registry->add( 'isShippingCalculatorEnabled', filter_var( get_option( 'woocommerce_enable_shipping_calc' ), FILTER_VALIDATE_BOOLEAN ), true );
$this->asset_data_registry->add( 'displayItemizedTaxes', 'itemized' === get_option( 'woocommerce_tax_total_display' ), true );
@@ -203,10 +232,14 @@ class Cart extends AbstractBlock {
$this->asset_data_registry->add( 'shippingEnabled', wc_shipping_enabled(), true );
$this->asset_data_registry->add( 'hasDarkEditorStyleSupport', current_theme_supports( 'dark-editor-style' ), true );
$this->asset_data_registry->register_page_id( isset( $attributes['checkoutPageId'] ) ? $attributes['checkoutPageId'] : 0 );
$this->asset_data_registry->add( 'isBlockTheme', wc_current_theme_is_fse_theme(), true );
$pickup_location_settings = get_option( 'woocommerce_pickup_location_settings', [] );
$this->asset_data_registry->add( 'localPickupEnabled', wc_string_to_bool( $pickup_location_settings['enabled'] ?? 'no' ), true );
// Hydrate the following data depending on admin or frontend context.
if ( ! is_admin() && ! WC()->is_rest_api_request() ) {
$this->hydrate_from_api();
$this->asset_data_registry->hydrate_api_request( '/wc/store/v1/cart' );
}
/**
@@ -217,36 +250,6 @@ class Cart extends AbstractBlock {
do_action( 'woocommerce_blocks_cart_enqueue_data' );
}
/**
* Removes accents from an array of values, sorts by the values, then returns the original array values sorted.
*
* @param array $array Array of values to sort.
* @return array Sorted array.
*/
protected function deep_sort_with_accents( $array ) {
if ( ! is_array( $array ) || empty( $array ) ) {
return $array;
}
$array_without_accents = array_map(
function( $value ) {
return is_array( $value )
? $this->deep_sort_with_accents( $value )
: remove_accents( wc_strtolower( html_entity_decode( $value ) ) );
},
$array
);
asort( $array_without_accents );
return array_replace( $array_without_accents, $array );
}
/**
* Hydrate the cart block with data from the API.
*/
protected function hydrate_from_api() {
$this->asset_data_registry->hydrate_api_request( '/wc/store/v1/cart' );
}
/**
* Register script and style assets for the block type before it is registered.
*

View File

@@ -1,8 +1,8 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\Package;
use Automattic\WooCommerce\StoreApi\Utilities\LocalPickupUtils;
use Automattic\WooCommerce\Blocks\Utils\CartCheckoutUtils;
/**
* Checkout class.
@@ -24,6 +24,31 @@ class Checkout extends AbstractBlock {
*/
protected $chunks_folder = 'checkout-blocks';
/**
* Initialize this block type.
*
* - Hook into WP lifecycle.
* - Register the block with WordPress.
*/
protected function initialize() {
parent::initialize();
add_action( 'wp_loaded', array( $this, 'register_patterns' ) );
}
/**
* Register block pattern for Empty Cart Message to make it translatable.
*/
public function register_patterns() {
register_block_pattern(
'woocommerce/checkout-heading',
array(
'title' => '',
'inserter' => false,
'content' => '<!-- wp:heading {"align":"wide", "level":1} --><h1 class="wp-block-heading alignwide">' . esc_html__( 'Checkout', 'woocommerce' ) . '</h1><!-- /wp:heading -->',
)
);
}
/**
* Get the editor script handle for this block type.
*
@@ -55,6 +80,15 @@ class Checkout extends AbstractBlock {
return $key ? $script[ $key ] : $script;
}
/**
* Get the frontend style handle for this block type.
*
* @return string[]
*/
protected function get_block_type_style() {
return array_merge( parent::get_block_type_style(), [ 'wc-blocks-packages-style' ] );
}
/**
* Enqueue frontend assets for this block, just in time for rendering.
*
@@ -88,7 +122,7 @@ class Checkout extends AbstractBlock {
if ( $this->is_checkout_endpoint() ) {
// Note: Currently the block only takes care of the main checkout form -- if an endpoint is set, refer to the
// legacy shortcode instead and do not render block.
return '[woocommerce_checkout]';
return wc_current_theme_is_fse_theme() ? do_shortcode( '[woocommerce_checkout]' ) : '[woocommerce_checkout]';
}
// Deregister core checkout scripts and styles.
@@ -185,54 +219,7 @@ class Checkout extends AbstractBlock {
protected function enqueue_data( array $attributes = [] ) {
parent::enqueue_data( $attributes );
$this->asset_data_registry->add(
'allowedCountries',
function() {
return $this->deep_sort_with_accents( WC()->countries->get_allowed_countries() );
},
true
);
$this->asset_data_registry->add(
'allowedStates',
function() {
return $this->deep_sort_with_accents( WC()->countries->get_allowed_country_states() );
},
true
);
if ( wc_shipping_enabled() ) {
$this->asset_data_registry->add(
'shippingCountries',
function() {
return $this->deep_sort_with_accents( WC()->countries->get_shipping_countries() );
},
true
);
$this->asset_data_registry->add(
'shippingStates',
function() {
return $this->deep_sort_with_accents( WC()->countries->get_shipping_country_states() );
},
true
);
}
$this->asset_data_registry->add(
'countryLocale',
function() {
// Merge country and state data to work around https://github.com/woocommerce/woocommerce/issues/28944.
$country_locale = wc()->countries->get_country_locale();
$states = wc()->countries->get_states();
foreach ( $states as $country => $states ) {
if ( empty( $states ) ) {
$country_locale[ $country ]['state']['required'] = false;
$country_locale[ $country ]['state']['hidden'] = true;
}
}
return $country_locale;
},
true
);
$this->asset_data_registry->add( 'countryData', CartCheckoutUtils::get_country_data(), true );
$this->asset_data_registry->add( 'baseLocation', wc_get_base_location(), true );
$this->asset_data_registry->add(
'checkoutAllowsGuest',
@@ -259,11 +246,10 @@ class Checkout extends AbstractBlock {
$this->asset_data_registry->add( 'shippingEnabled', wc_shipping_enabled(), true );
$this->asset_data_registry->add( 'hasDarkEditorStyleSupport', current_theme_supports( 'dark-editor-style' ), true );
$this->asset_data_registry->register_page_id( isset( $attributes['cartPageId'] ) ? $attributes['cartPageId'] : 0 );
$this->asset_data_registry->add( 'isBlockTheme', wc_current_theme_is_fse_theme(), true );
$pickup_location_settings = get_option( 'woocommerce_pickup_location_settings', [] );
$local_pickup_enabled = wc_string_to_bool( $pickup_location_settings['enabled'] ?? 'no' );
$this->asset_data_registry->add( 'localPickupEnabled', $local_pickup_enabled, true );
$this->asset_data_registry->add( 'localPickupEnabled', wc_string_to_bool( $pickup_location_settings['enabled'] ?? 'no' ), true );
$is_block_editor = $this->is_block_editor();
@@ -337,7 +323,8 @@ class Checkout extends AbstractBlock {
}
if ( ! is_admin() && ! WC()->is_rest_api_request() ) {
$this->hydrate_from_api();
$this->asset_data_registry->hydrate_api_request( '/wc/store/v1/cart' );
$this->asset_data_registry->hydrate_data_from_api_request( 'checkoutData', '/wc/store/v1/checkout' );
$this->hydrate_customer_payment_methods();
}
@@ -376,26 +363,6 @@ class Checkout extends AbstractBlock {
return $screen && $screen->is_block_editor();
}
/**
* Removes accents from an array of values, sorts by the values, then returns the original array values sorted.
*
* @param array $array Array of values to sort.
* @return array Sorted array.
*/
protected function deep_sort_with_accents( $array ) {
if ( ! is_array( $array ) || empty( $array ) ) {
return $array;
}
if ( is_array( reset( $array ) ) ) {
return array_map( [ $this, 'deep_sort_with_accents' ], $array );
}
$array_without_accents = array_map( 'remove_accents', array_map( 'wc_strtolower', array_map( 'html_entity_decode', $array ) ) );
asort( $array_without_accents );
return array_replace( $array_without_accents, $array );
}
/**
* Get saved customer payment methods for use in checkout.
*/
@@ -425,23 +392,6 @@ class Checkout extends AbstractBlock {
remove_filter( 'woocommerce_payment_methods_list_item', [ $this, 'include_token_id_with_payment_methods' ], 10, 2 );
}
/**
* Hydrate the checkout block with data from the API.
*/
protected function hydrate_from_api() {
$this->asset_data_registry->hydrate_api_request( '/wc/store/v1/cart' );
// Print existing notices now, otherwise they are caught by the Cart
// Controller and converted to exceptions.
wc_print_notices();
add_filter( 'woocommerce_store_api_disable_nonce_check', '__return_true' );
$rest_preload_api_requests = rest_preload_api_request( [], '/wc/store/v1/checkout' );
$this->asset_data_registry->add( 'checkoutData', $rest_preload_api_requests['/wc/store/v1/checkout']['body'] ?? [] );
remove_filter( 'woocommerce_store_api_disable_nonce_check', '__return_true' );
}
/**
* Callback for woocommerce_payment_methods_list_item filter to add token id
* to the generated list.

View File

@@ -3,11 +3,13 @@ namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\Templates\ProductAttributeTemplate;
use Automattic\WooCommerce\Blocks\Templates\ProductSearchResultsTemplate;
use Automattic\WooCommerce\Blocks\Templates\OrderConfirmationTemplate;
use Automattic\WooCommerce\Blocks\Utils\StyleAttributesUtils;
use WC_Query;
use WC_Shortcode_Checkout;
use WC_Frontend_Scripts;
/**
* Classic Single Product class
* Classic Template class
*
* @internal
*/
@@ -35,7 +37,30 @@ class ClassicTemplate extends AbstractDynamicBlock {
parent::initialize();
add_filter( 'render_block', array( $this, 'add_alignment_class_to_wrapper' ), 10, 2 );
add_filter( 'woocommerce_product_query_meta_query', array( $this, 'filter_products_by_stock' ) );
add_action( 'enqueue_block_assets', array( $this, 'enqueue_block_assets' ) );
}
/**
* Enqueue assets used for rendering the block in editor context.
*
* This is needed if a block is not yet within the post content--`render` and `enqueue_assets` may not have ran.
*/
public function enqueue_block_assets() {
// Ensures frontend styles for blocks exist in the site editor iframe.
if ( class_exists( 'WC_Frontend_Scripts' ) && is_admin() ) {
$frontend_scripts = new WC_Frontend_Scripts();
$styles = $frontend_scripts::get_styles();
foreach ( $styles as $handle => $style ) {
wp_enqueue_style(
$handle,
set_url_scheme( $style['src'] ),
$style['deps'],
$style['version'],
$style['media']
);
}
}
}
/**
@@ -58,36 +83,73 @@ class ClassicTemplate extends AbstractDynamicBlock {
* @see https://github.com/woocommerce/woocommerce-gutenberg-products-block/issues/5328#issuecomment-989013447
*/
if ( class_exists( 'WC_Frontend_Scripts' ) ) {
$frontend_scripts = new \WC_Frontend_Scripts();
$frontend_scripts = new WC_Frontend_Scripts();
$frontend_scripts::load_scripts();
}
$archive_templates = array( 'archive-product', 'taxonomy-product_cat', 'taxonomy-product_tag', ProductAttributeTemplate::SLUG, ProductSearchResultsTemplate::SLUG );
if ( OrderConfirmationTemplate::get_slug() === $attributes['template'] ) {
return $this->render_order_received();
}
if ( 'single-product' === $attributes['template'] ) {
if ( is_product() ) {
return $this->render_single_product();
} elseif ( in_array( $attributes['template'], $archive_templates, true ) ) {
}
$archive_templates = array(
'archive-product',
'taxonomy-product_cat',
'taxonomy-product_tag',
ProductAttributeTemplate::SLUG,
ProductSearchResultsTemplate::SLUG,
);
if ( in_array( $attributes['template'], $archive_templates, true ) ) {
// Set this so that our product filters can detect if it's a PHP template.
$this->asset_data_registry->add( 'is_rendering_php_template', true, true );
$this->asset_data_registry->add( 'isRenderingPhpTemplate', true, true );
// Set this so filter blocks being used as widgets know when to render.
$this->asset_data_registry->add( 'has_filterable_products', true, true );
$this->asset_data_registry->add( 'hasFilterableProducts', true, true );
$this->asset_data_registry->add(
'page_url',
'pageUrl',
html_entity_decode( get_pagenum_link() ),
''
);
return $this->render_archive_product();
} else {
ob_start();
echo "You're using the ClassicTemplate block";
wp_reset_postdata();
return ob_get_clean();
}
ob_start();
echo "You're using the ClassicTemplate block";
wp_reset_postdata();
return ob_get_clean();
}
/**
* Render method for rendering the order confirmation template.
*
* @return string Rendered block type output.
*/
protected function render_order_received() {
ob_start();
echo '<div class="wp-block-group">';
echo sprintf(
'<%1$s %2$s>%3$s</%1$s>',
'h1',
get_block_wrapper_attributes(), // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
esc_html__( 'Order confirmation', 'woocommerce' )
);
WC_Shortcode_Checkout::output( array() );
echo '</div>';
return ob_get_clean();
}
/**
@@ -336,4 +398,12 @@ class ClassicTemplate extends AbstractDynamicBlock {
return $meta_query;
}
/**
* Get the frontend style handle for this block type.
*
* @return null
*/
protected function get_block_type_style() {
return null;
}
}

View File

@@ -321,7 +321,6 @@ abstract class FeaturedItem extends AbstractDynamicBlock {
*/
protected function enqueue_data( array $attributes = [] ) {
parent::enqueue_data( $attributes );
$this->asset_data_registry->add( 'min_height', wc_get_theme_support( 'featured_block::min_height', 500 ), true );
$this->asset_data_registry->add( 'default_height', wc_get_theme_support( 'featured_block::default_height', 500 ), true );
$this->asset_data_registry->add( 'defaultHeight', wc_get_theme_support( 'featured_block::default_height', 500 ), true );
}
}

View File

@@ -11,4 +11,13 @@ class FilterWrapper extends AbstractBlock {
* @var string
*/
protected $block_name = 'filter-wrapper';
/**
* Get the frontend style handle for this block type.
*
* @return null
*/
protected function get_block_type_style() {
return null;
}
}

View File

@@ -21,7 +21,7 @@ class HandpickedProducts extends AbstractProductGrid {
$ids = array_map( 'absint', $this->attributes['products'] );
$query_args['post__in'] = $ids;
$query_args['posts_per_page'] = count( $ids );
$query_args['posts_per_page'] = count( $ids ); // phpcs:ignore WordPress.WP.PostsPerPage.posts_per_page_posts_per_page
}
/**

View File

@@ -8,9 +8,11 @@ use Automattic\WooCommerce\Blocks\Assets\Api as AssetApi;
use Automattic\WooCommerce\Blocks\Integrations\IntegrationRegistry;
use Automattic\WooCommerce\Blocks\Utils\StyleAttributesUtils;
use Automattic\WooCommerce\Blocks\Utils\BlockTemplateUtils;
use Automattic\WooCommerce\Blocks\Utils\Utils;
use Automattic\WooCommerce\Blocks\Utils\MiniCartUtils;
/**
* Mini Cart class.
* Mini-Cart class.
*
* @internal
*/
@@ -70,9 +72,7 @@ class MiniCart extends AbstractBlock {
protected function initialize() {
parent::initialize();
add_action( 'wp_loaded', array( $this, 'register_empty_cart_message_block_pattern' ) );
add_action( 'wp_print_footer_scripts', array( $this, 'enqueue_wc_settings' ), 1 );
// We need this action to run after the equivalent in AssetDataRegistry.
add_action( 'wp_print_footer_scripts', array( $this, 'print_lazy_load_scripts' ), 3 );
add_action( 'wp_print_footer_scripts', array( $this, 'print_lazy_load_scripts' ), 2 );
}
/**
@@ -110,6 +110,15 @@ class MiniCart extends AbstractBlock {
return $key ? $script[ $key ] : $script;
}
/**
* Get the frontend style handle for this block type.
*
* @return string[]
*/
protected function get_block_type_style() {
return array_merge( parent::get_block_type_style(), [ 'wc-blocks-packages-style' ] );
}
/**
* Extra data passed through from server to client for block.
*
@@ -136,20 +145,6 @@ class MiniCart extends AbstractBlock {
$this->tax_label,
''
);
$cart_payload = $this->get_cart_payload();
$this->asset_data_registry->add(
'cartTotals',
isset( $cart_payload['totals'] ) ? $cart_payload['totals'] : null,
null
);
$this->asset_data_registry->add(
'cartItemsCount',
isset( $cart_payload['items_count'] ) ? $cart_payload['items_count'] : null,
null
);
}
$this->asset_data_registry->add(
@@ -162,7 +157,7 @@ class MiniCart extends AbstractBlock {
if (
current_user_can( 'edit_theme_options' ) &&
wc_current_theme_is_fse_theme()
( wc_current_theme_is_fse_theme() || current_theme_supports( 'block-template-parts' ) )
) {
$theme_slug = BlockTemplateUtils::theme_has_template_part( 'mini-cart' ) ? wp_get_theme()->get_stylesheet() : BlockTemplateUtils::PLUGIN_SLUG;
@@ -181,12 +176,14 @@ class MiniCart extends AbstractBlock {
);
}
$template_part_edit_uri = add_query_arg(
array(
'postId' => sprintf( '%s//%s', $theme_slug, 'mini-cart' ),
'postType' => 'wp_template_part',
),
$site_editor_uri
$template_part_edit_uri = esc_url_raw(
add_query_arg(
array(
'postId' => sprintf( '%s//%s', $theme_slug, 'mini-cart' ),
'postType' => 'wp_template_part',
),
$site_editor_uri
)
);
}
@@ -204,30 +201,6 @@ class MiniCart extends AbstractBlock {
do_action( 'woocommerce_blocks_cart_enqueue_data' );
}
/**
* Function to enqueue `wc-settings` script and dequeue it later on so when
* AssetDataRegistry runs, it appears enqueued- This allows the necessary
* data to be printed to the page.
*/
public function enqueue_wc_settings() {
// Return early if another block has already enqueued `wc-settings`.
if ( wp_script_is( 'wc-settings', 'enqueued' ) ) {
return;
}
// We are lazy-loading `wc-settings`, but we need to enqueue it here so
// AssetDataRegistry knows it's going to load.
wp_enqueue_script( 'wc-settings' );
// After AssetDataRegistry function runs, we dequeue `wc-settings`.
add_action( 'wp_print_footer_scripts', array( $this, 'dequeue_wc_settings' ), 4 );
}
/**
* Function to dequeue `wc-settings` script.
*/
public function dequeue_wc_settings() {
wp_dequeue_script( 'wc-settings' );
}
/**
* Prints the variable containing information about the scripts to lazy load.
*/
@@ -282,6 +255,8 @@ class MiniCart extends AbstractBlock {
'products-table-frontend',
'cart-button-frontend',
'checkout-button-frontend',
'title-label-frontend',
'title-items-counter-frontend',
);
}
foreach ( $inner_blocks_frontend_scripts as $inner_block_frontend_script ) {
@@ -328,7 +303,7 @@ class MiniCart extends AbstractBlock {
$wp_scripts = wp_scripts();
// This script and its dependencies have already been appended.
if ( ! $script || array_key_exists( $script->handle, $this->scripts_to_lazy_load ) || isset( $wp_scripts->queue[ $script->handle ] ) ) {
if ( ! $script || array_key_exists( $script->handle, $this->scripts_to_lazy_load ) || wp_script_is( $script->handle, 'enqueued' ) ) {
return;
}
@@ -349,11 +324,19 @@ class MiniCart extends AbstractBlock {
$site_url = site_url() ?? wp_guess_url();
if ( Utils::wp_version_compare( '6.3', '>=' ) ) {
$script_before = $wp_scripts->get_inline_script_data( $script->handle, 'before' );
$script_after = $wp_scripts->get_inline_script_data( $script->handle, 'after' );
} else {
$script_before = $wp_scripts->print_inline_script( $script->handle, 'before', false );
$script_after = $wp_scripts->print_inline_script( $script->handle, 'after', false );
}
$this->scripts_to_lazy_load[ $script->handle ] = array(
'src' => preg_match( '|^(https?:)?//|', $script->src ) ? $script->src : $site_url . $script->src,
'version' => $script->ver,
'before' => $wp_scripts->print_inline_script( $script->handle, 'before', false ),
'after' => $wp_scripts->print_inline_script( $script->handle, 'after', false ),
'before' => $script_before,
'after' => $script_after,
'translations' => $wp_scripts->print_translations( $script->handle, false ),
);
}
@@ -369,32 +352,29 @@ class MiniCart extends AbstractBlock {
if ( isset( $attributes['hasHiddenPrice'] ) && false !== $attributes['hasHiddenPrice'] ) {
return;
}
$price_color = array_key_exists( 'priceColor', $attributes ) ? $attributes['priceColor']['color'] : '';
$cart = $this->get_cart_instance();
$cart_contents_total = $cart->get_subtotal();
if ( $cart->display_prices_including_tax() ) {
$cart_contents_total += $cart->get_subtotal_tax();
}
return '<span class="wc-block-mini-cart__amount">' . esc_html( wp_strip_all_tags( wc_price( $cart_contents_total ) ) ) . '</span>
' . $this->get_include_tax_label_markup();
return '<span class="wc-block-mini-cart__amount" style="color:' . $price_color . ' "></span>' . $this->get_include_tax_label_markup( $attributes );
}
/**
* Returns the markup for render the tax label.
*
* @param array $attributes Block attributes.
*
* @return string
*/
protected function get_include_tax_label_markup() {
$cart = $this->get_cart_instance();
$cart_contents_total = $cart->get_subtotal();
protected function get_include_tax_label_markup( $attributes ) {
if ( empty( $this->tax_label ) ) {
return '';
}
$price_color = array_key_exists( 'priceColor', $attributes ) ? $attributes['priceColor']['color'] : '';
return ( ! empty( $this->tax_label ) && 0 !== $cart_contents_total ) ? ( "<small class='wc-block-mini-cart__tax-label'>" . esc_html( $this->tax_label ) . '</small>' ) : '';
return '<small class="wc-block-mini-cart__tax-label" style="color:' . $price_color . ' " hidden>' . esc_html( $this->tax_label ) . '</small>';
}
/**
* Append frontend scripts when rendering the Mini Cart block.
* Append frontend scripts when rendering the Mini-Cart block.
*
* @param array $attributes Block attributes.
* @param string $content Block content.
@@ -402,11 +382,11 @@ class MiniCart extends AbstractBlock {
* @return string Rendered block type output.
*/
protected function render( $attributes, $content, $block ) {
return $content . $this->get_markup( $attributes );
return $content . $this->get_markup( MiniCartUtils::migrate_attributes_to_color_panel( $attributes ) );
}
/**
* Render the markup for the Mini Cart block.
* Render the markup for the Mini-Cart block.
*
* @param array $attributes Block attributes.
*
@@ -419,14 +399,6 @@ class MiniCart extends AbstractBlock {
return '';
}
$cart = $this->get_cart_instance();
$cart_contents_count = $cart->get_cart_contents_count();
$cart_contents_total = $cart->get_subtotal();
if ( $cart->display_prices_including_tax() ) {
$cart_contents_total += $cart->get_subtotal_tax();
}
$classes_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes, array( 'text_color', 'background_color', 'font_size', 'font_weight', 'font_family' ) );
$wrapper_classes = sprintf( 'wc-block-mini-cart wp-block-woocommerce-mini-cart %s', $classes_styles['classes'] );
if ( ! empty( $attributes['className'] ) ) {
@@ -434,26 +406,36 @@ class MiniCart extends AbstractBlock {
}
$wrapper_styles = $classes_styles['styles'];
$aria_label = sprintf(
/* translators: %1$d is the number of products in the cart. %2$s is the cart total */
_n(
'%1$d item in cart, total price of %2$s',
'%1$d items in cart, total price of %2$s',
$cart_contents_count,
'woocommerce'
),
$cart_contents_count,
wp_strip_all_tags( wc_price( $cart_contents_total ) )
);
$icon = '<svg class="wc-block-mini-cart__icon" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.84614 18.2769C7.89712 18.2769 7.93845 18.2356 7.93845 18.1846C7.93845 18.1336 7.89712 18.0923 7.84614 18.0923C7.79516 18.0923 7.75384 18.1336 7.75384 18.1846C7.75384 18.2356 7.79516 18.2769 7.84614 18.2769ZM6.03076 18.1846C6.03076 17.182 6.84353 16.3692 7.84614 16.3692C8.84875 16.3692 9.66152 17.182 9.66152 18.1846C9.66152 19.1872 8.84875 20 7.84614 20C6.84353 20 6.03076 19.1872 6.03076 18.1846Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.3231 18.2769C17.3741 18.2769 17.4154 18.2356 17.4154 18.1846C17.4154 18.1336 17.3741 18.0923 17.3231 18.0923C17.2721 18.0923 17.2308 18.1336 17.2308 18.1846C17.2308 18.2356 17.2721 18.2769 17.3231 18.2769ZM15.5077 18.1846C15.5077 17.182 16.3205 16.3692 17.3231 16.3692C18.3257 16.3692 19.1385 17.182 19.1385 18.1846C19.1385 19.1872 18.3257 20 17.3231 20C16.3205 20 15.5077 19.1872 15.5077 18.1846Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.0631 9.53835L19.4662 12.6685L19.4648 12.6757L19.4648 12.6757C19.3424 13.2919 19.0072 13.8454 18.5178 14.2394C18.031 14.6312 17.4226 14.8404 16.798 14.8308H8.44017C7.81556 14.8404 7.20714 14.6312 6.72038 14.2394C6.2312 13.8456 5.89605 13.2924 5.77352 12.6765L5.77335 12.6757L4.33477 5.48814C4.3286 5.46282 4.32345 5.43711 4.31934 5.41104L3.61815 1.90768H0.953842C0.42705 1.90768 0 1.48063 0 0.953842C0 0.42705 0.42705 0 0.953842 0H4.4C4.85462 0 5.24607 0.320858 5.33529 0.766644L6.04403 4.30769H12.785C13.0114 4.99157 13.3319 5.63258 13.7312 6.21538H6.42585L7.64421 12.3026L7.64449 12.304C7.67966 12.4811 7.77599 12.6402 7.91662 12.7534C8.05725 12.8666 8.23322 12.9267 8.41372 12.9233L8.432 12.9231H16.8062L16.8244 12.9233C17.0049 12.9267 17.1809 12.8666 17.3215 12.7534C17.4614 12.6408 17.5575 12.4828 17.5931 12.3068L17.5937 12.304L18.1649 9.30867C18.762 9.45873 19.387 9.53842 20.0307 9.53842C20.0415 9.53842 20.0523 9.5384 20.0631 9.53835Z" fill="currentColor"/>
</svg>';
$icon_color = array_key_exists( 'iconColor', $attributes ) ? $attributes['iconColor']['color'] : 'currentColor';
$product_count_color = array_key_exists( 'productCountColor', $attributes ) ? $attributes['productCountColor']['color'] : '';
// Default "Cart" icon.
$icon = '<svg class="wc-block-mini-cart__icon" width="32" height="32" viewBox="0 0 32 32" fill="' . $icon_color . '" xmlns="http://www.w3.org/2000/svg">
<circle cx="12.6667" cy="24.6667" r="2" fill="' . $icon_color . '"/>
<circle cx="23.3333" cy="24.6667" r="2" fill="' . $icon_color . '"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.28491 10.0356C9.47481 9.80216 9.75971 9.66667 10.0606 9.66667H25.3333C25.6232 9.66667 25.8989 9.79247 26.0888 10.0115C26.2787 10.2305 26.3643 10.5211 26.3233 10.8081L24.99 20.1414C24.9196 20.6341 24.4977 21 24 21H12C11.5261 21 11.1173 20.6674 11.0209 20.2034L9.08153 10.8701C9.02031 10.5755 9.09501 10.269 9.28491 10.0356ZM11.2898 11.6667L12.8136 19H23.1327L24.1803 11.6667H11.2898Z" fill="' . $icon_color . '"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.66669 6.66667C5.66669 6.11438 6.1144 5.66667 6.66669 5.66667H9.33335C9.81664 5.66667 10.2308 6.01229 10.3172 6.48778L11.0445 10.4878C11.1433 11.0312 10.7829 11.5517 10.2395 11.6505C9.69614 11.7493 9.17555 11.3889 9.07676 10.8456L8.49878 7.66667H6.66669C6.1144 7.66667 5.66669 7.21895 5.66669 6.66667Z" fill="' . $icon_color . '"/>
</svg>';
if ( isset( $attributes['miniCartIcon'] ) ) {
if ( 'bag' === $attributes['miniCartIcon'] ) {
$icon = '<svg class="wc-block-mini-cart__icon" width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.4444 14.2222C12.9354 14.2222 13.3333 14.6202 13.3333 15.1111C13.3333 15.8183 13.6143 16.4966 14.1144 16.9967C14.6145 17.4968 15.2927 17.7778 16 17.7778C16.7072 17.7778 17.3855 17.4968 17.8856 16.9967C18.3857 16.4966 18.6667 15.8183 18.6667 15.1111C18.6667 14.6202 19.0646 14.2222 19.5555 14.2222C20.0465 14.2222 20.4444 14.6202 20.4444 15.1111C20.4444 16.2898 19.9762 17.4203 19.1427 18.2538C18.3092 19.0873 17.1787 19.5555 16 19.5555C14.8212 19.5555 13.6908 19.0873 12.8573 18.2538C12.0238 17.4203 11.5555 16.2898 11.5555 15.1111C11.5555 14.6202 11.9535 14.2222 12.4444 14.2222Z" fill="' . $icon_color . '""/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.2408 6.68254C11.4307 6.46089 11.7081 6.33333 12 6.33333H20C20.2919 6.33333 20.5693 6.46089 20.7593 6.68254L24.7593 11.3492C25.0134 11.6457 25.0717 12.0631 24.9085 12.4179C24.7453 12.7727 24.3905 13 24 13H8.00001C7.60948 13 7.25469 12.7727 7.0915 12.4179C6.92832 12.0631 6.9866 11.6457 7.24076 11.3492L11.2408 6.68254ZM12.4599 8.33333L10.1742 11H21.8258L19.5401 8.33333H12.4599Z" fill="' . $icon_color . '"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7 12C7 11.4477 7.44772 11 8 11H24C24.5523 11 25 11.4477 25 12V25.3333C25 25.8856 24.5523 26.3333 24 26.3333H8C7.44772 26.3333 7 25.8856 7 25.3333V12ZM9 13V24.3333H23V13H9Z" fill="' . $icon_color . '"/>
</svg>';
} elseif ( 'bag-alt' === $attributes['miniCartIcon'] ) {
$icon = '<svg class="wc-block-mini-cart__icon" width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.5556 12.3333C19.0646 12.3333 18.6667 11.9354 18.6667 11.4444C18.6667 10.7372 18.3857 8.05893 17.8856 7.55883C17.3855 7.05873 16.7073 6.77778 16 6.77778C15.2928 6.77778 14.6145 7.05873 14.1144 7.55883C13.6143 8.05893 13.3333 10.7372 13.3333 11.4444C13.3333 11.9354 12.9354 12.3333 12.4445 12.3333C11.9535 12.3333 11.5556 11.9354 11.5556 11.4444C11.5556 10.2657 12.0238 7.13524 12.8573 6.30175C13.6908 5.46825 14.8213 5 16 5C17.1788 5 18.3092 5.46825 19.1427 6.30175C19.9762 7.13524 20.4445 10.2657 20.4445 11.4444C20.4445 11.9354 20.0465 12.3333 19.5556 12.3333Z" fill="' . $icon_color . '"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.5 12C7.5 11.4477 7.94772 11 8.5 11H23.5C24.0523 11 24.5 11.4477 24.5 12V25.3333C24.5 25.8856 24.0523 26.3333 23.5 26.3333H8.5C7.94772 26.3333 7.5 25.8856 7.5 25.3333V12ZM9.5 13V24.3333H22.5V13H9.5Z" fill="' . $icon_color . '" />
</svg>';
}
}
$button_html = $this->get_cart_price_markup( $attributes ) . '
<span class="wc-block-mini-cart__quantity-badge">
' . $icon . '
<span class="wc-block-mini-cart__badge">' . $cart_contents_count . '</span>
<span class="wc-block-mini-cart__badge" style="background:' . $product_count_color . '"></span>
</span>';
if ( is_cart() || is_checkout() ) {
@@ -461,18 +443,23 @@ class MiniCart extends AbstractBlock {
return '';
}
// It is not necessary to load the Mini Cart Block on Cart and Checkout page.
// It is not necessary to load the Mini-Cart Block on Cart and Checkout page.
return '<div class="' . $wrapper_classes . '" style="visibility:hidden" aria-hidden="true">
<button class="wc-block-mini-cart__button" aria-label="' . esc_attr( $aria_label ) . '" disabled>' . $button_html . '</button>
<button class="wc-block-mini-cart__button" disabled>' . $button_html . '</button>
</div>';
}
$template_part_contents = '';
// Determine if we need to load the template part from the theme, or WooCommerce in that order.
$theme_has_mini_cart = BlockTemplateUtils::theme_has_template_part( 'mini-cart' );
$template_slug_to_load = $theme_has_mini_cart ? get_stylesheet() : BlockTemplateUtils::PLUGIN_SLUG;
$template_part = BlockTemplateUtils::get_block_template( $template_slug_to_load . '//mini-cart', 'wp_template_part' );
// Determine if we need to load the template part from the DB, the theme or WooCommerce in that order.
$templates_from_db = BlockTemplateUtils::get_block_templates_from_db( array( 'mini-cart' ), 'wp_template_part' );
if ( count( $templates_from_db ) > 0 ) {
$template_slug_to_load = $templates_from_db[0]->theme;
} else {
$theme_has_mini_cart = BlockTemplateUtils::theme_has_template_part( 'mini-cart' );
$template_slug_to_load = $theme_has_mini_cart ? get_stylesheet() : BlockTemplateUtils::PLUGIN_SLUG;
}
$template_part = BlockTemplateUtils::get_block_template( $template_slug_to_load . '//mini-cart', 'wp_template_part' );
if ( $template_part && ! empty( $template_part->content ) ) {
$template_part_contents = do_blocks( $template_part->content );
@@ -486,13 +473,10 @@ class MiniCart extends AbstractBlock {
}
return '<div class="' . esc_attr( $wrapper_classes ) . '" style="' . esc_attr( $wrapper_styles ) . '">
<button class="wc-block-mini-cart__button" aria-label="' . esc_attr( $aria_label ) . '">' . $button_html . '</button>
<div class="wc-block-mini-cart__drawer is-loading is-mobile wc-block-components-drawer__screen-overlay wc-block-components-drawer__screen-overlay--is-hidden" aria-hidden="true">
<div class="components-modal__frame wc-block-components-drawer">
<div class="components-modal__content">
<div class="components-modal__header">
<div class="components-modal__header-heading-container"></div>
</div>
<button class="wc-block-mini-cart__button">' . $button_html . '</button>
<div class="is-loading wc-block-components-drawer__screen-overlay wc-block-components-drawer__screen-overlay--is-hidden" aria-hidden="true">
<div class="wc-block-mini-cart__drawer wc-block-components-drawer">
<div class="wc-block-components-drawer__content">
<div class="wc-block-mini-cart__template-part">'
. wp_kses_post( $template_part_contents ) .
'</div>
@@ -556,22 +540,6 @@ class MiniCart extends AbstractBlock {
);
}
/**
* Get Cart Payload.
*
* @return object;
*/
protected function get_cart_payload() {
$notices = wc_get_notices(); // Backup the notices because StoreAPI will remove them.
$payload = WC()->api->get_endpoint_data( '/wc/store/cart' );
if ( ! empty( $notices ) ) {
wc_set_notices( $notices ); // Restore the notices.
}
return $payload;
}
/**
* Prepare translations for inner blocks and dependencies.
*/
@@ -602,7 +570,7 @@ class MiniCart extends AbstractBlock {
register_block_pattern(
'woocommerce/mini-cart-empty-cart-message',
array(
'title' => __( 'Empty Mini Cart Message', 'woocommerce' ),
'title' => __( 'Empty Mini-Cart Message', 'woocommerce' ),
'inserter' => false,
'content' => '<!-- wp:paragraph {"align":"center"} --><p class="has-text-align-center"><strong>' . __( 'Your cart is currently empty!', 'woocommerce' ) . '</strong></p><!-- /wp:paragraph -->',
)
@@ -610,7 +578,7 @@ class MiniCart extends AbstractBlock {
}
/**
* Returns whether the mini cart should be rendered or not.
* Returns whether the Mini-Cart should be rendered or not.
*
* @param array $attributes Block attributes.
*

View File

@@ -4,7 +4,7 @@ namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\Utils\StyleAttributesUtils;
/**
* Mini Cart class.
* Mini-Cart Contents class.
*
* @internal
*/
@@ -40,13 +40,22 @@ class MiniCartContents extends AbstractBlock {
* @return null
*/
protected function get_block_type_script( $key = null ) {
// The frontend script is a dependency of the Mini Cart block so it's
// The frontend script is a dependency of the Mini-Cart block so it's
// already lazy-loaded.
return null;
}
/**
* Render the markup for the Mini Cart contents block.
* Get the frontend style handle for this block type.
*
* @return string[]
*/
protected function get_block_type_style() {
return array_merge( parent::get_block_type_style(), [ 'wc-blocks-packages-style' ] );
}
/**
* Render the markup for the Mini-Cart Contents block.
*
* @param array $attributes Block attributes.
* @param string $content Block content.
@@ -74,15 +83,6 @@ class MiniCartContents extends AbstractBlock {
$bg_color = StyleAttributesUtils::get_background_color_class_and_style( $attributes );
$styles = array(
array(
'selector' => '.wc-block-mini-cart__drawer .components-modal__header',
'properties' => array(
array(
'property' => 'color',
'value' => $text_color ? $text_color['value'] : false,
),
),
),
array(
'selector' => array(
'.wc-block-mini-cart__footer .wc-block-mini-cart__footer-actions .wc-block-mini-cart__footer-checkout',
@@ -111,6 +111,9 @@ class MiniCartContents extends AbstractBlock {
);
$parsed_style = '';
if ( array_key_exists( 'width', $attributes ) ) {
$parsed_style .= ':root{--drawer-width: ' . esc_html( $attributes['width'] ) . '}';
}
foreach ( $styles as $style ) {
$selector = is_array( $style['selector'] ) ? implode( ',', $style['selector'] ) : $style['selector'];
@@ -138,7 +141,7 @@ class MiniCartContents extends AbstractBlock {
}
/**
* Get list of Mini Cart block & its inner-block types.
* Get list of Mini-Cart Contents block & its inner-block types.
*
* @return array;
*/
@@ -155,6 +158,8 @@ class MiniCartContents extends AbstractBlock {
$block_types[] = 'MiniCartCartButtonBlock';
$block_types[] = 'MiniCartCheckoutButtonBlock';
$block_types[] = 'MiniCartTitleBlock';
$block_types[] = 'MiniCartTitleItemsCounterBlock';
$block_types[] = 'MiniCartTitleLabelBlock';
return $block_types;
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* MiniCartTitleItemsCounterBlock class.
*/
class MiniCartTitleItemsCounterBlock extends AbstractInnerBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'mini-cart-title-items-counter-block';
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* MiniCartTitleLabelBlock class.
*/
class MiniCartTitleLabelBlock extends AbstractInnerBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'mini-cart-title-label-block';
}

View File

@@ -12,7 +12,6 @@ class ProceedToCheckoutBlock extends AbstractInnerBlock {
*/
protected $block_name = 'proceed-to-checkout-block';
/**
* Extra data passed through from server to client for block.
*

View File

@@ -0,0 +1,103 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\Utils\StyleAttributesUtils;
/**
* ProductAverageRating class.
*/
class ProductAverageRating extends AbstractBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'product-average-rating';
/**
* API version name.
*
* @var string
*/
protected $api_version = '2';
/**
* Get block supports. Shared with the frontend.
* IMPORTANT: If you change anything here, make sure to update the JS file too.
*
* @return array
*/
protected function get_block_type_supports() {
return array(
'color' =>
array(
'text' => true,
'background' => true,
'__experimentalSkipSerialization' => true,
),
'spacing' =>
array(
'margin' => true,
'padding' => true,
'__experimentalSkipSerialization' => true,
),
'typography' =>
array(
'fontSize' => true,
'__experimentalFontWeight' => true,
'__experimentalSkipSerialization' => true,
),
'__experimentalSelector' => '.wc-block-components-product-average-rating',
);
}
/**
* Overwrite parent method to prevent script registration.
*
* It is necessary to register and enqueues assets during the render
* phase because we want to load assets only if the block has the content.
*/
protected function register_block_type_assets() {
return null;
}
/**
* Get the frontend style handle for this block type.
*
* @return null
*/
protected function get_block_type_style() {
return null;
}
/**
* Include and render the block.
*
* @param array $attributes Block attributes. Default empty array.
* @param string $content Block content. Default empty string.
* @param WP_Block $block Block instance.
* @return string Rendered block type output.
*/
protected function render( $attributes, $content, $block ) {
$post_id = $block->context['postId'];
$product = wc_get_product( $post_id );
if ( ! $product || ! $product->get_review_count() ) {
return '';
}
$styles_and_classes = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes );
$text_align_styles_and_classes = StyleAttributesUtils::get_text_align_class_and_style( $attributes );
return sprintf(
'<div class="wc-block-components-product-average-rating-counter %1$s %2$s" style="%3$s">%4$s</div>',
esc_attr( $text_align_styles_and_classes['class'] ?? '' ),
esc_attr( $styles_and_classes['classes'] ),
esc_attr( $styles_and_classes['styles'] ?? '' ),
$product->get_average_rating()
);
}
}

View File

@@ -15,48 +15,20 @@ class ProductButton extends AbstractBlock {
*/
protected $block_name = 'product-button';
/**
* API version name.
*
* @var string
*/
protected $api_version = '2';
/**
* Get block supports. Shared with the frontend.
* IMPORTANT: If you change anything here, make sure to update the JS file too.
* Get the frontend script handle for this block type.
*
* @return array
* @param string $key Data to get, or default to everything.
*/
protected function get_block_type_supports() {
return array(
'color' =>
array(
'background' => true,
'link' => false,
'text' => true,
'__experimentalSkipSerialization' => true,
),
'__experimentalBorder' =>
array(
'radius' => true,
'__experimentalSkipSerialization' => true,
),
'typography' =>
array(
'fontSize' => true,
'__experimentalFontWeight' => true,
'__experimentalSkipSerialization' => true,
),
'__experimentalSelector' => '.wp-block-button.wc-block-components-product-button .wc-block-components-product-button__button',
);
}
protected function get_block_type_script( $key = null ) {
$script = [
'handle' => 'wc-' . $this->block_name . '-interactivity-frontend',
'path' => $this->asset_api->get_block_asset_build_path( $this->block_name . '-interactivity-frontend' ),
'dependencies' => [ 'wc-interactivity' ],
];
/**
* It is necessary to register and enqueue assets during the render phase because we want to load assets only if the block has the content.
*/
protected function register_block_type_assets() {
return null;
return $key ? $script[ $key ] : $script;
}
/**
@@ -66,6 +38,31 @@ class ProductButton extends AbstractBlock {
return [ 'query', 'queryId', 'postId' ];
}
/**
* Enqueue frontend assets for this block, just in time for rendering.
*
* @param array $attributes Any attributes that currently are available from the block.
*/
protected function enqueue_assets( array $attributes ) {
parent::enqueue_assets( $attributes );
if ( wc_current_theme_is_fse_theme() ) {
add_action(
'wp_enqueue_scripts',
array( $this, 'dequeue_add_to_cart_scripts' )
);
} else {
$this->dequeue_add_to_cart_scripts();
}
}
/**
* Dequeue the add-to-cart script.
* The block uses Interactivity API, it isn't necessary enqueue the add-to-cart script.
*/
public function dequeue_add_to_cart_scripts() {
wp_dequeue_script( 'wc-add-to-cart' );
}
/**
* Include and render the block.
*
@@ -75,23 +72,24 @@ class ProductButton extends AbstractBlock {
* @return string Rendered block type output.
*/
protected function render( $attributes, $content, $block ) {
if ( ! empty( $content ) ) {
parent::register_block_type_assets();
$this->register_chunk_translations( [ $this->block_name ] );
return $content;
}
$post_id = $block->context['postId'];
$post_id = isset( $block->context['postId'] ) ? $block->context['postId'] : '';
$product = wc_get_product( $post_id );
if ( $product ) {
$number_of_items_in_cart = $this->get_cart_item_quantities_by_product_id( $product->get_id() );
$more_than_one_item = $number_of_items_in_cart > 0;
$initial_product_text = $more_than_one_item ? sprintf(
/* translators: %s: product number. */
__( '%s in cart', 'woocommerce' ),
$number_of_items_in_cart
) : $product->add_to_cart_text();
$cart_redirect_after_add = get_option( 'woocommerce_cart_redirect_after_add' ) === 'yes';
$ajax_add_to_cart_enabled = get_option( 'woocommerce_enable_ajax_add_to_cart' ) === 'yes';
$is_ajax_button = $ajax_add_to_cart_enabled && ! $cart_redirect_after_add && $product->supports( 'ajax_add_to_cart' ) && $product->is_purchasable() && $product->is_in_stock();
$html_element = $is_ajax_button ? 'button' : 'a';
$styles_and_classes = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes );
$text_align_styles_and_classes = StyleAttributesUtils::get_text_align_class_and_style( $attributes );
$classname = isset( $attributes['className'] ) ? $attributes['className'] : '';
$classname = $attributes['className'] ?? '';
$custom_width_classes = isset( $attributes['width'] ) ? 'has-custom-width wp-block-button__width-' . $attributes['width'] : '';
$html_classes = implode(
' ',
@@ -107,6 +105,40 @@ class ProductButton extends AbstractBlock {
)
)
);
wc_store(
array(
'state' => array(
'woocommerce' => array(
'inTheCartText' => sprintf(
/* translators: %s: product number. */
__( '%s in cart', 'woocommerce' ),
'###'
),
),
),
)
);
$default_quantity = 1;
$context = array(
'woocommerce' => array(
/**
* Filters the change the quantity to add to cart.
*
* @since 10.9.0
* @param number $default_quantity The default quantity.
* @param number $product_id The product id.
*/
'quantityToAdd' => apply_filters( 'woocommerce_add_to_cart_quantity', $default_quantity, $product->get_id() ),
'productId' => $product->get_id(),
'addToCartText' => null !== $product->add_to_cart_text() ? $product->add_to_cart_text() : __( 'Add to cart', 'woocommerce' ),
'temporaryNumberOfItems' => $number_of_items_in_cart,
'animationStatus' => 'IDLE',
),
);
/**
* Allow filtering of the add to cart button arguments.
*
@@ -130,6 +162,25 @@ class ProductButton extends AbstractBlock {
$args['attributes']['aria-label'] = wp_strip_all_tags( $args['attributes']['aria-label'] );
}
if ( isset( WC()->cart ) && ! WC()->cart->is_empty() ) {
$this->prevent_cache();
}
$div_directives = 'data-wc-context=\'' . wp_json_encode( $context, JSON_NUMERIC_CHECK ) . '\'';
$button_directives = '
data-wc-on--click="actions.woocommerce.addToCart"
data-wc-class--loading="context.woocommerce.isLoading"
';
$span_button_directives = '
data-wc-text="selectors.woocommerce.addToCartText"
data-wc-class--wc-block-slide-in="selectors.woocommerce.slideInAnimation"
data-wc-class--wc-block-slide-out="selectors.woocommerce.slideOutAnimation"
data-wc-effect="effects.woocommerce.startAnimation"
data-wc-on--animationend="actions.woocommerce.handleAnimationEnd"
';
/**
* Filters the add to cart button class.
*
@@ -139,22 +190,83 @@ class ProductButton extends AbstractBlock {
*/
return apply_filters(
'woocommerce_loop_add_to_cart_link',
sprintf(
'<div class="wp-block-button wc-block-components-product-button %1$s %2$s">
<%3$s href="%4$s" class="%5$s" style="%6$s" %7$s>%8$s</%3$s>
strtr(
'<div data-wc-interactive class="wp-block-button wc-block-components-product-button {classes} {custom_classes}"
{div_directives}
>
<{html_element}
href="{add_to_cart_url}"
class="{button_classes}"
style="{button_styles}"
{attributes}
{button_directives}
>
<span {span_button_directives}> {add_to_cart_text} </span>
</{html_element}>
{view_cart_html}
</div>',
esc_attr( $text_align_styles_and_classes['class'] ?? '' ),
esc_attr( $classname . ' ' . $custom_width_classes ),
$html_element,
esc_url( $product->add_to_cart_url() ),
isset( $args['class'] ) ? esc_attr( $args['class'] ) : '',
esc_attr( $styles_and_classes['styles'] ),
isset( $args['attributes'] ) ? wc_implode_html_attributes( $args['attributes'] ) : '',
esc_html( $product->add_to_cart_text() )
array(
'{classes}' => esc_attr( $text_align_styles_and_classes['class'] ?? '' ),
'{custom_classes}' => esc_attr( $classname . ' ' . $custom_width_classes ),
'{html_element}' => $html_element,
'{add_to_cart_url}' => esc_url( $product->add_to_cart_url() ),
'{button_classes}' => isset( $args['class'] ) ? esc_attr( $args['class'] ) : '',
'{button_styles}' => esc_attr( $styles_and_classes['styles'] ),
'{attributes}' => isset( $args['attributes'] ) ? wc_implode_html_attributes( $args['attributes'] ) : '',
'{add_to_cart_text}' => esc_html( $initial_product_text ),
'{div_directives}' => $is_ajax_button ? $div_directives : '',
'{button_directives}' => $is_ajax_button ? $button_directives : '',
'{span_button_directives}' => $is_ajax_button ? $span_button_directives : '',
'{view_cart_html}' => $is_ajax_button ? $this->get_view_cart_html() : '',
)
),
$product,
$args
);
}
}
/**
* Get the number of items in the cart for a given product id.
*
* @param number $product_id The product id.
* @return number The number of items in the cart.
*/
private function get_cart_item_quantities_by_product_id( $product_id ) {
if ( ! isset( WC()->cart ) ) {
return 0;
}
$cart = WC()->cart->get_cart_item_quantities();
return isset( $cart[ $product_id ] ) ? $cart[ $product_id ] : 0;
}
/**
* Prevent caching on certain pages
*/
private function prevent_cache() {
\WC_Cache_Helper::set_nocache_constants();
nocache_headers();
}
/**
* Get the view cart link html.
*
* @return string The view cart html.
*/
private function get_view_cart_html() {
return sprintf(
'<span hidden data-wc-bind--hidden="!selectors.woocommerce.displayViewCart">
<a
href="%1$s"
class="added_to_cart wc_forward"
title="%2$s"
>
%2$s
</a>
</span>',
wc_get_cart_url(),
__( 'View cart', 'woocommerce' )
);
}
}

View File

@@ -22,11 +22,12 @@ class ProductCategories extends AbstractDynamicBlock {
* @var array
*/
protected $defaults = array(
'hasCount' => true,
'hasImage' => false,
'hasEmpty' => false,
'isDropdown' => false,
'isHierarchical' => true,
'hasCount' => true,
'hasImage' => false,
'hasEmpty' => false,
'isDropdown' => false,
'isHierarchical' => true,
'showChildrenOnly' => false,
);
/**
@@ -38,17 +39,18 @@ class ProductCategories extends AbstractDynamicBlock {
return array_merge(
parent::get_block_type_attributes(),
array(
'align' => $this->get_schema_align(),
'className' => $this->get_schema_string(),
'hasCount' => $this->get_schema_boolean( true ),
'hasImage' => $this->get_schema_boolean( false ),
'hasEmpty' => $this->get_schema_boolean( false ),
'isDropdown' => $this->get_schema_boolean( false ),
'isHierarchical' => $this->get_schema_boolean( true ),
'textColor' => $this->get_schema_string(),
'fontSize' => $this->get_schema_string(),
'lineHeight' => $this->get_schema_string(),
'style' => array( 'type' => 'object' ),
'align' => $this->get_schema_align(),
'className' => $this->get_schema_string(),
'hasCount' => $this->get_schema_boolean( true ),
'hasImage' => $this->get_schema_boolean( false ),
'hasEmpty' => $this->get_schema_boolean( false ),
'isDropdown' => $this->get_schema_boolean( false ),
'isHierarchical' => $this->get_schema_boolean( true ),
'showChildrenOnly' => $this->get_schema_boolean( false ),
'textColor' => $this->get_schema_string(),
'fontSize' => $this->get_schema_string(),
'lineHeight' => $this->get_schema_string(),
'style' => array( 'type' => 'object' ),
)
);
}
@@ -134,15 +136,30 @@ class ProductCategories extends AbstractDynamicBlock {
* @return array
*/
protected function get_categories( $attributes ) {
$hierarchical = wc_string_to_bool( $attributes['isHierarchical'] );
$categories = get_terms(
'product_cat',
[
'hide_empty' => ! $attributes['hasEmpty'],
'pad_counts' => true,
'hierarchical' => true,
]
);
$hierarchical = wc_string_to_bool( $attributes['isHierarchical'] );
$children_only = wc_string_to_bool( $attributes['showChildrenOnly'] ) && is_product_category();
if ( $children_only ) {
$term_id = get_queried_object_id();
$categories = get_terms(
'product_cat',
[
'hide_empty' => ! $attributes['hasEmpty'],
'pad_counts' => true,
'hierarchical' => true,
'child_of' => $term_id,
]
);
} else {
$categories = get_terms(
'product_cat',
[
'hide_empty' => ! $attributes['hasEmpty'],
'pad_counts' => true,
'hierarchical' => true,
]
);
}
if ( ! is_array( $categories ) || empty( $categories ) ) {
return [];
@@ -157,17 +174,17 @@ class ProductCategories extends AbstractDynamicBlock {
}
);
}
return $hierarchical ? $this->build_category_tree( $categories ) : $categories;
return $hierarchical ? $this->build_category_tree( $categories, $children_only ) : $categories;
}
/**
* Build hierarchical tree of categories.
*
* @param array $categories List of terms.
* @param bool $children_only Is the block rendering only the children of the current category.
* @return array
*/
protected function build_category_tree( $categories ) {
protected function build_category_tree( $categories, $children_only ) {
$categories_by_parent = [];
foreach ( $categories as $category ) {
@@ -177,8 +194,9 @@ class ProductCategories extends AbstractDynamicBlock {
$categories_by_parent[ 'cat-' . $category->parent ][] = $category;
}
$tree = $categories_by_parent['cat-0'];
unset( $categories_by_parent['cat-0'] );
$parent_id = $children_only ? get_queried_object_id() : 0;
$tree = $categories_by_parent[ 'cat-' . $parent_id ]; // these are top level categories. So all parents.
unset( $categories_by_parent[ 'cat-' . $parent_id ] );
foreach ( $tree as $category ) {
if ( ! empty( $categories_by_parent[ 'cat-' . $category->term_id ] ) ) {

View File

@@ -0,0 +1,959 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use WP_Query;
/**
* ProductCollection class.
*/
class ProductCollection extends AbstractBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'product-collection';
/**
* The Block with its attributes before it gets rendered
*
* @var array
*/
protected $parsed_block;
/**
* All query args from WP_Query.
*
* @var array
*/
protected $valid_query_vars;
/**
* All the query args related to the filter by attributes block.
*
* @var array
*/
protected $attributes_filter_query_args = array();
/**
* Orderby options not natively supported by WordPress REST API
*
* @var array
*/
protected $custom_order_opts = array( 'popularity', 'rating' );
/**
* Get the frontend script handle for this block type.
*
* @param string $key Data to get, or default to everything.
*/
protected function get_block_type_script( $key = null ) {
return null;
}
/**
* Get the frontend style handle for this block type.
*
* @return null
*/
protected function get_block_type_style() {
return null;
}
/**
* Initialize this block type.
*
* - Hook into WP lifecycle.
* - Register the block with WordPress.
* - Hook into pre_render_block to update the query.
*/
protected function initialize() {
parent::initialize();
// Update query for frontend rendering.
add_filter(
'query_loop_block_query_vars',
array( $this, 'build_frontend_query' ),
10,
3
);
add_filter(
'pre_render_block',
array( $this, 'add_support_for_filter_blocks' ),
10,
2
);
// Update the query for Editor.
add_filter( 'rest_product_query', array( $this, 'update_rest_query_in_editor' ), 10, 2 );
// Extend allowed `collection_params` for the REST API.
add_filter( 'rest_product_collection_params', array( $this, 'extend_rest_query_allowed_params' ), 10, 1 );
// Interactivity API: Add navigation directives to the product collection block.
add_filter( 'render_block_woocommerce/product-collection', array( $this, 'add_navigation_id_directive' ), 10, 3 );
add_filter( 'render_block_core/query-pagination', array( $this, 'add_navigation_link_directives' ), 10, 3 );
}
/**
* Mark the Product Collection as an interactive region so it can be updated
* during client-side navigation.
*
* @param string $block_content The block content.
* @param array $block The full block, including name and attributes.
* @param \WP_Block $instance The block instance.
*/
public function add_navigation_id_directive( $block_content, $block, $instance ) {
$is_product_collection_block = $block['attrs']['query']['isProductCollectionBlock'] ?? false;
if ( $is_product_collection_block ) {
// Enqueue the Interactivity API runtime.
wp_enqueue_script( 'wc-interactivity' );
$p = new \WP_HTML_Tag_Processor( $block_content );
// Add `data-wc-navigation-id to the query block.
if ( $p->next_tag( array( 'class_name' => 'wp-block-woocommerce-product-collection' ) ) ) {
$p->set_attribute(
'data-wc-navigation-id',
'wc-product-collection-' . $this->parsed_block['attrs']['queryId']
);
$p->set_attribute( 'data-wc-interactive', true );
$block_content = $p->get_updated_html();
}
}
return $block_content;
}
/**
* Add interactive links to all anchors inside the Query Pagination block.
*
* @param string $block_content The block content.
* @param array $block The full block, including name and attributes.
* @param \WP_Block $instance The block instance.
*/
public function add_navigation_link_directives( $block_content, $block, $instance ) {
$is_product_collection_block = $instance->context['query']['isProductCollectionBlock'] ?? false;
if (
$is_product_collection_block &&
$instance->context['queryId'] === $this->parsed_block['attrs']['queryId']
) {
$p = new \WP_HTML_Tag_Processor( $block_content );
$p->next_tag( array( 'class_name' => 'wp-block-query-pagination' ) );
while ( $p->next_tag( 'a' ) ) {
$class_attr = $p->get_attribute( 'class' );
$class_list = preg_split( '/\s+/', $class_attr );
$is_previous = in_array( 'wp-block-query-pagination-previous', $class_list, true );
$is_next = in_array( 'wp-block-query-pagination-next', $class_list, true );
$is_previous_or_next = $is_previous || $is_next;
$navigation_link_payload = array(
'prefetch' => $is_previous_or_next,
'scroll' => false,
);
$p->set_attribute(
'data-wc-navigation-link',
wp_json_encode( $navigation_link_payload )
);
if ( $is_previous ) {
$p->set_attribute( 'key', 'pagination-previous' );
} elseif ( $is_next ) {
$p->set_attribute( 'key', 'pagination-next' );
}
}
$block_content = $p->get_updated_html();
}
return $block_content;
}
/**
* Extra data passed through from server to client for block.
*
* @param array $attributes Any attributes that currently are available from the block.
* Note, this will be empty in the editor context when the block is
* not in the post content on editor load.
*/
protected function enqueue_data( array $attributes = [] ) {
parent::enqueue_data( $attributes );
// The `loop_shop_per_page` filter can be found in WC_Query::product_query().
// phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
$this->asset_data_registry->add( 'loopShopPerPage', apply_filters( 'loop_shop_per_page', wc_get_default_products_per_row() * wc_get_default_product_rows_per_page() ), true );
}
/**
* Update the query for the product query block in Editor.
*
* @param array $args Query args.
* @param WP_REST_Request $request Request.
*/
public function update_rest_query_in_editor( $args, $request ): array {
// Only update the query if this is a product collection block.
$is_product_collection_block = $request->get_param( 'isProductCollectionBlock' );
if ( ! $is_product_collection_block ) {
return $args;
}
$orderby = $request->get_param( 'orderBy' );
$on_sale = $request->get_param( 'woocommerceOnSale' ) === 'true';
$stock_status = $request->get_param( 'woocommerceStockStatus' );
$product_attributes = $request->get_param( 'woocommerceAttributes' );
$handpicked_products = $request->get_param( 'woocommerceHandPickedProducts' );
$args['author'] = $request->get_param( 'author' ) ?? '';
return $this->get_final_query_args(
$args,
array(
'orderby' => $orderby,
'on_sale' => $on_sale,
'stock_status' => $stock_status,
'product_attributes' => $product_attributes,
'handpicked_products' => $handpicked_products,
)
);
}
/**
* Add support for filter blocks:
* - Price filter block
* - Attributes filter block
* - Rating filter block
* - In stock filter block etc.
*
* @param array $pre_render The pre-rendered block.
* @param array $parsed_block The parsed block.
*/
public function add_support_for_filter_blocks( $pre_render, $parsed_block ) {
$is_product_collection_block = $parsed_block['attrs']['query']['isProductCollectionBlock'] ?? false;
if ( ! $is_product_collection_block ) {
return;
}
$this->parsed_block = $parsed_block;
$this->asset_data_registry->add( 'hasFilterableProducts', true, true );
/**
* It enables the page to refresh when a filter is applied, ensuring that the product collection block,
* which is a server-side rendered (SSR) block, retrieves the products that match the filters.
*/
$this->asset_data_registry->add( 'isRenderingPhpTemplate', true, true );
}
/**
* Return a custom query based on attributes, filters and global WP_Query.
*
* @param WP_Query $query The WordPress Query.
* @param WP_Block $block The block being rendered.
* @param int $page The page number.
*
* @return array
*/
public function build_frontend_query( $query, $block, $page ) {
// If not in context of product collection block, return the query as is.
$is_product_collection_block = $block->context['query']['isProductCollectionBlock'] ?? false;
if ( ! $is_product_collection_block ) {
return $query;
}
$block_context_query = $block->context['query'];
// phpcs:ignore WordPress.DB.SlowDBQuery
$block_context_query['tax_query'] = ! empty( $query['tax_query'] ) ? $query['tax_query'] : array();
return $this->get_final_frontend_query( $block_context_query, $page );
}
/**
* Get the final query arguments for the frontend.
*
* @param array $query The query arguments.
* @param int $page The page number.
* @param bool $is_exclude_applied_filters Whether to exclude the applied filters or not.
*/
private function get_final_frontend_query( $query, $page = 1, $is_exclude_applied_filters = false ) {
$offset = $query['offset'] ?? 0;
$per_page = $query['perPage'] ?? 9;
$common_query_values = array(
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
'meta_query' => array(),
'posts_per_page' => $query['perPage'],
'order' => $query['order'],
'offset' => ( $per_page * ( $page - 1 ) ) + $offset,
'post__in' => array(),
'post_status' => 'publish',
'post_type' => 'product',
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
'tax_query' => array(),
'paged' => $page,
's' => $query['search'],
'author' => $query['author'] ?? '',
);
$is_on_sale = $query['woocommerceOnSale'] ?? false;
$taxonomies_query = $this->get_filter_by_taxonomies_query( $query['tax_query'] ?? [] );
$handpicked_products = $query['woocommerceHandPickedProducts'] ?? [];
$final_query = $this->get_final_query_args(
$common_query_values,
array(
'on_sale' => $is_on_sale,
'stock_status' => $query['woocommerceStockStatus'],
'orderby' => $query['orderBy'],
'product_attributes' => $query['woocommerceAttributes'],
'taxonomies_query' => $taxonomies_query,
'handpicked_products' => $handpicked_products,
),
$is_exclude_applied_filters
);
return $final_query;
}
/**
* Get final query args based on provided values
*
* @param array $common_query_values Common query values.
* @param array $query Query from block context.
* @param bool $is_exclude_applied_filters Whether to exclude the applied filters or not.
*/
private function get_final_query_args( $common_query_values, $query, $is_exclude_applied_filters = false ) {
$handpicked_products = $query['handpicked_products'] ?? [];
$orderby_query = $query['orderby'] ? $this->get_custom_orderby_query( $query['orderby'] ) : [];
$on_sale_query = $this->get_on_sale_products_query( $query['on_sale'] );
$stock_query = $this->get_stock_status_query( $query['stock_status'] );
$visibility_query = is_array( $query['stock_status'] ) ? $this->get_product_visibility_query( $stock_query ) : [];
$attributes_query = $this->get_product_attributes_query( $query['product_attributes'] );
$taxonomies_query = $query['taxonomies_query'] ?? [];
$tax_query = $this->merge_tax_queries( $visibility_query, $attributes_query, $taxonomies_query );
// We exclude applied filters to generate product ids for the filter blocks.
$applied_filters_query = $is_exclude_applied_filters ? [] : $this->get_queries_by_applied_filters();
$merged_query = $this->merge_queries( $common_query_values, $orderby_query, $on_sale_query, $stock_query, $tax_query, $applied_filters_query );
return $this->filter_query_to_only_include_ids( $merged_query, $handpicked_products );
}
/**
* Extends allowed `collection_params` for the REST API
*
* By itself, the REST API doesn't accept custom `orderby` values,
* even if they are supported by a custom post type.
*
* @param array $params A list of allowed `orderby` values.
*
* @return array
*/
public function extend_rest_query_allowed_params( $params ) {
$original_enum = isset( $params['orderby']['enum'] ) ? $params['orderby']['enum'] : array();
$params['orderby']['enum'] = array_unique( array_merge( $original_enum, $this->custom_order_opts ) );
return $params;
}
/**
* Merge in the first parameter the keys "post_in", "meta_query" and "tax_query" of the second parameter.
*
* @param array[] ...$queries Query arrays to be merged.
* @return array
*/
private function merge_queries( ...$queries ) {
$merged_query = array_reduce(
$queries,
function( $acc, $query ) {
if ( ! is_array( $query ) ) {
return $acc;
}
// If the $query doesn't contain any valid query keys, we unpack/spread it then merge.
if ( empty( array_intersect( $this->get_valid_query_vars(), array_keys( $query ) ) ) ) {
return $this->merge_queries( $acc, ...array_values( $query ) );
}
return $this->array_merge_recursive_replace_non_array_properties( $acc, $query );
},
array()
);
/**
* If there are duplicated items in post__in, it means that we need to
* use the intersection of the results, which in this case, are the
* duplicated items.
*/
if (
! empty( $merged_query['post__in'] ) &&
count( $merged_query['post__in'] ) > count( array_unique( $merged_query['post__in'] ) )
) {
$merged_query['post__in'] = array_unique(
array_diff(
$merged_query['post__in'],
array_unique( $merged_query['post__in'] )
)
);
}
return $merged_query;
}
/**
* Return query params to support custom sort values
*
* @param string $orderby Sort order option.
*
* @return array
*/
private function get_custom_orderby_query( $orderby ) {
if ( ! in_array( $orderby, $this->custom_order_opts, true ) ) {
return array( 'orderby' => $orderby );
}
$meta_keys = array(
'popularity' => 'total_sales',
'rating' => '_wc_average_rating',
);
return array(
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
'meta_key' => $meta_keys[ $orderby ],
'orderby' => 'meta_value_num',
);
}
/**
* Return a query for on sale products.
*
* @param bool $is_on_sale Whether to query for on sale products.
*
* @return array
*/
private function get_on_sale_products_query( $is_on_sale ) {
if ( ! $is_on_sale ) {
return array();
}
return array(
'post__in' => wc_get_product_ids_on_sale(),
);
}
/**
* Return or initialize $valid_query_vars.
*
* @return array
*/
private function get_valid_query_vars() {
if ( ! empty( $this->valid_query_vars ) ) {
return $this->valid_query_vars;
}
$valid_query_vars = array_keys( ( new WP_Query() )->fill_query_vars( array() ) );
$this->valid_query_vars = array_merge(
$valid_query_vars,
// fill_query_vars doesn't include these vars so we need to add them manually.
array(
'date_query',
'exact',
'ignore_sticky_posts',
'lazy_load_term_meta',
'meta_compare_key',
'meta_compare',
'meta_query',
'meta_type_key',
'meta_type',
'nopaging',
'offset',
'order',
'orderby',
'page',
'post_type',
'posts_per_page',
'suppress_filters',
'tax_query',
)
);
return $this->valid_query_vars;
}
/**
* Merge two array recursively but replace the non-array values instead of
* merging them. The merging strategy:
*
* - If keys from merge array doesn't exist in the base array, create them.
* - For array items with numeric keys, we merge them as normal.
* - For array items with string keys:
*
* - If the value isn't array, we'll use the value comming from the merge array.
* $base = ['orderby' => 'date']
* $new = ['orderby' => 'meta_value_num']
* Result: ['orderby' => 'meta_value_num']
*
* - If the value is array, we'll use recursion to merge each key.
* $base = ['meta_query' => [
* [
* 'key' => '_stock_status',
* 'compare' => 'IN'
* 'value' => ['instock', 'onbackorder']
* ]
* ]]
* $new = ['meta_query' => [
* [
* 'relation' => 'AND',
* [...<max_price_query>],
* [...<min_price_query>],
* ]
* ]]
* Result: ['meta_query' => [
* [
* 'key' => '_stock_status',
* 'compare' => 'IN'
* 'value' => ['instock', 'onbackorder']
* ],
* [
* 'relation' => 'AND',
* [...<max_price_query>],
* [...<min_price_query>],
* ]
* ]]
*
* $base = ['post__in' => [1, 2, 3, 4, 5]]
* $new = ['post__in' => [3, 4, 5, 6, 7]]
* Result: ['post__in' => [1, 2, 3, 4, 5, 3, 4, 5, 6, 7]]
*
* @param array $base First array.
* @param array $new Second array.
*/
private function array_merge_recursive_replace_non_array_properties( $base, $new ) {
foreach ( $new as $key => $value ) {
if ( is_numeric( $key ) ) {
$base[] = $value;
} else {
if ( is_array( $value ) ) {
if ( ! isset( $base[ $key ] ) ) {
$base[ $key ] = array();
}
$base[ $key ] = $this->array_merge_recursive_replace_non_array_properties( $base[ $key ], $value );
} else {
$base[ $key ] = $value;
}
}
}
return $base;
}
/**
* Return a query for products depending on their stock status.
*
* @param array $stock_statuses An array of acceptable stock statuses.
* @return array
*/
private function get_stock_status_query( $stock_statuses ) {
if ( ! is_array( $stock_statuses ) ) {
return array();
}
$stock_status_options = array_keys( wc_get_product_stock_status_options() );
/**
* If all available stock status are selected, we don't need to add the
* meta query for stock status.
*/
if (
count( $stock_statuses ) === count( $stock_status_options ) &&
array_diff( $stock_statuses, $stock_status_options ) === array_diff( $stock_status_options, $stock_statuses )
) {
return array();
}
/**
* If all stock statuses are selected except 'outofstock', we use the
* product visibility query to filter out out of stock products.
*
* @see get_product_visibility_query()
*/
$diff = array_diff( $stock_status_options, $stock_statuses );
if ( count( $diff ) === 1 && in_array( 'outofstock', $diff, true ) ) {
return array();
}
return array(
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
'meta_query' => array(
array(
'key' => '_stock_status',
'value' => (array) $stock_statuses,
'compare' => 'IN',
),
),
);
}
/**
* Return a query for product visibility depending on their stock status.
*
* @param array $stock_query Stock status query.
*
* @return array Tax query for product visibility.
*/
private function get_product_visibility_query( $stock_query ) {
$product_visibility_terms = wc_get_product_visibility_term_ids();
$product_visibility_not_in = array( is_search() ? $product_visibility_terms['exclude-from-search'] : $product_visibility_terms['exclude-from-catalog'] );
// Hide out of stock products.
if ( empty( $stock_query ) && 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ) {
$product_visibility_not_in[] = $product_visibility_terms['outofstock'];
}
return array(
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
'tax_query' => array(
array(
'taxonomy' => 'product_visibility',
'field' => 'term_taxonomy_id',
'terms' => $product_visibility_not_in,
'operator' => 'NOT IN',
),
),
);
}
/**
* Merge tax_queries from various queries.
*
* @param array ...$queries Query arrays to be merged.
* @return array
*/
private function merge_tax_queries( ...$queries ) {
$tax_query = [];
foreach ( $queries as $query ) {
if ( ! empty( $query['tax_query'] ) ) {
$tax_query = array_merge( $tax_query, $query['tax_query'] );
}
}
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
return [ 'tax_query' => $tax_query ];
}
/**
* Return the `tax_query` for the requested attributes
*
* @param array $attributes Attributes and their terms.
*
* @return array
*/
private function get_product_attributes_query( $attributes = array() ) {
if ( empty( $attributes ) ) {
return array();
}
$grouped_attributes = array_reduce(
$attributes,
function ( $carry, $item ) {
$taxonomy = sanitize_title( $item['taxonomy'] );
if ( ! key_exists( $taxonomy, $carry ) ) {
$carry[ $taxonomy ] = array(
'field' => 'term_id',
'operator' => 'IN',
'taxonomy' => $taxonomy,
'terms' => array( $item['termId'] ),
);
} else {
$carry[ $taxonomy ]['terms'][] = $item['termId'];
}
return $carry;
},
array()
);
return array(
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
'tax_query' => array_values( $grouped_attributes ),
);
}
/**
* Return a query to filter products by taxonomies (product categories, product tags, etc.)
*
* For example:
* User could provide "Product Categories" using "Filters" ToolsPanel available in Inspector Controls.
* We use this function to extract its query from $tax_query.
*
* For example, this is how the query for product categories will look like in $tax_query array:
* Array
* (
* [taxonomy] => product_cat
* [terms] => Array
* (
* [0] => 36
* )
* )
*
* For product tags, taxonomy would be "product_tag"
*
* @param array $tax_query Query to filter products by taxonomies.
* @return array Query to filter products by taxonomies.
*/
private function get_filter_by_taxonomies_query( $tax_query ): array {
if ( ! is_array( $tax_query ) ) {
return [];
}
/**
* Get an array of taxonomy names associated with the "product" post type because
* we also want to include custom taxonomies associated with the "product" post type.
*/
$product_taxonomies = get_taxonomies( [ 'object_type' => [ 'product' ] ], 'names' );
$result = array_filter(
$tax_query,
function( $item ) use ( $product_taxonomies ) {
return isset( $item['taxonomy'] ) && in_array( $item['taxonomy'], $product_taxonomies, true );
}
);
// phpcs:ignore WordPress.DB.SlowDBQuery
return ! empty( $result ) ? [ 'tax_query' => $result ] : [];
}
/**
* Apply the query only to a subset of products
*
* @param array $query The query.
* @param array $ids Array of selected product ids.
*
* @return array
*/
private function filter_query_to_only_include_ids( $query, $ids ) {
if ( ! empty( $ids ) ) {
$query['post__in'] = empty( $query['post__in'] ) ?
$ids : array_intersect( $ids, $query['post__in'] );
}
return $query;
}
/**
* Return queries that are generated by query args.
*
* @return array
*/
private function get_queries_by_applied_filters() {
return array(
'price_filter' => $this->get_filter_by_price_query(),
'attributes_filter' => $this->get_filter_by_attributes_query(),
'stock_status_filter' => $this->get_filter_by_stock_status_query(),
'rating_filter' => $this->get_filter_by_rating_query(),
);
}
/**
* Return a query that filters products by price.
*
* @return array
*/
private function get_filter_by_price_query() {
$min_price = get_query_var( PriceFilter::MIN_PRICE_QUERY_VAR );
$max_price = get_query_var( PriceFilter::MAX_PRICE_QUERY_VAR );
$max_price_query = empty( $max_price ) ? array() : [
'key' => '_price',
'value' => $max_price,
'compare' => '<',
'type' => 'numeric',
];
$min_price_query = empty( $min_price ) ? array() : [
'key' => '_price',
'value' => $min_price,
'compare' => '>=',
'type' => 'numeric',
];
if ( empty( $min_price_query ) && empty( $max_price_query ) ) {
return array();
}
return array(
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
'meta_query' => array(
array(
'relation' => 'AND',
$max_price_query,
$min_price_query,
),
),
);
}
/**
* Return a query that filters products by attributes.
*
* @return array
*/
private function get_filter_by_attributes_query() {
$attributes_filter_query_args = $this->get_filter_by_attributes_query_vars();
$queries = array_reduce(
$attributes_filter_query_args,
function( $acc, $query_args ) {
$attribute_name = $query_args['filter'];
$attribute_query_type = $query_args['query_type'];
$attribute_value = get_query_var( $attribute_name );
$attribute_query = get_query_var( $attribute_query_type );
if ( empty( $attribute_value ) ) {
return $acc;
}
// It is necessary explode the value because $attribute_value can be a string with multiple values (e.g. "red,blue").
$attribute_value = explode( ',', $attribute_value );
$acc[] = array(
'taxonomy' => str_replace( AttributeFilter::FILTER_QUERY_VAR_PREFIX, 'pa_', $attribute_name ),
'field' => 'slug',
'terms' => $attribute_value,
'operator' => 'and' === $attribute_query ? 'AND' : 'IN',
);
return $acc;
},
array()
);
if ( empty( $queries ) ) {
return array();
}
return array(
// phpcs:ignore WordPress.DB.SlowDBQuery
'tax_query' => array(
array(
'relation' => 'AND',
$queries,
),
),
);
}
/**
* Get all the query args related to the filter by attributes block.
*
* @return array
* [color] => Array
* (
* [filter] => filter_color
* [query_type] => query_type_color
* )
*
* [size] => Array
* (
* [filter] => filter_size
* [query_type] => query_type_size
* )
* )
*/
private function get_filter_by_attributes_query_vars() {
if ( ! empty( $this->attributes_filter_query_args ) ) {
return $this->attributes_filter_query_args;
}
$this->attributes_filter_query_args = array_reduce(
wc_get_attribute_taxonomies(),
function( $acc, $attribute ) {
$acc[ $attribute->attribute_name ] = array(
'filter' => AttributeFilter::FILTER_QUERY_VAR_PREFIX . $attribute->attribute_name,
'query_type' => AttributeFilter::QUERY_TYPE_QUERY_VAR_PREFIX . $attribute->attribute_name,
);
return $acc;
},
array()
);
return $this->attributes_filter_query_args;
}
/**
* Return a query that filters products by stock status.
*
* @return array
*/
private function get_filter_by_stock_status_query() {
$filter_stock_status_values = get_query_var( StockFilter::STOCK_STATUS_QUERY_VAR );
if ( empty( $filter_stock_status_values ) ) {
return array();
}
$filtered_stock_status_values = array_filter(
explode( ',', $filter_stock_status_values ),
function( $stock_status ) {
return in_array( $stock_status, StockFilter::get_stock_status_query_var_values(), true );
}
);
if ( empty( $filtered_stock_status_values ) ) {
return array();
}
return array(
// Ignoring the warning of not using meta queries.
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
'meta_query' => array(
array(
'key' => '_stock_status',
'value' => $filtered_stock_status_values,
'operator' => 'IN',
),
),
);
}
/**
* Return a query that filters products by rating.
*
* @return array
*/
private function get_filter_by_rating_query() {
$filter_rating_values = get_query_var( RatingFilter::RATING_QUERY_VAR );
if ( empty( $filter_rating_values ) ) {
return array();
}
$parsed_filter_rating_values = explode( ',', $filter_rating_values );
$product_visibility_terms = wc_get_product_visibility_term_ids();
if ( empty( $parsed_filter_rating_values ) || empty( $product_visibility_terms ) ) {
return array();
}
$rating_terms = array_map(
function( $rating ) use ( $product_visibility_terms ) {
return $product_visibility_terms[ 'rated-' . $rating ];
},
$parsed_filter_rating_values
);
return array(
// phpcs:ignore WordPress.DB.SlowDBQuery
'tax_query' => array(
array(
'field' => 'term_taxonomy_id',
'taxonomy' => 'product_visibility',
'terms' => $rating_terms,
'operator' => 'IN',
'rating_filter' => true,
),
),
);
}
}

View File

@@ -39,10 +39,13 @@ class ProductDetails extends AbstractBlock {
return sprintf(
'<div class="wp-block-woocommerce-product-details %1$s %2$s">
%3$s
<div style="%3$s">
%4$s
</div>
</div>',
esc_attr( $classes_and_styles['classes'] ),
esc_attr( $classname ),
esc_attr( $classes_and_styles['styles'] ),
$tabs
);
}
@@ -54,7 +57,7 @@ class ProductDetails extends AbstractBlock {
*/
protected function render_tabs() {
ob_start();
rewind_posts();
while ( have_posts() ) {
the_post();
woocommerce_output_product_data_tabs();

View File

@@ -0,0 +1,23 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* ProductGallery class.
*/
class ProductGallery extends AbstractBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'product-gallery';
/**
* Get the frontend style handle for this block type.
*
* @return null
*/
protected function get_block_type_style() {
return null;
}
}

View File

@@ -0,0 +1,80 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
/**
* ProductGalleryLargeImage class.
*/
class ProductGalleryLargeImage extends AbstractBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'product-gallery-large-image';
/**
* It isn't necessary register block assets because it is a server side block.
*/
protected function register_block_type_assets() {
return null;
}
/**
* Register the context
*
* @return string[]
*/
protected function get_block_type_uses_context() {
return [ 'postId' ];
}
/**
* Include and render the block.
*
* @param array $attributes Block attributes. Default empty array.
* @param string $content Block content. Default empty string.
* @param WP_Block $block Block instance.
* @return string Rendered block type output.
*/
protected function render( $attributes, $content, $block ) {
$post_id = $block->context['postId'];
if ( ! isset( $post_id ) ) {
return '';
}
global $product;
$previous_product = $product;
$product = wc_get_product( $post_id );
if ( ! $product instanceof \WC_Product ) {
$product = $previous_product;
return '';
}
if ( class_exists( 'WC_Frontend_Scripts' ) ) {
$frontend_scripts = new \WC_Frontend_Scripts();
$frontend_scripts::load_scripts();
}
ob_start();
woocommerce_show_product_sale_flash();
$sale_badge_html = ob_get_clean();
ob_start();
remove_action( 'woocommerce_product_thumbnails', 'woocommerce_show_product_thumbnails', 20 );
woocommerce_show_product_images();
$product_image_gallery_html = ob_get_clean();
add_action( 'woocommerce_product_thumbnails', 'woocommerce_show_product_thumbnails', 20 );
$product = $previous_product;
$classname = $attributes['className'] ?? '';
return sprintf(
'<div class="wp-block-woocommerce-product-gallery-large-image %1$s">%2$s %3$s</div>',
esc_attr( $classname ),
$sale_badge_html,
$product_image_gallery_html
);
}
}

View File

@@ -0,0 +1,93 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\Utils\StyleAttributesUtils;
/**
* ProductGalleryLargeImage class.
*/
class ProductGalleryThumbnails extends AbstractBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'product-gallery-thumbnails';
/**
* It isn't necessary register block assets because it is a server side block.
*/
protected function register_block_type_assets() {
return null;
}
/**
* Register the context
*
* @return string[]
*/
protected function get_block_type_uses_context() {
return [ 'productGalleryClientId', 'postId', 'thumbnailsNumberOfThumbnails', 'thumbnailsPosition' ];
}
/**
* Include and render the block.
*
* @param array $attributes Block attributes. Default empty array.
* @param string $content Block content. Default empty string.
* @param WP_Block $block Block instance.
* @return string Rendered block type output.
*/
protected function render( $attributes, $content, $block ) {
if ( isset( $block->context['thumbnailsPosition'] ) && '' !== $block->context['thumbnailsPosition'] && 'off' !== $block->context['thumbnailsPosition'] ) {
if ( ! empty( $content ) ) {
parent::register_block_type_assets();
$this->register_chunk_translations( [ $this->block_name ] );
return $content;
}
$classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes );
$post_id = isset( $block->context['postId'] ) ? $block->context['postId'] : '';
$product = wc_get_product( $post_id );
$post_thumbnail_id = $product->get_image_id();
$html = '';
if ( $product ) {
$attachment_ids = $product->get_gallery_image_ids();
if ( $attachment_ids && $post_thumbnail_id ) {
$html .= wc_get_gallery_image_html( $post_thumbnail_id, true );
$number_of_thumbnails = isset( $block->context['thumbnailsNumberOfThumbnails'] ) ? $block->context['thumbnailsNumberOfThumbnails'] : 3;
$thumbnails_count = 1;
foreach ( $attachment_ids as $attachment_id ) {
if ( $thumbnails_count >= $number_of_thumbnails ) {
break;
}
/**
* Filter the HTML markup for a single product image thumbnail in the gallery.
*
* @param string $thumbnail_html The HTML markup for the thumbnail.
* @param int $attachment_id The attachment ID of the thumbnail.
*
* @since 7.9.0
*/
$html .= apply_filters( 'woocommerce_single_product_image_thumbnail_html', wc_get_gallery_image_html( $attachment_id ), $attachment_id ); // phpcs:disable WordPress.XSS.EscapeOutput.OutputNotEscaped
$thumbnails_count++;
}
}
return sprintf(
'<div class="wc-block-components-product-gallery-thumbnails %1$s" style="%2$s">
%3$s
</div>',
esc_attr( $classes_and_styles['classes'] ),
esc_attr( $classes_and_styles['styles'] ),
$html
);
}
}
}
}

View File

@@ -75,9 +75,10 @@ class ProductImage extends AbstractBlock {
'showProductLink' => true,
'showSaleBadge' => true,
'saleBadgeAlign' => 'right',
'imageSizing' => 'full-size',
'imageSizing' => 'single',
'productId' => 'number',
'isDescendentOfQueryLoop' => 'false',
'scale' => 'cover',
);
return wp_parse_args( $attributes, $defaults );
@@ -141,22 +142,43 @@ class ProductImage extends AbstractBlock {
* Render Image.
*
* @param \WC_Product $product Product object.
* @param array $attributes Parsed attributes.
* @return string
*/
private function render_image( $product ) {
$image_info = wp_get_attachment_image_src( get_post_thumbnail_id( $product->get_id() ), 'woocommerce_thumbnail' );
private function render_image( $product, $attributes ) {
$image_size = 'single' === $attributes['imageSizing'] ? 'woocommerce_single' : 'woocommerce_thumbnail';
if ( ! isset( $image_info[0] ) ) {
$image_style = 'max-width:none;';
if ( ! empty( $attributes['height'] ) ) {
$image_style .= sprintf( 'height:%s;', $attributes['height'] );
}
if ( ! empty( $attributes['width'] ) ) {
$image_style .= sprintf( 'width:%s;', $attributes['width'] );
}
if ( ! empty( $attributes['scale'] ) ) {
$image_style .= sprintf( 'object-fit:%s;', $attributes['scale'] );
}
if ( ! $product->get_image_id() ) {
// The alt text is left empty on purpose, as it's considered a decorative image.
// More can be found here: https://www.w3.org/WAI/tutorials/images/decorative/.
// Github discussion for a context: https://github.com/woocommerce/woocommerce-blocks/pull/7651#discussion_r1019560494.
return sprintf( '<img src="%s" alt="" />', wc_placeholder_img_src( 'woocommerce_thumbnail' ) );
return wc_placeholder_img(
$image_size,
array(
'alt' => '',
'style' => $image_style,
)
);
}
return sprintf(
'<img data-testid="product-image" alt="%s" src="%s">',
$product->get_title(),
$image_info[0]
return $product->get_image(
$image_size,
array(
'alt' => $product->get_title(),
'data-testid' => 'product-image',
'style' => $image_style,
)
);
}
@@ -168,7 +190,7 @@ class ProductImage extends AbstractBlock {
* not in the post content on editor load.
*/
protected function enqueue_data( array $attributes = [] ) {
$this->asset_data_registry->add( 'is_block_theme_enabled', wc_current_theme_is_fse_theme(), false );
$this->asset_data_registry->add( 'isBlockThemeEnabled', wc_current_theme_is_fse_theme(), false );
}
@@ -188,11 +210,9 @@ class ProductImage extends AbstractBlock {
}
$parsed_attributes = $this->parse_attributes( $attributes );
$border_radius = StyleAttributesUtils::get_border_radius_class_and_style( $attributes );
$margin = StyleAttributesUtils::get_margin_class_and_style( $attributes );
$classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes );
$post_id = $block->context['postId'];
$post_id = isset( $block->context['postId'] ) ? $block->context['postId'] : '';
$product = wc_get_product( $post_id );
if ( $product ) {
@@ -205,7 +225,7 @@ class ProductImage extends AbstractBlock {
$this->render_anchor(
$product,
$this->render_on_sale_badge( $product, $parsed_attributes ),
$this->render_image( $product ),
$this->render_image( $product, $parsed_attributes ),
$parsed_attributes
)
);

View File

@@ -22,7 +22,7 @@ class ProductImageGallery extends AbstractBlock {
/**
* Register the context
*
* @var string
* @return string[]
*/
protected function get_block_type_uses_context() {
return [ 'query', 'queryId', 'postId' ];
@@ -37,17 +37,27 @@ class ProductImageGallery extends AbstractBlock {
* @return string Rendered block type output.
*/
protected function render( $attributes, $content, $block ) {
$post_id = $block->context['postId'];
if ( ! isset( $post_id ) ) {
return '';
}
global $product;
$product = wc_get_product( $post_id );
$previous_product = $product;
$product = wc_get_product( $post_id );
if ( ! $product instanceof \WC_Product ) {
$product = $previous_product;
return '';
}
if ( class_exists( 'WC_Frontend_Scripts' ) ) {
$frontend_scripts = new \WC_Frontend_Scripts();
$frontend_scripts::load_scripts();
}
$classname = $attributes['className'] ?? '';
ob_start();
woocommerce_show_product_sale_flash();
$sale_badge_html = ob_get_clean();
@@ -56,12 +66,13 @@ class ProductImageGallery extends AbstractBlock {
woocommerce_show_product_images();
$product_image_gallery_html = ob_get_clean();
$product = $previous_product;
$classname = $attributes['className'] ?? '';
return sprintf(
'<div class="wp-block-woocommerce-product-image-gallery %1$s">%2$s %3$s</div>',
esc_attr( $classname ),
$sale_badge_html,
$product_image_gallery_html
);
}
}

View File

@@ -42,10 +42,19 @@ class ProductPrice extends AbstractBlock {
'__experimentalFontWeight' => true,
'__experimentalFontStyle' => true,
),
'__experimentalSelector' => '.wc-block-components-product-price',
'__experimentalSelector' => '.wp-block-woocommerce-product-price .wc-block-components-product-price',
);
}
/**
* Get the frontend style handle for this block type.
*
* @return null
*/
protected function get_block_type_style() {
return null;
}
/**
* Overwrite parent method to prevent script registration.
*
@@ -78,7 +87,7 @@ class ProductPrice extends AbstractBlock {
return $content;
}
$post_id = $block->context['postId'];
$post_id = isset( $block->context['postId'] ) ? $block->context['postId'] : '';
$product = wc_get_product( $post_id );
if ( $product ) {
@@ -86,9 +95,9 @@ class ProductPrice extends AbstractBlock {
$text_align_styles_and_classes = StyleAttributesUtils::get_text_align_class_and_style( $attributes );
return sprintf(
'<div class="wc-block-components-product-price wc-block-grid__product-price %1$s %2$s" style="%3$s">
'<div class="wp-block-woocommerce-product-price"><div class="wc-block-components-product-price wc-block-grid__product-price %1$s %2$s" style="%3$s">
%4$s
</div>',
</div></div>',
esc_attr( $text_align_styles_and_classes['class'] ?? '' ),
esc_attr( $styles_and_classes['classes'] ),
esc_attr( $styles_and_classes['styles'] ?? '' ),

View File

@@ -2,6 +2,7 @@
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use WP_Query;
use Automattic\WooCommerce\Blocks\Utils\Utils;
// phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_tax_query
// phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_query
@@ -71,10 +72,69 @@ class ProductQuery extends AbstractBlock {
10,
2
);
add_filter(
'render_block',
array( $this, 'enqueue_styles' ),
10,
2
);
add_filter( 'rest_product_query', array( $this, 'update_rest_query' ), 10, 2 );
add_filter( 'rest_product_collection_params', array( $this, 'extend_rest_query_allowed_params' ), 10, 1 );
}
/**
* Post Template support for grid view was introduced in Gutenberg 16 / WordPress 6.3
* Fixed in:
* - https://github.com/woocommerce/woocommerce-blocks/pull/9916
* - https://github.com/woocommerce/woocommerce-blocks/pull/10360
*/
private function check_if_post_template_has_support_for_grid_view() {
if ( Utils::wp_version_compare( '6.3', '>=' ) ) {
return true;
}
if ( is_plugin_active( 'gutenberg/gutenberg.php' ) ) {
$gutenberg_version = '';
if ( defined( 'GUTENBERG_VERSION' ) ) {
$gutenberg_version = GUTENBERG_VERSION;
}
if ( ! $gutenberg_version ) {
$gutenberg_data = get_file_data(
WP_PLUGIN_DIR . '/gutenberg/gutenberg.php',
array( 'Version' => 'Version' )
);
$gutenberg_version = $gutenberg_data['Version'];
}
return version_compare( $gutenberg_version, '16.0', '>=' );
}
return false;
}
/**
* Extra data passed through from server to client for block.
*
* @param array $attributes Any attributes that currently are available from the block.
* Note, this will be empty in the editor context when the block is
* not in the post content on editor load.
*/
protected function enqueue_data( array $attributes = [] ) {
parent::enqueue_data( $attributes );
$post_template_has_support_for_grid_view = $this->check_if_post_template_has_support_for_grid_view();
$this->asset_data_registry->add(
'postTemplateHasSupportForGridView',
$post_template_has_support_for_grid_view
);
// The `loop_shop_per_page` filter can be found in WC_Query::product_query().
// phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
$this->asset_data_registry->add( 'loopShopPerPage', apply_filters( 'loop_shop_per_page', wc_get_default_products_per_row() * wc_get_default_product_rows_per_page() ), true );
}
/**
* Check if a given block
*
@@ -86,6 +146,22 @@ class ProductQuery extends AbstractBlock {
&& substr( $parsed_block['attrs']['namespace'], 0, 11 ) === 'woocommerce';
}
/**
* Enqueues the variation styles when rendering the Product Query variation.
*
* @param string $block_content The block content.
* @param array $block The full block, including name and attributes.
*
* @return string The block content.
*/
public function enqueue_styles( string $block_content, array $block ) {
if ( 'core/query' === $block['blockName'] && self::is_woocommerce_variation( $block ) ) {
wp_enqueue_style( 'wc-blocks-style-product-query' );
}
return $block_content;
}
/**
* Update the query for the product query block.
*
@@ -101,9 +177,8 @@ class ProductQuery extends AbstractBlock {
if ( self::is_woocommerce_variation( $parsed_block ) ) {
// Set this so that our product filters can detect if it's a PHP template.
$this->asset_data_registry->add( 'has_filterable_products', true, true );
$this->asset_data_registry->add( 'is_rendering_php_template', true, true );
$this->asset_data_registry->add( 'product_ids', $this->get_products_ids_by_attributes( $parsed_block ), true );
$this->asset_data_registry->add( 'hasFilterableProducts', true, true );
$this->asset_data_registry->add( 'isRenderingPhpTemplate', true, true );
add_filter(
'query_loop_block_query_vars',
array( $this, 'build_query' ),
@@ -165,18 +240,21 @@ class ProductQuery extends AbstractBlock {
}
$common_query_values = array(
'post_type' => 'product',
'post__in' => array(),
'post_status' => 'publish',
'meta_query' => array(),
'posts_per_page' => $query['posts_per_page'],
'orderby' => $query['orderby'],
'order' => $query['order'],
'offset' => $query['offset'],
'meta_query' => array(),
'post__in' => array(),
'post_status' => 'publish',
'post_type' => 'product',
'tax_query' => array(),
);
return $this->merge_queries(
$handpicked_products = isset( $parsed_block['attrs']['query']['include'] ) ?
$parsed_block['attrs']['query']['include'] : $common_query_values['post__in'];
$merged_query = $this->merge_queries(
$common_query_values,
$this->get_global_query( $parsed_block ),
$this->get_custom_orderby_query( $query['orderby'] ),
@@ -185,33 +263,8 @@ class ProductQuery extends AbstractBlock {
$this->get_filter_by_taxonomies_query( $query ),
$this->get_filter_by_keyword_query( $query )
);
}
/**
* Return the product ids based on the attributes and global query.
* This is used to allow the filter blocks to render data that matches with variations. More details here: https://github.com/woocommerce/woocommerce-blocks/issues/7245
*
* @param array $parsed_block The block being rendered.
* @return array
*/
private function get_products_ids_by_attributes( $parsed_block ) {
$query = $this->merge_queries(
array(
'post_type' => 'product',
'post__in' => array(),
'post_status' => 'publish',
'posts_per_page' => -1,
'meta_query' => array(),
'tax_query' => array(),
),
$this->get_queries_by_custom_attributes( $parsed_block ),
$this->get_global_query( $parsed_block )
);
$products = new \WP_Query( $query );
$post_ids = wp_list_pluck( $products->posts, 'ID' );
return $post_ids;
return $this->filter_query_to_only_include_ids( $merged_query, $handpicked_products );
}
/**
@@ -307,6 +360,23 @@ class ProductQuery extends AbstractBlock {
);
}
/**
* Apply the query only to a subset of products
*
* @param array $query The query.
* @param array $ids Array of selected product ids.
*
* @return array
*/
private function filter_query_to_only_include_ids( $query, $ids ) {
if ( ! empty( $ids ) ) {
$query['post__in'] = empty( $query['post__in'] ) ?
$ids : array_intersect( $ids, $query['post__in'] );
}
return $query;
}
/**
* Return the `tax_query` for the requested attributes
*

View File

@@ -51,6 +51,25 @@ class ProductRating extends AbstractBlock {
);
}
/**
* Get the block's attributes.
*
* @param array $attributes Block attributes. Default empty array.
* @return array Block attributes merged with defaults.
*/
private function parse_attributes( $attributes ) {
// These should match what's set in JS `registerBlockType`.
$defaults = array(
'productId' => 0,
'isDescendentOfQueryLoop' => false,
'textAlign' => '',
'isDescendentOfSingleProductBlock' => false,
'isDescendentOfSingleProductTemplate' => false,
);
return wp_parse_args( $attributes, $defaults );
}
/**
* Overwrite parent method to prevent script registration.
*
@@ -61,6 +80,15 @@ class ProductRating extends AbstractBlock {
return null;
}
/**
* Get the frontend style handle for this block type.
*
* @return null
*/
protected function get_block_type_style() {
return array_merge( parent::get_block_type_style(), [ 'wc-blocks-packages-style' ] );
}
/**
* Register the context.
*/
@@ -68,28 +96,6 @@ class ProductRating extends AbstractBlock {
return [ 'query', 'queryId', 'postId' ];
}
/**
* Filter the output from wc_get_rating_html.
*
* @param string $html Star rating markup. Default empty string.
* @param float $rating Rating being shown.
* @param int $count Total number of ratings.
* @return string
*/
public function filter_rating_html( $html, $rating, $count ) {
$product_permalink = get_permalink();
if ( 0 < $rating || false === $product_permalink ) {
/* translators: %s: rating */
$label = sprintf( __( 'Rated %s out of 5', 'woocommerce' ), $rating );
$html = '<div class="wc-block-components-product-rating__stars wc-block-grid__product-rating__stars" role="img" aria-label="' . esc_attr( $label ) . '">' . wc_get_star_rating_html( $rating, $count ) . '</div>';
} else {
$product_review_url = esc_url( $product_permalink . '#reviews' );
$html = '<a class="wc-block-components-product-rating__link" href="' . $product_review_url . '">' . __( 'Add review', 'woocommerce' ) . '</a>';
}
return $html;
}
/**
* Include and render the block.
*
@@ -108,14 +114,80 @@ class ProductRating extends AbstractBlock {
$post_id = $block->context['postId'];
$product = wc_get_product( $post_id );
if ( $product ) {
if ( $product && $product->get_review_count() > 0 ) {
$product_reviews_count = $product->get_review_count();
$product_rating = $product->get_average_rating();
$parsed_attributes = $this->parse_attributes( $attributes );
$is_descendent_of_single_product_block = $parsed_attributes['isDescendentOfSingleProductBlock'];
$is_descendent_of_single_product_template = $parsed_attributes['isDescendentOfSingleProductTemplate'];
$styles_and_classes = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes );
$text_align_styles_and_classes = StyleAttributesUtils::get_text_align_class_and_style( $attributes );
/**
* Filter the output from wc_get_rating_html.
*
* @param string $html Star rating markup. Default empty string.
* @param float $rating Rating being shown.
* @param int $count Total number of ratings.
* @return string
*/
$filter_rating_html = function( $html, $rating, $count ) use ( $post_id, $product_rating, $product_reviews_count, $is_descendent_of_single_product_block, $is_descendent_of_single_product_template ) {
$product_permalink = get_permalink( $post_id );
$reviews_count = $count;
$average_rating = $rating;
if ( $product_rating ) {
$average_rating = $product_rating;
}
if ( $product_reviews_count ) {
$reviews_count = $product_reviews_count;
}
if ( 0 < $average_rating || false === $product_permalink ) {
/* translators: %s: rating */
$label = sprintf( __( 'Rated %s out of 5', 'woocommerce' ), $average_rating );
$customer_reviews_count = sprintf(
/* translators: %s is referring to the total of reviews for a product */
_n(
'(%s customer review)',
'(%s customer reviews)',
$reviews_count,
'woocommerce'
),
esc_html( $reviews_count )
);
if ( $is_descendent_of_single_product_block ) {
$customer_reviews_count = '<a href="' . esc_url( $product_permalink ) . '#reviews">' . $customer_reviews_count . '</a>';
} elseif ( $is_descendent_of_single_product_template ) {
$customer_reviews_count = '<a class="woocommerce-review-link" rel="nofollow" href="#reviews">' . $customer_reviews_count . '</a>';
}
$reviews_count_html = sprintf( '<span class="wc-block-components-product-rating__reviews_count">%1$s</span>', $customer_reviews_count );
$html = sprintf(
'<div class="wc-block-components-product-rating__container">
<div class="wc-block-components-product-rating__stars wc-block-grid__product-rating__stars" role="img" aria-label="%1$s">
%2$s
</div>
%3$s
</div>
',
esc_attr( $label ),
wc_get_star_rating_html( $average_rating, $reviews_count ),
$is_descendent_of_single_product_block || $is_descendent_of_single_product_template ? $reviews_count_html : ''
);
} else {
$html = '';
}
return $html;
};
add_filter(
'woocommerce_product_get_rating_html',
[ $this, 'filter_rating_html' ],
$filter_rating_html,
10,
3
);
@@ -124,7 +196,7 @@ class ProductRating extends AbstractBlock {
remove_filter(
'woocommerce_product_get_rating_html',
[ $this, 'filter_rating_html' ],
$filter_rating_html,
10
);
@@ -138,5 +210,6 @@ class ProductRating extends AbstractBlock {
$rating_html
);
}
return '';
}
}

View File

@@ -0,0 +1,209 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\Utils\StyleAttributesUtils;
/**
* ProductRatingCounter class.
*/
class ProductRatingCounter extends AbstractBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'product-rating-counter';
/**
* API version name.
*
* @var string
*/
protected $api_version = '2';
/**
* Get block supports. Shared with the frontend.
* IMPORTANT: If you change anything here, make sure to update the JS file too.
*
* @return array
*/
protected function get_block_type_supports() {
return array(
'color' =>
array(
'text' => true,
'background' => false,
'link' => false,
'__experimentalSkipSerialization' => true,
),
'typography' =>
array(
'fontSize' => true,
'__experimentalSkipSerialization' => true,
),
'spacing' =>
array(
'margin' => true,
'padding' => true,
'__experimentalSkipSerialization' => true,
),
'__experimentalSelector' => '.wc-block-components-product-rating-counter',
);
}
/**
* Get the block's attributes.
*
* @param array $attributes Block attributes. Default empty array.
* @return array Block attributes merged with defaults.
*/
private function parse_attributes( $attributes ) {
// These should match what's set in JS `registerBlockType`.
$defaults = array(
'productId' => 0,
'isDescendentOfQueryLoop' => false,
'textAlign' => '',
'isDescendentOfSingleProductBlock' => false,
'isDescendentOfSingleProductTemplate' => false,
);
return wp_parse_args( $attributes, $defaults );
}
/**
* Overwrite parent method to prevent script registration.
*
* It is necessary to register and enqueues assets during the render
* phase because we want to load assets only if the block has the content.
*/
protected function register_block_type_assets() {
return null;
}
/**
* Get the frontend style handle for this block type.
*
* @return null
*/
protected function get_block_type_style() {
return null;
}
/**
* Register the context.
*/
protected function get_block_type_uses_context() {
return [ 'query', 'queryId', 'postId' ];
}
/**
* Include and render the block.
*
* @param array $attributes Block attributes. Default empty array.
* @param string $content Block content. Default empty string.
* @param WP_Block $block Block instance.
* @return string Rendered block type output.
*/
protected function render( $attributes, $content, $block ) {
if ( ! empty( $content ) ) {
parent::register_block_type_assets();
$this->register_chunk_translations( [ $this->block_name ] );
return $content;
}
$post_id = $block->context['postId'];
$product = wc_get_product( $post_id );
if ( $product && $product->get_review_count() > 0 ) {
$product_reviews_count = $product->get_review_count();
$product_rating = $product->get_average_rating();
$parsed_attributes = $this->parse_attributes( $attributes );
$is_descendent_of_single_product_block = $parsed_attributes['isDescendentOfSingleProductBlock'];
$is_descendent_of_single_product_template = $parsed_attributes['isDescendentOfSingleProductTemplate'];
$styles_and_classes = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes );
$text_align_styles_and_classes = StyleAttributesUtils::get_text_align_class_and_style( $attributes );
/**
* Filter the output from wc_get_rating_html.
*
* @param string $html Star rating markup. Default empty string.
* @param float $rating Rating being shown.
* @param int $count Total number of ratings.
* @return string
*/
$filter_rating_html = function( $html, $rating, $count ) use ( $post_id, $product_rating, $product_reviews_count, $is_descendent_of_single_product_block, $is_descendent_of_single_product_template ) {
$product_permalink = get_permalink( $post_id );
$reviews_count = $count;
$average_rating = $rating;
if ( $product_rating ) {
$average_rating = $product_rating;
}
if ( $product_reviews_count ) {
$reviews_count = $product_reviews_count;
}
if ( 0 < $average_rating || false === $product_permalink ) {
/* translators: %s: rating */
$customer_reviews_count = sprintf(
/* translators: %s is referring to the total of reviews for a product */
_n(
'(%s customer review)',
'(%s customer reviews)',
$reviews_count,
'woocommerce'
),
esc_html( $reviews_count )
);
if ( $is_descendent_of_single_product_block ) {
$customer_reviews_count = '<a href="' . esc_url( $product_permalink ) . '#reviews">' . $customer_reviews_count . '</a>';
} elseif ( $is_descendent_of_single_product_template ) {
$customer_reviews_count = '<a class="woocommerce-review-link" rel="nofollow" href="#reviews">' . $customer_reviews_count . '</a>';
}
$html = sprintf(
'<div class="wc-block-components-product-rating-counter__container">
<span class="wc-block-components-product-rating-counter__reviews_count">%1$s</span>
</div>
',
$customer_reviews_count
);
} else {
$html = '';
}
return $html;
};
add_filter(
'woocommerce_product_get_rating_html',
$filter_rating_html,
10,
3
);
$rating_html = wc_get_rating_html( $product->get_average_rating() );
remove_filter(
'woocommerce_product_get_rating_html',
$filter_rating_html,
10
);
return sprintf(
'<div class="wc-block-components-product-rating-counter wc-block-grid__product-rating-counter %1$s %2$s" style="%3$s">
%4$s
</div>',
esc_attr( $text_align_styles_and_classes['class'] ?? '' ),
esc_attr( $styles_and_classes['classes'] ),
esc_attr( $styles_and_classes['styles'] ?? '' ),
$rating_html
);
}
return '';
}
}

View File

@@ -0,0 +1,163 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\Utils\StyleAttributesUtils;
/**
* ProductRatingStars class.
*/
class ProductRatingStars extends AbstractBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'product-rating-stars';
/**
* API version name.
*
* @var string
*/
protected $api_version = '2';
/**
* Get block supports. Shared with the frontend.
* IMPORTANT: If you change anything here, make sure to update the JS file too.
*
* @return array
*/
protected function get_block_type_supports() {
return array(
'color' =>
array(
'text' => true,
'background' => false,
'link' => false,
'__experimentalSkipSerialization' => true,
),
'typography' =>
array(
'fontSize' => true,
'__experimentalSkipSerialization' => true,
),
'spacing' =>
array(
'margin' => true,
'padding' => true,
'__experimentalSkipSerialization' => true,
),
'__experimentalSelector' => '.wc-block-components-product-rating-stars',
);
}
/**
* Overwrite parent method to prevent script registration.
*
* It is necessary to register and enqueues assets during the render
* phase because we want to load assets only if the block has the content.
*/
protected function register_block_type_assets() {
return null;
}
/**
* Register the context.
*/
protected function get_block_type_uses_context() {
return [ 'query', 'queryId', 'postId' ];
}
/**
* Include and render the block.
*
* @param array $attributes Block attributes. Default empty array.
* @param string $content Block content. Default empty string.
* @param WP_Block $block Block instance.
* @return string Rendered block type output.
*/
protected function render( $attributes, $content, $block ) {
if ( ! empty( $content ) ) {
parent::register_block_type_assets();
$this->register_chunk_translations( [ $this->block_name ] );
return $content;
}
$post_id = $block->context['postId'];
$product = wc_get_product( $post_id );
if ( $product ) {
$product_reviews_count = $product->get_review_count();
$product_rating = $product->get_average_rating();
$styles_and_classes = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes );
$text_align_styles_and_classes = StyleAttributesUtils::get_text_align_class_and_style( $attributes );
/**
* Filter the output from wc_get_rating_html.
*
* @param string $html Star rating markup. Default empty string.
* @param float $rating Rating being shown.
* @param int $count Total number of ratings.
* @return string
*/
$filter_rating_html = function( $html, $rating, $count ) use ( $product_rating, $product_reviews_count ) {
$product_permalink = get_permalink();
$reviews_count = $count;
$average_rating = $rating;
if ( $product_rating ) {
$average_rating = $product_rating;
}
if ( $product_reviews_count ) {
$reviews_count = $product_reviews_count;
}
if ( 0 < $average_rating || false === $product_permalink ) {
/* translators: %s: rating */
$label = sprintf( __( 'Rated %s out of 5', 'woocommerce' ), $average_rating );
$html = sprintf(
'<div class="wc-block-components-product-rating-stars__container">
<div class="wc-block-components-product-rating__stars wc-block-grid__product-rating__stars" role="img" aria-label="%1$s">
%2$s
</div>
</div>
',
esc_attr( $label ),
wc_get_star_rating_html( $average_rating, $reviews_count )
);
} else {
$html = '';
}
return $html;
};
add_filter(
'woocommerce_product_get_rating_html',
$filter_rating_html,
10,
3
);
$rating_html = wc_get_rating_html( $product->get_average_rating() );
remove_filter(
'woocommerce_product_get_rating_html',
$filter_rating_html,
10
);
return sprintf(
'<div class="wc-block-components-product-rating wc-block-grid__product-rating %1$s %2$s" style="%3$s">
%4$s
</div>',
esc_attr( $text_align_styles_and_classes['class'] ?? '' ),
esc_attr( $styles_and_classes['classes'] ),
esc_attr( $styles_and_classes['styles'] ?? '' ),
$rating_html
);
}
}
}

View File

@@ -70,9 +70,9 @@ class ProductSKU extends AbstractBlock {
$styles_and_classes = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes );
return sprintf(
'<div class="wc-block-components-product-sku wc-block-grid__product-sku wp-block-woocommerce-product-sku %1$s" style="%2$s">
'<div class="wc-block-components-product-sku wc-block-grid__product-sku wp-block-woocommerce-product-sku product_meta %1$s" style="%2$s">
SKU:
<strong>%3$s</strong>
<strong class="sku">%3$s</strong>
</div>',
esc_attr( $styles_and_classes['classes'] ),
esc_attr( $styles_and_classes['styles'] ?? '' ),

View File

@@ -22,28 +22,6 @@ class ProductStockIndicator extends AbstractBlock {
*/
protected $api_version = '2';
/**
* Get block supports. Shared with the frontend.
* IMPORTANT: If you change anything here, make sure to update the JS file too.
*
* @return array
*/
protected function get_block_type_supports() {
return array(
'color' =>
array(
'link' => false,
'background' => false,
'text' => true,
),
'typography' =>
array(
'fontSize' => true,
),
'__experimentalSelector' => '.wc-block-components-product-stock-indicator',
);
}
/**
* Register script and style assets for the block type before it is registered.
*
@@ -125,7 +103,7 @@ class ProductStockIndicator extends AbstractBlock {
$classnames .= $is_on_backorder ? ' wc-block-components-product-stock-indicator--available-on-backorder ' : '';
$output = '';
$output .= '<div class="wc-block-components-product-stock-indicator ' . esc_attr( $classnames ) . '"';
$output .= '<div class="wc-block-components-product-stock-indicator wp-block-woocommerce-product-stock-indicator ' . esc_attr( $classnames ) . '"';
$output .= isset( $classes_and_styles['styles'] ) ? ' style="' . esc_attr( $classes_and_styles['styles'] ) . '"' : '';
$output .= '>';
$output .= wp_kses_post( self::getTextBasedOnStock( $is_in_stock, $is_low_stock, $low_stock_amount, $is_on_backorder ) );

View File

@@ -0,0 +1,142 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
use WP_Block;
use WP_Query;
/**
* ProductTemplate class.
*/
class ProductTemplate extends AbstractBlock {
/**
* Block name.
*
* @var string
*/
protected $block_name = 'product-template';
/**
* Get the frontend script handle for this block type.
*
* @param string $key Data to get, or default to everything.
*/
protected function get_block_type_script( $key = null ) {
return null;
}
/**
* Render the block.
*
* @param array $attributes Block attributes.
* @param string $content Block content.
* @param WP_Block $block Block instance.
*
* @return string | void Rendered block output.
*/
protected function render( $attributes, $content, $block ) {
$page_key = isset( $block->context['queryId'] ) ? 'query-' . $block->context['queryId'] . '-page' : 'query-page';
// phpcs:ignore WordPress.Security.NonceVerification
$page = empty( $_GET[ $page_key ] ) ? 1 : (int) $_GET[ $page_key ];
// Use global query if needed.
$use_global_query = ( isset( $block->context['query']['inherit'] ) && $block->context['query']['inherit'] );
if ( $use_global_query ) {
global $wp_query;
$query = clone $wp_query;
} else {
$query_args = build_query_vars_from_query_block( $block, $page );
$query = new WP_Query( $query_args );
}
if ( ! $query->have_posts() ) {
return '';
}
if ( $this->block_core_post_template_uses_featured_image( $block->inner_blocks ) ) {
update_post_thumbnail_cache( $query );
}
$classnames = '';
if ( isset( $block->context['displayLayout'] ) && isset( $block->context['query'] ) ) {
if ( isset( $block->context['displayLayout']['type'] ) && 'flex' === $block->context['displayLayout']['type'] ) {
$classnames = "is-flex-container columns-{$block->context['displayLayout']['columns']}";
}
}
if ( isset( $attributes['style']['elements']['link']['color']['text'] ) ) {
$classnames .= ' has-link-color';
}
$classnames .= ' wc-block-product-template';
$wrapper_attributes = get_block_wrapper_attributes( array( 'class' => trim( $classnames ) ) );
$content = '';
while ( $query->have_posts() ) {
$query->the_post();
// Get an instance of the current Post Template block.
$block_instance = $block->parsed_block;
// Set the block name to one that does not correspond to an existing registered block.
// This ensures that for the inner instances of the Post Template block, we do not render any block supports.
$block_instance['blockName'] = 'core/null';
// Render the inner blocks of the Post Template block with `dynamic` set to `false` to prevent calling
// `render_callback` and ensure that no wrapper markup is included.
$block_content = (
new WP_Block(
$block_instance,
array(
'postType' => get_post_type(),
'postId' => get_the_ID(),
)
)
)->render( array( 'dynamic' => false ) );
// Wrap the render inner blocks in a `li` element with the appropriate post classes.
$post_classes = implode( ' ', get_post_class( 'wc-block-product' ) );
$content .= '<li class="' . esc_attr( $post_classes ) . '">' . $block_content . '</li>';
}
/*
* Use this function to restore the context of the template tags
* from a secondary query loop back to the main query loop.
* Since we use two custom loops, it's safest to always restore.
*/
wp_reset_postdata();
return sprintf(
'<ul %1$s>%2$s</ul>',
$wrapper_attributes,
$content
);
}
/**
* Determines whether a block list contains a block that uses the featured image.
*
* @param WP_Block_List $inner_blocks Inner block instance.
*
* @return bool Whether the block list contains a block that uses the featured image.
*/
protected function block_core_post_template_uses_featured_image( $inner_blocks ) {
foreach ( $inner_blocks as $block ) {
if ( 'core/post-featured-image' === $block->name ) {
return true;
}
if (
'core/cover' === $block->name &&
! empty( $block->attributes['useFeaturedImage'] )
) {
return true;
}
if ( $block->inner_blocks && block_core_post_template_uses_featured_image( $block->inner_blocks ) ) {
return true;
}
}
return false;
}
}

View File

@@ -14,4 +14,21 @@ class RatingFilter extends AbstractBlock {
protected $block_name = 'rating-filter';
const RATING_QUERY_VAR = 'rating_filter';
/**
* Get the frontend script handle for this block type.
*
* @param string $key Data to get, or default to everything.
*/
protected function get_block_type_script( $key = null ) {
return null;
}
/**
* Get the frontend style handle for this block type.
*
* @return string[]
*/
protected function get_block_type_style() {
return array_merge( parent::get_block_type_style(), [ 'wc-blocks-packages-style' ] );
}
}

View File

@@ -51,6 +51,15 @@ class RelatedProducts extends AbstractBlock {
return null;
}
/**
* Get the frontend style handle for this block type.
*
* @return null
*/
protected function get_block_type_style() {
return null;
}
/**
* Update the query for the product query block.
*
@@ -87,15 +96,16 @@ class RelatedProducts extends AbstractBlock {
return $query;
}
$related_products_ids = $this->get_related_products_ids();
$related_products_ids = $this->get_related_products_ids( $query['posts_per_page'] );
if ( count( $related_products_ids ) < 1 ) {
return array();
}
return array(
'post_type' => 'product',
'post__in' => $related_products_ids,
'post_status' => 'publish',
'post_type' => 'product',
'post__in' => $related_products_ids,
'post_status' => 'publish',
'posts_per_page' => $query['posts_per_page'],
);
}
@@ -112,16 +122,15 @@ class RelatedProducts extends AbstractBlock {
return $content;
}
// If there are no related products, render nothing.
$related_products_ids = $this->get_related_products_ids();
if ( count( $related_products_ids ) < 1 ) {
return '';
}
return $content;
}
/**
* Determines whether the block is a related products block.
*
@@ -141,14 +150,15 @@ class RelatedProducts extends AbstractBlock {
* Get related products ids.
* The logic is copied from the core function woocommerce_related_products. https://github.com/woocommerce/woocommerce/blob/ca49caabcba84ce9f60a03c6d3534ec14b350b80/plugins/woocommerce/includes/wc-template-functions.php/#L2039-L2074
*
* @param number $product_per_page Products per page.
* @return array Products ids.
*/
private function get_related_products_ids() {
private function get_related_products_ids( $product_per_page = 5 ) {
global $post;
$product = wc_get_product( $post->ID );
$related_products = array_filter( array_map( 'wc_get_product', wc_get_related_products( $product->get_id(), 5, $product->get_upsell_ids() ) ), 'wc_products_array_filter_visible' );
$related_products = array_filter( array_map( 'wc_get_product', wc_get_related_products( $product->get_id(), $product_per_page, $product->get_upsell_ids() ) ), 'wc_products_array_filter_visible' );
$related_products = wc_products_array_orderby( $related_products, 'rand', 'desc' );
$related_product_ids = array_map(

View File

@@ -36,7 +36,32 @@ class SingleProduct extends AbstractBlock {
*/
protected function initialize() {
parent::initialize();
add_filter( 'render_block_context', array( $this, 'update_context' ), 10, 3 );
add_filter( 'render_block_context', [ $this, 'update_context' ], 10, 3 );
add_filter( 'render_block_core/post-excerpt', [ $this, 'restore_global_post' ], 10, 3 );
add_filter( 'render_block_core/post-title', [ $this, 'restore_global_post' ], 10, 3 );
}
/**
* Restore the global post variable right before generating the render output for the post title and/or post excerpt blocks.
*
* This is required due to the changes made via the replace_post_for_single_product_inner_block method.
* It is a temporary fix to ensure these blocks work as expected until Gutenberg versions 15.2 and 15.6 are part of the core of WordPress.
*
* @see https://github.com/WordPress/gutenberg/pull/48001
* @see https://github.com/WordPress/gutenberg/pull/49495
*
* @param string $block_content The block content.
* @param array $parsed_block The full block, including name and attributes.
* @param \WP_Block $block_instance The block instance.
*
* @return mixed
*/
public function restore_global_post( $block_content, $parsed_block, $block_instance ) {
if ( isset( $block_instance->context['singleProduct'] ) && $block_instance->context['singleProduct'] ) {
wp_reset_postdata();
}
return $block_content;
}
@@ -50,13 +75,10 @@ class SingleProduct extends AbstractBlock {
* @return string Rendered block output.
*/
protected function render( $attributes, $content, $block ) {
$classname = $attributes['className'] ?? '';
$html = sprintf(
'<div class="wp-block-woocommerce-single-product %1$s">
%2$s
'<div class="woocommerce">
%1$s
</div>',
esc_attr( $classname ),
$content
);
@@ -74,7 +96,7 @@ class SingleProduct extends AbstractBlock {
* @return array Updated block context.
*/
public function update_context( $context, $block, $parent_block ) {
if ( 'woocommerce/single-product' == $block['blockName']
if ( 'woocommerce/single-product' === $block['blockName']
&& isset( $block['attrs']['productId'] ) ) {
$this->product_id = $block['attrs']['productId'];
@@ -125,18 +147,27 @@ class SingleProduct extends AbstractBlock {
$block_name = array_pop( $this->single_product_inner_blocks_names );
if ( $block_name === $block['blockName'] ) {
// @todo This is a temporary fix to make the Post Excerpt block work while https://github.com/WordPress/gutenberg/pull/49495 is not merged
if ( 'core/post-excerpt' === $block_name ) {
/**
* This is a temporary fix to ensure the Post Title and Excerpt blocks work as expected
* until Gutenberg versions 15.2 and 15.6 are included in the core of WordPress.
*
* Important: the original post data is restored in the restore_global_post method.
*
* @see https://github.com/WordPress/gutenberg/pull/48001
* @see https://github.com/WordPress/gutenberg/pull/49495
*/
if ( 'core/post-excerpt' === $block_name || 'core/post-title' === $block_name ) {
global $post;
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
$post = get_post( $this->product_id );
setup_postdata( $post );
}
$context['postId'] = $this->product_id;
}
if ( ! $this->single_product_inner_blocks_names ) {
wp_reset_postdata();
if ( $post instanceof \WP_Post ) {
setup_postdata( $post );
}
}
$context['postId'] = $this->product_id;
$context['singleProduct'] = true;
}
}
}

View File

@@ -33,4 +33,13 @@ class StockFilter extends AbstractBlock {
public static function get_stock_status_query_var_values() {
return array_keys( wc_get_product_stock_status_options() );
}
/**
* Get the frontend style handle for this block type.
*
* @return string[]
*/
protected function get_block_type_style() {
return array_merge( parent::get_block_type_style(), [ 'wc-blocks-packages-style' ] );
}
}

View File

@@ -57,4 +57,13 @@ class StoreNotices extends AbstractBlock {
protected function get_block_type_script( $key = null ) {
return null;
}
/**
* Get the frontend style handle for this block type.
*
* @return null
*/
protected function get_block_type_style() {
return null;
}
}

View File

@@ -1,7 +1,6 @@
<?php
namespace Automattic\WooCommerce\Blocks;
use Automattic\WooCommerce\Blocks\BlockTypes\AtomicBlock;
use Automattic\WooCommerce\Blocks\Package;
use Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry;
use Automattic\WooCommerce\Blocks\Assets\Api as AssetApi;
@@ -61,10 +60,10 @@ final class BlockTypesController {
$block_types = $this->get_block_types();
foreach ( $block_types as $block_type ) {
$block_type_class = __NAMESPACE__ . '\\BlockTypes\\' . $block_type;
$block_type_instance = new $block_type_class( $this->asset_api, $this->asset_data_registry, new IntegrationRegistry() );
}
$block_type_class = __NAMESPACE__ . '\\BlockTypes\\' . $block_type;
new $block_type_class( $this->asset_api, $this->asset_data_registry, new IntegrationRegistry() );
}
}
/**
@@ -196,7 +195,10 @@ final class BlockTypesController {
'ProductOnSale',
'ProductPrice',
'ProductQuery',
'ProductAverageRating',
'ProductRating',
'ProductRatingCounter',
'ProductRatingStars',
'ProductResultsCount',
'ProductReviews',
'ProductSaleBadge',
@@ -213,6 +215,7 @@ final class BlockTypesController {
'ReviewsByProduct',
'RelatedProducts',
'ProductDetails',
'SingleProduct',
'StockFilter',
];
@@ -224,21 +227,11 @@ final class BlockTypesController {
);
if ( Package::feature()->is_experimental_build() ) {
$block_types[] = 'SingleProduct';
}
/**
* This disables specific blocks in Widget Areas by not registering them.
*/
if ( in_array( $pagenow, [ 'widgets.php', 'themes.php', 'customize.php' ], true ) && ( empty( $_GET['page'] ) || 'gutenberg-edit-site' !== $_GET['page'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
$block_types = array_diff(
$block_types,
[
'AllProducts',
'Cart',
'Checkout',
]
);
$block_types[] = 'ProductCollection';
$block_types[] = 'ProductTemplate';
$block_types[] = 'ProductGallery';
$block_types[] = 'ProductGalleryLargeImage';
$block_types[] = 'ProductGalleryThumbnails';
}
/**

View File

@@ -12,6 +12,7 @@ use Automattic\WooCommerce\Blocks\Domain\Services\Notices;
use Automattic\WooCommerce\Blocks\Domain\Services\DraftOrders;
use Automattic\WooCommerce\Blocks\Domain\Services\FeatureGating;
use Automattic\WooCommerce\Blocks\Domain\Services\GoogleAnalytics;
use Automattic\WooCommerce\Blocks\Domain\Services\Hydration;
use Automattic\WooCommerce\Blocks\InboxNotifications;
use Automattic\WooCommerce\Blocks\Installer;
use Automattic\WooCommerce\Blocks\Migration;
@@ -22,7 +23,11 @@ use Automattic\WooCommerce\Blocks\Payments\Integrations\Cheque;
use Automattic\WooCommerce\Blocks\Payments\Integrations\PayPal;
use Automattic\WooCommerce\Blocks\Payments\PaymentMethodRegistry;
use Automattic\WooCommerce\Blocks\Registry\Container;
use Automattic\WooCommerce\Blocks\Templates\CartTemplate;
use Automattic\WooCommerce\Blocks\Templates\CheckoutHeaderTemplate;
use Automattic\WooCommerce\Blocks\Templates\CheckoutTemplate;
use Automattic\WooCommerce\Blocks\Templates\ClassicTemplatesCompatibility;
use Automattic\WooCommerce\Blocks\Templates\OrderConfirmationTemplate;
use Automattic\WooCommerce\Blocks\Templates\ProductAttributeTemplate;
use Automattic\WooCommerce\Blocks\Templates\ProductSearchResultsTemplate;
use Automattic\WooCommerce\StoreApi\RoutesController;
@@ -95,12 +100,12 @@ class Bootstrap {
protected function init() {
$this->register_dependencies();
$this->register_payment_methods();
$this->load_interactivity_api();
if ( $this->package->is_experimental_build() && is_admin() ) {
if ( $this->package->get_version() !== $this->package->get_version_stored_on_db() ) {
$this->migration->run_migrations();
$this->package->set_version_stored_on_db();
}
// This is just a temporary solution to make sure the migrations are run. We have to refactor this. More details: https://github.com/woocommerce/woocommerce-blocks/issues/10196.
if ( $this->package->get_version() !== $this->package->get_version_stored_on_db() ) {
$this->migration->run_migrations();
$this->package->set_version_stored_on_db();
}
add_action(
@@ -115,29 +120,43 @@ class Bootstrap {
);
$is_rest = wc()->is_rest_api_request();
// phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$is_store_api_request = $is_rest && ! empty( $_SERVER['REQUEST_URI'] ) && ( false !== strpos( $_SERVER['REQUEST_URI'], trailingslashit( rest_get_url_prefix() ) . 'wc/store/' ) );
// Load and init assets.
$this->container->get( StoreApi::class )->init();
$this->container->get( PaymentsApi::class )->init();
$this->container->get( DraftOrders::class )->init();
$this->container->get( CreateAccount::class )->init();
$this->container->get( ShippingController::class )->init();
// Load assets in admin and on the frontend.
if ( ! $is_rest ) {
$this->add_build_notice();
$this->container->get( AssetDataRegistry::class );
$this->container->get( Installer::class );
$this->container->get( AssetsController::class );
$this->container->get( Installer::class )->init();
$this->container->get( GoogleAnalytics::class )->init();
}
// Load assets unless this is a request specifically for the store API.
if ( ! $is_store_api_request ) {
// Template related functionality. These won't be loaded for store API requests, but may be loaded for
// regular rest requests to maintain compatibility with the store editor.
$this->container->get( BlockPatterns::class );
$this->container->get( BlockTypesController::class );
$this->container->get( BlockTemplatesController::class );
$this->container->get( ProductSearchResultsTemplate::class );
$this->container->get( ProductAttributeTemplate::class );
$this->container->get( CartTemplate::class );
$this->container->get( CheckoutTemplate::class );
$this->container->get( CheckoutHeaderTemplate::class );
$this->container->get( OrderConfirmationTemplate::class );
$this->container->get( ClassicTemplatesCompatibility::class );
$this->container->get( ArchiveProductTemplatesCompatibility::class )->init();
$this->container->get( SingleProductTemplateCompatibility::class )->init();
$this->container->get( Notices::class )->init();
}
$this->container->get( DraftOrders::class )->init();
$this->container->get( CreateAccount::class )->init();
$this->container->get( Notices::class )->init();
$this->container->get( StoreApi::class )->init();
$this->container->get( GoogleAnalytics::class );
$this->container->get( BlockTypesController::class );
$this->container->get( BlockTemplatesController::class );
$this->container->get( ProductSearchResultsTemplate::class );
$this->container->get( ProductAttributeTemplate::class );
$this->container->get( ClassicTemplatesCompatibility::class );
$this->container->get( ArchiveProductTemplatesCompatibility::class )->init();
$this->container->get( SingleProductTemplateCompatibility::class )->init();
$this->container->get( BlockPatterns::class );
$this->container->get( PaymentsApi::class );
$this->container->get( ShippingController::class )->init();
}
/**
@@ -207,6 +226,13 @@ class Bootstrap {
);
}
/**
* Load and set up the Interactivity API if enabled.
*/
protected function load_interactivity_api() {
require_once __DIR__ . '/../Interactivity/load.php';
}
/**
* Register core dependencies with the container.
*/
@@ -273,6 +299,30 @@ class Bootstrap {
return new ProductAttributeTemplate();
}
);
$this->container->register(
CartTemplate::class,
function () {
return new CartTemplate();
}
);
$this->container->register(
CheckoutTemplate::class,
function () {
return new CheckoutTemplate();
}
);
$this->container->register(
CheckoutHeaderTemplate::class,
function () {
return new CheckoutHeaderTemplate();
}
);
$this->container->register(
OrderConfirmationTemplate::class,
function () {
return new OrderConfirmationTemplate();
}
);
$this->container->register(
ClassicTemplatesCompatibility::class,
function ( Container $container ) {
@@ -308,10 +358,6 @@ class Bootstrap {
$this->container->register(
GoogleAnalytics::class,
function( Container $container ) {
// Require Google Analytics Integration to be activated.
if ( ! class_exists( 'WC_Google_Analytics_Integration', false ) ) {
return;
}
$asset_api = $container->get( AssetApi::class );
return new GoogleAnalytics( $asset_api );
}
@@ -322,6 +368,12 @@ class Bootstrap {
return new Notices( $container->get( Package::class ) );
}
);
$this->container->register(
Hydration::class,
function( Container $container ) {
return new Hydration( $container->get( AssetDataRegistry::class ) );
}
);
$this->container->register(
PaymentsApi::class,
function ( Container $container ) {

View File

@@ -22,13 +22,16 @@ class GoogleAnalytics {
*/
public function __construct( AssetApi $asset_api ) {
$this->asset_api = $asset_api;
$this->init();
}
/**
* Hook into WP.
*/
protected function init() {
public function init() {
// Require Google Analytics Integration to be activated.
if ( ! class_exists( 'WC_Google_Analytics_Integration', false ) ) {
return;
}
add_action( 'init', array( $this, 'register_assets' ) );
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
add_filter( 'script_loader_tag', array( $this, 'async_script_loader_tags' ), 10, 3 );

View File

@@ -0,0 +1,97 @@
<?php
namespace Automattic\WooCommerce\Blocks\Domain\Services;
use Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry;
/**
* Service class that handles hydration of API data for blocks.
*/
class Hydration {
/**
* Instance of the asset data registry.
*
* @var AssetDataRegistry
*/
protected $asset_data_registry;
/**
* Cached notices to restore after hydrating the API.
*
* @var array
*/
protected $cached_store_notices = [];
/**
* Constructor.
*
* @param AssetDataRegistry $asset_data_registry Instance of the asset data registry.
*/
public function __construct( AssetDataRegistry $asset_data_registry ) {
$this->asset_data_registry = $asset_data_registry;
}
/**
* Hydrates the asset data registry with data from the API. Disables notices and nonces so requests contain valid
* data that is not polluted by the current session.
*
* @param array $path API paths to hydrate e.g. '/wc/store/v1/cart'.
* @return array Response data.
*/
public function get_rest_api_response_data( $path = '' ) {
$this->cache_store_notices();
$this->disable_nonce_check();
// Preload the request and add it to the array. It will be $preloaded_requests['path'] and contain 'body' and 'headers'.
$preloaded_requests = rest_preload_api_request( [], $path );
$this->restore_cached_store_notices();
$this->restore_nonce_check();
// Returns just the single preloaded request.
return $preloaded_requests[ $path ];
}
/**
* Disable the nonce check temporarily.
*/
protected function disable_nonce_check() {
add_filter( 'woocommerce_store_api_disable_nonce_check', [ $this, 'disable_nonce_check_callback' ] );
}
/**
* Callback to disable the nonce check. While we could use `__return_true`, we use a custom named callback so that
* we can remove it later without affecting other filters.
*/
public function disable_nonce_check_callback() {
return true;
}
/**
* Restore the nonce check.
*/
protected function restore_nonce_check() {
remove_filter( 'woocommerce_store_api_disable_nonce_check', [ $this, 'disable_nonce_check_callback' ] );
}
/**
* Cache notices before hydrating the API if the customer has a session.
*/
protected function cache_store_notices() {
if ( ! did_action( 'woocommerce_init' ) || null === WC()->session ) {
return;
}
$this->cached_store_notices = WC()->session->get( 'wc_notices', array() );
WC()->session->set( 'wc_notices', null );
}
/**
* Restore notices into current session from cache.
*/
protected function restore_cached_store_notices() {
if ( ! did_action( 'woocommerce_init' ) || null === WC()->session ) {
return;
}
WC()->session->set( 'wc_notices', $this->cached_store_notices );
$this->cached_store_notices = [];
}
}

View File

@@ -2,6 +2,7 @@
namespace Automattic\WooCommerce\Blocks\Domain\Services;
use Automattic\WooCommerce\Blocks\Domain\Package;
use Automattic\WooCommerce\Blocks\Utils\CartCheckoutUtils;
/**
* Service class for adding new-style Notices to WooCommerce core.
@@ -41,15 +42,7 @@ class Notices {
* is using the new block based cart/checkout.
*/
public function init() {
// Core page IDs.
$cart_page_id = wc_get_page_id( 'cart' );
$checkout_page_id = wc_get_page_id( 'checkout' );
// Checks a specific page (by ID) to see if it contains the named block.
$has_block_cart = $cart_page_id && has_block( 'woocommerce/cart', $cart_page_id );
$has_block_checkout = $checkout_page_id && has_block( 'woocommerce/checkout', $checkout_page_id );
if ( $has_block_cart || $has_block_checkout ) {
if ( CartCheckoutUtils::is_cart_block_default() || CartCheckoutUtils::is_checkout_block_default() ) {
add_filter( 'woocommerce_kses_notice_allowed_tags', [ $this, 'add_kses_notice_allowed_tags' ] );
add_filter( 'wc_get_template', [ $this, 'get_notices_template' ], 10, 5 );
add_action(

View File

@@ -8,14 +8,6 @@ namespace Automattic\WooCommerce\Blocks;
* @internal
*/
class Installer {
/**
* Constructor
*/
public function __construct() {
$this->init();
}
/**
* Installation tasks ran on admin_init callback.
*/
@@ -26,7 +18,7 @@ class Installer {
/**
* Initialize class features.
*/
protected function init() {
public function init() {
add_action( 'admin_init', array( $this, 'install' ) );
}

View File

@@ -0,0 +1,56 @@
<?php
/**
* Manages the initial state of the Interactivity API store in the server and
* its serialization so it can be restored in the browser upon hydration.
*
* It's a private class, exposed by other functions, like `wc_store`.
*
* @access private
*/
class WC_Interactivity_Store {
/**
* Store.
*
* @var array
*/
private static $store = array();
/**
* Get store data.
*
* @return array
*/
static function get_data() {
return self::$store;
}
/**
* Merge data.
*
* @param array $data The data that will be merged with the exsisting store.
*/
static function merge_data( $data ) {
self::$store = array_replace_recursive( self::$store, $data );
}
/**
* Reset the store data.
*/
static function reset() {
self::$store = array();
}
/**
* Render the store data.
*/
static function render() {
if ( empty( self::$store ) ) {
return;
}
echo sprintf(
'<script id="wc-interactivity-store-data" type="application/json">%s</script>',
wp_json_encode( self::$store )
);
}
}

View File

@@ -1,22 +0,0 @@
<?php
require_once __DIR__ . '/../utils.php';
function process_woo_bind( $tags, $context ) {
if ( $tags->is_tag_closer() ) {
return;
}
$prefixed_attributes = $tags->get_attribute_names_with_prefix( 'data-woo-bind:' );
foreach ( $prefixed_attributes as $attr ) {
list( , $bound_attr ) = explode( ':', $attr );
if ( empty( $bound_attr ) ) {
continue;
}
$expr = $tags->get_attribute( $attr );
$value = woo_directives_evaluate( $expr, $context->get_context() );
$tags->set_attribute( $bound_attr, $value );
}
}

View File

@@ -1,26 +0,0 @@
<?php
require_once __DIR__ . '/../utils.php';
function process_woo_class( $tags, $context ) {
if ( $tags->is_tag_closer() ) {
return;
}
$prefixed_attributes = $tags->get_attribute_names_with_prefix( 'data-woo-class:' );
foreach ( $prefixed_attributes as $attr ) {
list( , $class_name ) = explode( ':', $attr );
if ( empty( $class_name ) ) {
continue;
}
$expr = $tags->get_attribute( $attr );
$add_class = woo_directives_evaluate( $expr, $context->get_context() );
if ( $add_class ) {
$tags->add_class( $class_name );
} else {
$tags->remove_class( $class_name );
}
}
}

View File

@@ -1,18 +0,0 @@
<?php
function process_woo_context_attribute( $tags, $context ) {
if ( $tags->is_tag_closer() ) {
$context->rewind_context();
return;
}
$value = $tags->get_attribute( 'data-woo-context' );
if ( null === $value ) {
// No woo-context directive.
return;
}
$new_context = json_decode( $value, true );
$context->set_context( $new_context );
}

View File

@@ -1,29 +0,0 @@
<?php
require_once __DIR__ . '/../utils.php';
function process_woo_style( $tags, $context ) {
if ( $tags->is_tag_closer() ) {
return;
}
$prefixed_attributes = $tags->get_attribute_names_with_prefix( 'data-woo-style:' );
foreach ( $prefixed_attributes as $attr ) {
list( , $style_name ) = explode( ':', $attr );
if ( empty( $style_name ) ) {
continue;
}
$expr = $tags->get_attribute( $attr );
$style_value = woo_directives_evaluate( $expr, $context->get_context() );
if ( $style_value ) {
$style_attr = $tags->get_attribute( 'style' );
$style_attr = woo_directives_set_style( $style_attr, $style_name, $style_value );
$tags->set_attribute( 'style', $style_attr );
} else {
// $tags->remove_class( $style_name );
}
}
}

View File

@@ -1,73 +0,0 @@
<?php
/**
* Context data implementation.
*
* @package block-hydration-experiments
*/
/**
* This is a data structure to hold the current context.
*
* Whenever encountering a `woo-context` directive, we need to update
* the context with the data found in that directive. Conversely,
* when "leaving" that context (by encountering a closing tag), we
* need to reset the context to its previous state. This means that
* we actually need sort of a stack to keep track of all nested contexts.
*
* Example:
*
* <woo-context data='{ "foo": 123 }'>
* <!-- foo should be 123 here. -->
* <woo-context data='{ "foo": 456 }'>
* <!-- foo should be 456 here. -->
* </woo-context>
* <!-- foo should be reset to 123 here. -->
* </woo-context>
*/
class Woo_Directive_Context {
/**
* The stack used to store contexts internally.
*
* @var array An array of contexts.
*/
protected $stack = array( array() );
/**
* Constructor.
*
* Accepts a context as an argument to initialize this with.
*
* @param array $context A context.
*/
function __construct( $context = array() ) {
$this->set_context( $context );
}
/**
* Return the current context.
*
* @return array The current context.
*/
public function get_context() {
return end( $this->stack );
}
/**
* Set the current context.
*
* @param array $context The context to be set.
* @return void
*/
public function set_context( $context ) {
array_push( $this->stack, array_replace_recursive( $this->get_context(), $context ) );
}
/**
* Reset the context to its previous state.
*
* @return void
*/
public function rewind_context() {
array_pop( $this->stack );
}
}

View File

@@ -1,31 +0,0 @@
<?php
class Woo_Directive_Store {
private static $store = array();
static function get_data() {
return self::$store;
}
static function merge_data( $data ) {
self::$store = array_replace_recursive( self::$store, $data );
}
static function serialize() {
return json_encode( self::$store );
}
static function reset() {
self::$store = array();
}
static function render() {
if ( empty( self::$store ) ) {
return;
}
$id = 'store';
$store = self::serialize();
echo "<script id=\"$id\" type=\"application/json\">$store</script>";
}
}

View File

@@ -1,18 +0,0 @@
<?php
function process_woo_context_tag( $tags, $context ) {
if ( $tags->is_tag_closer() ) {
$context->rewind_context();
return;
}
$value = $tags->get_attribute( 'data' );
if ( null === $value ) {
// No woo-context directive.
return;
}
$new_context = json_decode( $value, true );
$context->set_context( $new_context );
}

View File

@@ -1,49 +0,0 @@
<?php
require_once __DIR__ . '/class-woo-directive-store.php';
function woo_directives_store( $data ) {
Woo_Directive_Store::merge_data( $data );
}
function woo_directives_evaluate( string $path, array $context = array() ) {
$current = array_merge(
Woo_Directive_Store::get_data(),
array( 'context' => $context )
);
$array = explode( '.', $path );
foreach ( $array as $p ) {
if ( isset( $current[ $p ] ) ) {
$current = $current[ $p ];
} else {
return null;
}
}
return $current;
}
function woo_directives_set_style( $style, $name, $value ) {
$style_assignments = explode( ';', $style );
$modified = false;
foreach ( $style_assignments as $style_assignment ) {
list( $style_name ) = explode( ':', $style_assignment );
if ( trim( $style_name ) === $name ) {
$style_assignment = $style_name . ': ' . $value;
$modified = true;
break;
}
}
if ( ! $modified ) {
$new_style_assignment = $name . ': ' . $value;
// If the last element is empty or whitespace-only, we insert
// the new "key: value" pair before it.
if ( empty( trim( end( $style_assignments ) ) ) ) {
array_splice( $style_assignments, - 1, 0, $new_style_assignment );
} else {
array_push( $style_assignments, $new_style_assignment );
}
}
return implode( ';', $style_assignments );
}

View File

@@ -1,85 +0,0 @@
<?php
require_once __DIR__ . '/class-woo-directive-context.php';
function woo_process_directives( $tags, $prefix, $tag_directives, $attribute_directives ) {
$context = new Woo_Directive_Context;
$tag_stack = array();
while ( $tags->next_tag( array( 'tag_closers' => 'visit' ) ) ) {
$tag_name = strtolower( $tags->get_tag() );
if ( array_key_exists( $tag_name, $tag_directives ) ) {
call_user_func( $tag_directives[ $tag_name ], $tags, $context );
} else {
// Components can't have directives (unless we change our mind about this).
// Is this a tag that closes the latest opening tag?
if ( $tags->is_tag_closer() ) {
if ( 0 === count( $tag_stack ) ) {
continue;
}
list( $latest_opening_tag_name, $attributes ) = end( $tag_stack );
if ( $latest_opening_tag_name === $tag_name ) {
array_pop( $tag_stack );
// If the matching opening tag didn't have any attribute directives,
// we move on.
if ( 0 === count( $attributes ) ) {
continue;
}
}
} else {
// Helper that removes the part after the colon before looking
// for the directive processor inside `$attribute_directives`.
$get_directive_type = function ( $attr ) {
return strtok( $attr, ':' );
};
$attributes = $tags->get_attribute_names_with_prefix( $prefix );
$attributes = array_map( $get_directive_type, $attributes );
$attributes = array_intersect( $attributes, array_keys( $attribute_directives ) );
// If this is an open tag, and if it either has attribute directives,
// or if we're inside a tag that does, take note of this tag and its attribute
// directives so we can call its directive processor once we encounter the
// matching closing tag.
if (
! woo_directives_is_html_void_element( $tags->get_tag() ) &&
( 0 !== count( $attributes ) || 0 !== count( $tag_stack ) )
) {
$tag_stack[] = array( $tag_name, $attributes );
}
}
foreach ( $attributes as $attribute ) {
call_user_func( $attribute_directives[ $attribute ], $tags, $context );
}
}
}
return $tags;
}
// See e.g. https://github.com/WordPress/gutenberg/pull/47573.
function woo_directives_is_html_void_element( $tag_name ) {
switch ( $tag_name ) {
case 'AREA':
case 'BASE':
case 'BR':
case 'COL':
case 'EMBED':
case 'HR':
case 'IMG':
case 'INPUT':
case 'LINK':
case 'META':
case 'SOURCE':
case 'TRACK':
case 'WBR':
return true;
default:
return false;
}
}

View File

@@ -0,0 +1,4 @@
<?php
require __DIR__ . '/class-wc-interactivity-store.php';
require __DIR__ . '/store.php';
require __DIR__ . '/scripts.php';

View File

@@ -0,0 +1,50 @@
<?php
/**
* Move interactive scripts to the footer. This is a temporary measure to make
* it work with `wc_store` and it should be replaced with deferred scripts or
* modules.
*/
function woocommerce_interactivity_move_interactive_scripts_to_the_footer() {
// Move the @woocommerce/interactivity package to the footer.
wp_script_add_data( 'wc-interactivity', 'group', 1 );
// Move all the view scripts of the interactive blocks to the footer.
$registered_blocks = \WP_Block_Type_Registry::get_instance()->get_all_registered();
foreach ( array_values( $registered_blocks ) as $block ) {
if ( isset( $block->supports['interactivity'] ) && $block->supports['interactivity'] ) {
foreach ( $block->view_script_handles as $handle ) {
wp_script_add_data( $handle, 'group', 1 );
}
}
}
}
add_action( 'wp_enqueue_scripts', 'woocommerce_interactivity_move_interactive_scripts_to_the_footer', 11 );
/**
* Register the Interactivity API runtime and make it available to be enqueued
* as a dependency in interactive blocks.
*/
function woocommerce_interactivity_register_runtime() {
$plugin_path = \Automattic\WooCommerce\Blocks\Package::get_path();
$plugin_url = plugin_dir_url( $plugin_path . '/index.php' );
$file = 'build/wc-interactivity.js';
$file_path = $plugin_path . $file;
$file_url = $plugin_url . $file;
if ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG && file_exists( $file_path ) ) {
$version = filemtime( $file_path );
} else {
$version = \Automattic\WooCommerce\Blocks\Package::get_version();
}
wp_register_script(
'wc-interactivity',
$file_url,
array(),
$version,
true
);
}
add_action( 'wp_enqueue_scripts', 'woocommerce_interactivity_register_runtime' );

View File

@@ -0,0 +1,19 @@
<?php
/**
* Merge data with the exsisting store.
*
* @param array $data Data that will be merged with the exsisting store.
*
* @return $data The current store data.
*/
function wc_store( $data = null ) {
if ( $data ) {
WC_Interactivity_Store::merge_data( $data );
}
return WC_Interactivity_Store::get_data();
}
/**
* Render the Interactivity API store in the frontend.
*/
add_action( 'wp_footer', array( 'WC_Interactivity_Store', 'render' ), 8 );

View File

@@ -1,121 +0,0 @@
<?php
require_once __DIR__ . '/directives/class-woo-directive-context.php';
require_once __DIR__ . '/directives/class-woo-directive-store.php';
require_once __DIR__ . '/directives/woo-process-directives.php';
require_once __DIR__ . '/directives/attributes/woo-bind.php';
require_once __DIR__ . '/directives/attributes/woo-class.php';
require_once __DIR__ . '/directives/attributes/woo-style.php';
require_once __DIR__ . '/directives/tags/woo-context.php';
/**
* Register the Interactivity API scripts. These files are enqueued when a block
* defines `woo-directives-runtime` as a dependency.
*/
function woo_directives_register_scripts() {
wp_register_script(
'woo-directives-vendors',
plugins_url( '../../build/woo-directives-vendors.js', __FILE__ ),
array(),
'1.0.0',
true
);
wp_register_script(
'woo-directives-runtime',
plugins_url( '../../build/woo-directives-runtime.js', __FILE__ ),
array( 'woo-directives-vendors' ),
'1.0.0',
true
);
}
add_action( 'init', 'woo_directives_register_scripts' );
function woo_directives_get_client_side_navigation() {
static $client_side_navigation = null;
if ( is_null( $client_side_navigation ) ) {
$client_side_navigation = apply_filters( 'client_side_navigation', false );
}
return $client_side_navigation;
}
function woo_directives_add_client_side_navigation_meta_tag() {
if ( woo_directives_get_client_side_navigation() ) {
echo '<meta itemprop="woo-client-side-navigation" content="active">';
}
}
add_action( 'wp_head', 'woo_directives_add_client_side_navigation_meta_tag' );
function woo_directives_mark_interactive_blocks( $block_content, $block, $instance ) {
if ( woo_directives_get_client_side_navigation() ) {
return $block_content;
}
// Append the `data-woo-ignore` attribute for inner blocks of interactive blocks.
if ( isset( $instance->parsed_block['isolated'] ) ) {
$w = new WP_HTML_Tag_Processor( $block_content );
$w->next_tag();
$w->set_attribute( 'data-woo-ignore', true );
$block_content = (string) $w;
}
// Return if it's not interactive.
if ( ! block_has_support( $instance->block_type, array( 'interactivity' ) ) ) {
return $block_content;
}
// Add the `data-woo-island` attribute if it's interactive.
$w = new WP_HTML_Tag_Processor( $block_content );
$w->next_tag();
$w->set_attribute( 'data-woo-island', true );
return (string) $w;
}
add_filter( 'render_block', 'woo_directives_mark_interactive_blocks', 10, 3 );
/**
* Add a flag to mark inner blocks of isolated interactive blocks.
*/
function woo_directives_inner_blocks( $parsed_block, $source_block, $parent_block ) {
if (
isset( $parent_block ) &&
block_has_support(
$parent_block->block_type,
array(
'interactivity',
'isolated',
)
)
) {
$parsed_block['isolated'] = true;
}
return $parsed_block;
}
add_filter( 'render_block_data', 'woo_directives_inner_blocks', 10, 3 );
function woo_process_directives_in_block( $block_content ) {
$tag_directives = array(
'woo-context' => 'process_woo_context_tag',
);
$attribute_directives = array(
'data-woo-context' => 'process_woo_context_attribute',
'data-woo-bind' => 'process_woo_bind',
'data-woo-class' => 'process_woo_class',
'data-woo-style' => 'process_woo_style',
);
$tags = new WP_HTML_Tag_Processor( $block_content );
$tags = woo_process_directives( $tags, 'data-woo-', $tag_directives, $attribute_directives );
return $tags->get_updated_html();
}
add_filter(
'render_block',
'woo_process_directives_in_block',
10,
1
);
add_action( 'wp_footer', array( 'Woo_Directive_Store', 'render' ), 9 );

View File

@@ -7,7 +7,6 @@ namespace Automattic\WooCommerce\Blocks;
* @since 2.5.0
*/
class Migration {
/**
* DB updates and callbacks that need to be run per version.
*
@@ -17,10 +16,9 @@ class Migration {
* @var array
*/
private $db_upgrades = array(
// We don't need to do the following migration yet, but we'll keep it here for future use.
// '7.10.0' => array(
// 'wc_blocks_update_710_blockified_product_grid_block',
// ).
'10.3.0' => array(
'wc_blocks_update_1030_blockified_product_grid_block',
),
);
/**
@@ -30,8 +28,19 @@ class Migration {
*/
public function run_migrations() {
$current_db_version = get_option( Options::WC_BLOCK_VERSION, '' );
$schema_version = get_option( 'wc_blocks_db_schema_version', '' );
// This check is necessary because the version was not being set in the database until 10.3.0.
// Checking wc_blocks_db_schema_version determines if it's a fresh install (value will be empty)
// or an update from WC Blocks older than 10.3.0 (it will have some value). In the latter scenario
// we should run the migration.
// We can remove this check in the next months.
if ( ! empty( $schema_version ) && ( empty( $current_db_version ) ) ) {
$this->wc_blocks_update_1030_blockified_product_grid_block();
}
if ( empty( $current_db_version ) ) {
// This is a fresh install, so we don't need to run any migrations.
return;
}
@@ -47,7 +56,7 @@ class Migration {
/**
* Set a flag to indicate if the blockified Product Grid Block should be rendered by default.
*/
public static function wc_blocks_update_710_blockified_product_grid_block() {
public static function wc_blocks_update_1030_blockified_product_grid_block() {
update_option( Options::WC_BLOCK_USE_BLOCKIFIED_PRODUCT_GRID_BLOCK_AS_TEMPLATE, wc_bool_to_string( false ) );
}
}

View File

@@ -109,7 +109,7 @@ class Package {
NewPackage::class,
function ( $container ) {
// leave for automated version bumping.
$version = '10.0.6';
$version = '10.9.3';
return new NewPackage(
$version,
dirname( __DIR__ ),

View File

@@ -37,13 +37,12 @@ class Api {
public function __construct( PaymentMethodRegistry $payment_method_registry, AssetDataRegistry $asset_registry ) {
$this->payment_method_registry = $payment_method_registry;
$this->asset_registry = $asset_registry;
$this->init();
}
/**
* Initialize class features.
*/
protected function init() {
public function init() {
add_action( 'init', array( $this->payment_method_registry, 'initialize' ), 5 );
add_filter( 'woocommerce_blocks_register_script_dependencies', array( $this, 'add_payment_method_script_dependencies' ), 10, 2 );
add_action( 'woocommerce_blocks_checkout_enqueue_data', array( $this, 'add_payment_method_script_data' ) );
@@ -80,13 +79,13 @@ class Api {
* Add payment method data to Asset Registry.
*/
public function add_payment_method_script_data() {
// Enqueue the order of enabled gateways as `paymentGatewaySortOrder`.
if ( ! $this->asset_registry->exists( 'paymentGatewaySortOrder' ) ) {
// Enqueue the order of enabled gateways.
if ( ! $this->asset_registry->exists( 'paymentMethodSortOrder' ) ) {
// We use payment_gateways() here to get the sort order of all enabled gateways. Some may be
// programmatically disabled later on, but we still need to know where the enabled ones are in the list.
$payment_gateways = WC()->payment_gateways->payment_gateways();
$enabled_gateways = array_filter( $payment_gateways, array( $this, 'is_payment_gateway_enabled' ) );
$this->asset_registry->add( 'paymentGatewaySortOrder', array_keys( $enabled_gateways ) );
$this->asset_registry->add( 'paymentMethodSortOrder', array_keys( $enabled_gateways ) );
}
// Enqueue all registered gateway data (settings/config etc).

View File

@@ -59,9 +59,9 @@ final class PaymentMethodRegistry extends IntegrationRegistry {
$payment_methods = $this->get_all_active_registered();
foreach ( $payment_methods as $payment_method ) {
$script_data[ $payment_method->get_name() . '_data' ] = $payment_method->get_payment_method_data();
$script_data[ $payment_method->get_name() ] = $payment_method->get_payment_method_data();
}
return array_filter( $script_data );
return array( 'paymentMethodData' => array_filter( $script_data ) );
}
}

View File

@@ -8,6 +8,20 @@ use WC_Shipping_Method;
*/
class PickupLocation extends WC_Shipping_Method {
/**
* Pickup locations.
*
* @var array
*/
protected $pickup_locations = [];
/**
* Cost
*
* @var string
*/
protected $cost = '';
/**
* Constructor.
*/
@@ -31,6 +45,48 @@ class PickupLocation extends WC_Shipping_Method {
add_filter( 'woocommerce_attribute_label', array( $this, 'translate_meta_data' ), 10, 3 );
}
/**
* Checks if a given address is complete.
*
* @param array $address Address.
* @return bool
*/
protected function has_valid_pickup_location( $address ) {
// Normalize address.
$address_fields = wp_parse_args(
(array) $address,
array(
'city' => '',
'postcode' => '',
'state' => '',
'country' => '',
)
);
// Country is always required.
if ( empty( $address_fields['country'] ) ) {
return false;
}
// If all fields are provided, we can skip further checks.
if ( ! empty( $address_fields['city'] ) && ! empty( $address_fields['postcode'] ) && ! empty( $address_fields['state'] ) ) {
return true;
}
// Check validity based on requirements for the country.
$country_address_fields = wc()->countries->get_address_fields( $address_fields['country'], 'shipping_' );
foreach ( $country_address_fields as $field_name => $field ) {
$key = str_replace( 'shipping_', '', $field_name );
if ( isset( $address_fields[ $key ] ) && true === $field['required'] && empty( $address_fields[ $key ] ) ) {
return false;
}
}
return true;
}
/**
* Calculate shipping.
*
@@ -51,7 +107,7 @@ class PickupLocation extends WC_Shipping_Method {
'cost' => $this->cost,
'meta_data' => array(
'pickup_location' => wp_kses_post( $location['name'] ),
'pickup_address' => wc()->countries->get_formatted_address( $location['address'], ', ' ),
'pickup_address' => $this->has_valid_pickup_location( $location['address'] ) ? wc()->countries->get_formatted_address( $location['address'], ', ' ) : '',
'pickup_details' => wp_kses_post( $location['details'] ),
),
)

View File

@@ -27,6 +27,13 @@ class ShippingController {
*/
protected $asset_data_registry;
/**
* Whether local pickup is enabled.
*
* @var bool
*/
private $local_pickup_enabled;
/**
* Constructor.
*
@@ -36,6 +43,8 @@ class ShippingController {
public function __construct( AssetApi $asset_api, AssetDataRegistry $asset_data_registry ) {
$this->asset_api = $asset_api;
$this->asset_data_registry = $asset_data_registry;
$this->local_pickup_enabled = LocalPickupUtils::is_local_pickup_enabled();
}
/**
@@ -81,7 +90,7 @@ class ShippingController {
* @return boolean Whether shipping cost calculation should require an address to be entered before calculating.
*/
public function override_cost_requires_address_option( $value ) {
if ( CartCheckoutUtils::is_checkout_block_default() ) {
if ( CartCheckoutUtils::is_checkout_block_default() && $this->local_pickup_enabled ) {
return 'no';
}
return $value;
@@ -94,7 +103,7 @@ class ShippingController {
* @return boolean Whether shipping should continue to be enabled/disabled.
*/
public function force_shipping_enabled( $enabled ) {
if ( CartCheckoutUtils::is_checkout_block_default() ) {
if ( CartCheckoutUtils::is_checkout_block_default() && $this->local_pickup_enabled ) {
return true;
}
return $enabled;
@@ -126,9 +135,13 @@ class ShippingController {
$location = $shipping_method->get_meta( 'pickup_location' );
$address = $shipping_method->get_meta( 'pickup_address' );
if ( ! $address ) {
return $return;
}
return sprintf(
// Translators: %s location name.
__( 'Pickup from <strong>%s</strong>:', 'woocommerce' ),
__( 'Collection from <strong>%s</strong>:', 'woocommerce' ),
$location
) . '<br/><address>' . str_replace( ',', ',<br/>', $address ) . '</address><br/>' . $details;
}
@@ -141,48 +154,31 @@ class ShippingController {
*/
public function remove_shipping_settings( $settings ) {
// Do not add the "Hide shipping costs until an address is entered" setting if the Checkout block is not used on the WC checkout page.
if ( CartCheckoutUtils::is_checkout_block_default() ) {
$settings = array_filter(
$settings,
function( $setting ) {
return ! in_array(
$setting['id'],
array(
'woocommerce_shipping_cost_requires_address',
),
true
);
}
);
}
// Do not add the shipping calculator setting if the Cart block is not used on the WC cart page.
if ( CartCheckoutUtils::is_cart_block_default() ) {
// If the Cart is default, but not the checkout, we should ensure the 'Calculations' title is added to the
// `woocommerce_shipping_cost_requires_address` options group, since it is attached to the
// `woocommerce_enable_shipping_calc` option that we're going to remove later.
if ( ! CartCheckoutUtils::is_checkout_block_default() ) {
$calculations_title = '';
// Ensure the 'Calculations' title is added to the `woocommerce_shipping_cost_requires_address` options
// group, since it is attached to the `woocommerce_enable_shipping_calc` option that gets removed if the
// Cart block is in use.
$calculations_title = '';
// Get Calculations title so we can add it to 'Hide shipping costs until an address is entered' option.
foreach ( $settings as $setting ) {
if ( 'woocommerce_enable_shipping_calc' === $setting['id'] ) {
$calculations_title = $setting['title'];
break;
}
}
// Add Calculations title to 'Hide shipping costs until an address is entered' option.
foreach ( $settings as $index => $setting ) {
if ( 'woocommerce_shipping_cost_requires_address' === $setting['id'] ) {
$settings[ $index ]['title'] = $calculations_title;
$settings[ $index ]['checkboxgroup'] = 'start';
break;
}
// Get Calculations title so we can add it to 'Hide shipping costs until an address is entered' option.
foreach ( $settings as $setting ) {
if ( 'woocommerce_enable_shipping_calc' === $setting['id'] ) {
$calculations_title = $setting['title'];
break;
}
}
// Add Calculations title to 'Hide shipping costs until an address is entered' option.
foreach ( $settings as $index => $setting ) {
if ( 'woocommerce_shipping_cost_requires_address' === $setting['id'] ) {
$settings[ $index ]['title'] = $calculations_title;
$settings[ $index ]['checkboxgroup'] = 'start';
break;
}
}
$settings = array_filter(
$settings,
function( $setting ) {
@@ -196,6 +192,18 @@ class ShippingController {
}
);
}
if ( CartCheckoutUtils::is_checkout_block_default() && $this->local_pickup_enabled ) {
foreach ( $settings as $index => $setting ) {
if ( 'woocommerce_shipping_cost_requires_address' === $setting['id'] ) {
$settings[ $index ]['desc'] .= ' (' . __( 'Not available when using WooCommerce Blocks Local Pickup', 'woocommerce' ) . ')';
$settings[ $index ]['disabled'] = true;
$settings[ $index ]['value'] = 'no';
break;
}
}
}
return $settings;
}
@@ -456,6 +464,10 @@ class ShippingController {
if ( count( $valid_packages ) !== count( $packages ) ) {
$packages = array_map(
function( $package ) {
if ( ! is_array( $package['rates'] ) ) {
$package['rates'] = [];
return $package;
}
$package['rates'] = array_filter(
$package['rates'],
function( $rate ) {

View File

@@ -2,17 +2,113 @@
namespace Automattic\WooCommerce\StoreApi;
use Automattic\WooCommerce\StoreApi\Utilities\RateLimits;
use Automattic\WooCommerce\StoreApi\Utilities\JsonWebToken;
/**
* Authentication class.
*/
class Authentication {
/**
* Hook into WP lifecycle events.
* Hook into WP lifecycle events. This is hooked by the StoreAPI class on `rest_api_init`.
*/
public function init() {
if ( ! $this->is_request_to_store_api() ) {
return;
}
add_filter( 'rest_authentication_errors', array( $this, 'check_authentication' ) );
add_action( 'set_logged_in_cookie', array( $this, 'set_logged_in_cookie' ) );
add_filter( 'rest_pre_serve_request', array( $this, 'send_cors_headers' ), 10, 3 );
add_filter( 'rest_allowed_cors_headers', array( $this, 'allowed_cors_headers' ) );
// Remove the default CORS headers--we will add our own.
remove_filter( 'rest_pre_serve_request', 'rest_send_cors_headers' );
}
/**
* Add allowed cors headers for store API headers.
*
* @param array $allowed_headers Allowed headers.
* @return array
*/
public function allowed_cors_headers( $allowed_headers ) {
$allowed_headers[] = 'Cart-Token';
$allowed_headers[] = 'Nonce';
$allowed_headers[] = 'X-WC-Store-API-Nonce';
return $allowed_headers;
}
/**
* Add CORS headers to a response object.
*
* These checks prevent access to the Store API from non-allowed origins. By default, the WordPress REST API allows
* access from any origin. Because some Store API routes return PII, we need to add our own CORS headers.
*
* Allowed origins can be changed using the WordPress `allowed_http_origins` or `allowed_http_origin` filters if
* access needs to be granted to other domains.
*
* Users of valid Cart Tokens are also allowed access from any origin.
*
* @param bool $value Whether the request has already been served.
* @param \WP_HTTP_Response $result Result to send to the client. Usually a `WP_REST_Response`.
* @param \WP_REST_Request $request Request used to generate the response.
* @return bool
*/
public function send_cors_headers( $value, $result, $request ) {
$origin = get_http_origin();
if ( 'null' !== $origin ) {
$origin = esc_url_raw( $origin );
}
// Send standard CORS headers.
$server = rest_get_server();
$server->send_header( 'Access-Control-Allow-Methods', 'OPTIONS, GET, POST, PUT, PATCH, DELETE' );
$server->send_header( 'Access-Control-Allow-Credentials', 'true' );
$server->send_header( 'Vary', 'Origin', false );
// Allow preflight requests, certain http origins, and any origin if a cart token is present. Preflight requests
// are allowed because we'll be unable to validate cart token headers at that point.
if ( $this->is_preflight() || $this->has_valid_cart_token( $request ) || is_allowed_http_origin( $origin ) ) {
$server->send_header( 'Access-Control-Allow-Origin', $origin );
}
// Exit early during preflight requests. This is so someone cannot access API data by sending an OPTIONS request
// with preflight headers and a _GET property to override the method.
if ( $this->is_preflight() ) {
exit;
}
return $value;
}
/**
* Is the request a preflight request? Checks the request method
*
* @return boolean
*/
protected function is_preflight() {
return isset( $_SERVER['REQUEST_METHOD'], $_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD'], $_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'], $_SERVER['HTTP_ORIGIN'] ) && 'OPTIONS' === $_SERVER['REQUEST_METHOD'];
}
/**
* Checks if we're using a cart token to access the Store API.
*
* @param \WP_REST_Request $request Request object.
* @return boolean
*/
protected function has_valid_cart_token( \WP_REST_Request $request ) {
$cart_token = $request->get_header( 'Cart-Token' );
return $cart_token && JsonWebToken::validate( $cart_token, $this->get_cart_token_secret() );
}
/**
* Gets the secret for the cart token using wp_salt.
*
* @return string
*/
protected function get_cart_token_secret() {
return '@' . wp_salt();
}
/**
@@ -22,10 +118,6 @@ class Authentication {
* @return \WP_Error|null|bool
*/
public function check_authentication( $result ) {
if ( ! $this->is_request_to_store_api() ) {
return $result;
}
// Enable Rate Limiting for logged-in users without 'edit posts' capability.
if ( ! current_user_can( 'edit_posts' ) ) {
$result = $this->apply_rate_limiting( $result );
@@ -42,7 +134,7 @@ class Authentication {
* @param string $logged_in_cookie The value for the logged in cookie.
*/
public function set_logged_in_cookie( $logged_in_cookie ) {
if ( ! defined( 'LOGGED_IN_COOKIE' ) || ! $this->is_request_to_store_api() ) {
if ( ! defined( 'LOGGED_IN_COOKIE' ) ) {
return;
}
$_COOKIE[ LOGGED_IN_COOKIE ] = $logged_in_cookie;

View File

@@ -114,15 +114,17 @@ abstract class AbstractCartRoute extends AbstractRoute {
}
}
if ( is_wp_error( $response ) ) {
$response = $this->error_to_response( $response );
}
// For update requests, this will recalculate cart totals and sync draft orders with the current cart.
if ( $this->is_update_request( $request ) ) {
$this->cart_updated( $request );
}
return $this->add_response_headers( $response );
// Format error responses.
if ( is_wp_error( $response ) ) {
$response = $this->error_to_response( $response );
}
return $this->add_response_headers( rest_ensure_response( $response ) );
}
/**
@@ -232,7 +234,9 @@ abstract class AbstractCartRoute extends AbstractRoute {
$draft_order = $this->get_draft_order();
if ( $draft_order ) {
$this->order_controller->update_order_from_cart( $draft_order );
// This does not trigger a recalculation of the cart--endpoints should have already done so before returning
// the cart response.
$this->order_controller->update_order_from_cart( $draft_order, false );
wc_do_deprecated_action(
'woocommerce_blocks_cart_update_order_from_request',

View File

@@ -163,9 +163,10 @@ abstract class AbstractRoute implements RouteInterface {
*
* @throws RouteException On error.
* @param \WP_REST_Request $request Request object.
* @return \WP_REST_Response
*/
protected function get_route_response( \WP_REST_Request $request ) {
throw new RouteException( 'woocommerce_rest_invalid_endpoint', __( 'Method not implemented', 'woocommerce' ), 404 );
return $this->get_route_error_response( 'woocommerce_rest_invalid_endpoint', __( 'Method not implemented', 'woocommerce' ), 404 );
}
/**
@@ -175,9 +176,10 @@ abstract class AbstractRoute implements RouteInterface {
*
* @throws RouteException On error.
* @param \WP_REST_Request $request Request object.
* @return \WP_REST_Response
*/
protected function get_route_post_response( \WP_REST_Request $request ) {
throw new RouteException( 'woocommerce_rest_invalid_endpoint', __( 'Method not implemented', 'woocommerce' ), 404 );
return $this->get_route_error_response( 'woocommerce_rest_invalid_endpoint', __( 'Method not implemented', 'woocommerce' ), 404 );
}
/**
@@ -187,9 +189,10 @@ abstract class AbstractRoute implements RouteInterface {
*
* @throws RouteException On error.
* @param \WP_REST_Request $request Request object.
* @return \WP_REST_Response
*/
protected function get_route_update_response( \WP_REST_Request $request ) {
throw new RouteException( 'woocommerce_rest_invalid_endpoint', __( 'Method not implemented', 'woocommerce' ), 404 );
return $this->get_route_error_response( 'woocommerce_rest_invalid_endpoint', __( 'Method not implemented', 'woocommerce' ), 404 );
}
/**
@@ -199,9 +202,10 @@ abstract class AbstractRoute implements RouteInterface {
*
* @throws RouteException On error.
* @param \WP_REST_Request $request Request object.
* @return \WP_REST_Response
*/
protected function get_route_delete_response( \WP_REST_Request $request ) {
throw new RouteException( 'woocommerce_rest_invalid_endpoint', __( 'Method not implemented', 'woocommerce' ), 404 );
return $this->get_route_error_response( 'woocommerce_rest_invalid_endpoint', __( 'Method not implemented', 'woocommerce' ), 404 );
}
/**

View File

@@ -37,7 +37,7 @@ class CartSelectShippingRate extends AbstractCartRoute {
'args' => [
'package_id' => array(
'description' => __( 'The ID of the package being shipped. Leave blank to apply to all packages.', 'woocommerce' ),
'type' => [ 'integer', 'string' ],
'type' => [ 'integer', 'string', 'null' ],
'required' => false,
),
'rate_id' => [
@@ -69,8 +69,8 @@ class CartSelectShippingRate extends AbstractCartRoute {
}
$cart = $this->cart_controller->get_cart_instance();
$package_id = isset( $request['package_id'] ) ? wc_clean( wp_unslash( $request['package_id'] ) ) : null;
$rate_id = wc_clean( wp_unslash( $request['rate_id'] ) );
$package_id = isset( $request['package_id'] ) ? sanitize_text_field( wp_unslash( $request['package_id'] ) ) : null;
$rate_id = sanitize_text_field( wp_unslash( $request['rate_id'] ) );
try {
if ( ! is_null( $package_id ) ) {

View File

@@ -7,7 +7,7 @@ use Automattic\WooCommerce\StoreApi\Utilities\ValidationUtils;
/**
* CartUpdateCustomer class.
*
* Updates the customer billing and shipping address and returns an updated cart--things such as taxes may be recalculated.
* Updates the customer billing and shipping addresses, recalculates the cart totals, and returns an updated cart.
*/
class CartUpdateCustomer extends AbstractCartRoute {
use DraftOrderTrait;

View File

@@ -1,7 +1,6 @@
<?php
namespace Automattic\WooCommerce\StoreApi\Routes\V1;
use Automattic\WooCommerce\StoreApi\Payments\PaymentContext;
use Automattic\WooCommerce\StoreApi\Payments\PaymentResult;
use Automattic\WooCommerce\StoreApi\Exceptions\InvalidStockLevelsInCartException;
use Automattic\WooCommerce\StoreApi\Exceptions\InvalidCartException;
@@ -9,12 +8,14 @@ use Automattic\WooCommerce\StoreApi\Exceptions\RouteException;
use Automattic\WooCommerce\StoreApi\Utilities\DraftOrderTrait;
use Automattic\WooCommerce\Checkout\Helpers\ReserveStock;
use Automattic\WooCommerce\Checkout\Helpers\ReserveStockException;
use Automattic\WooCommerce\StoreApi\Utilities\CheckoutTrait;
/**
* Checkout class.
*/
class Checkout extends AbstractCartRoute {
use DraftOrderTrait;
use CheckoutTrait;
/**
* The route identifier.
@@ -138,29 +139,6 @@ class Checkout extends AbstractCartRoute {
return $this->add_response_headers( $response );
}
/**
* Prepare a single item for response. Handles setting the status based on the payment result.
*
* @param mixed $item Item to format to schema.
* @param \WP_REST_Request $request Request object.
* @return \WP_REST_Response $response Response data.
*/
public function prepare_item_for_response( $item, \WP_REST_Request $request ) {
$response = parent::prepare_item_for_response( $item, $request );
$status_codes = [
'success' => 200,
'pending' => 202,
'failure' => 400,
'error' => 500,
];
if ( isset( $item->payment_result ) && $item->payment_result instanceof PaymentResult ) {
$response->set_status( $status_codes[ $item->payment_result->status ] ?? 200 );
}
return $response;
}
/**
* Convert the cart into a new draft order, or update an existing draft order, and return an updated cart response.
*
@@ -352,7 +330,7 @@ class Checkout extends AbstractCartRoute {
if ( ! $this->order ) {
$this->order = $this->order_controller->create_order_from_cart();
} else {
$this->order_controller->update_order_from_cart( $this->order );
$this->order_controller->update_order_from_cart( $this->order, true );
}
wc_do_deprecated_action(
@@ -466,120 +444,6 @@ class Checkout extends AbstractCartRoute {
$customer->save();
}
/**
* Update the current order using the posted values from the request.
*
* @param \WP_REST_Request $request Full details about the request.
*/
private function update_order_from_request( \WP_REST_Request $request ) {
$this->order->set_customer_note( $request['customer_note'] ?? '' );
$this->order->set_payment_method( $this->get_request_payment_method_id( $request ) );
wc_do_deprecated_action(
'__experimental_woocommerce_blocks_checkout_update_order_from_request',
array(
$this->order,
$request,
),
'6.3.0',
'woocommerce_store_api_checkout_update_order_from_request',
'This action was deprecated in WooCommerce Blocks version 6.3.0. Please use woocommerce_store_api_checkout_update_order_from_request instead.'
);
wc_do_deprecated_action(
'woocommerce_blocks_checkout_update_order_from_request',
array(
$this->order,
$request,
),
'7.2.0',
'woocommerce_store_api_checkout_update_order_from_request',
'This action was deprecated in WooCommerce Blocks version 7.2.0. Please use woocommerce_store_api_checkout_update_order_from_request instead.'
);
/**
* Fires when the Checkout Block/Store API updates an order's from the API request data.
*
* This hook gives extensions the chance to update orders based on the data in the request. This can be used in
* conjunction with the ExtendSchema class to post custom data and then process it.
*
* @since 7.2.0
*
* @param \WC_Order $order Order object.
* @param \WP_REST_Request $request Full details about the request.
*/
do_action( 'woocommerce_store_api_checkout_update_order_from_request', $this->order, $request );
$this->order->save();
}
/**
* For orders which do not require payment, just update status.
*
* @param \WP_REST_Request $request Request object.
* @param PaymentResult $payment_result Payment result object.
*/
private function process_without_payment( \WP_REST_Request $request, PaymentResult $payment_result ) {
// Transition the order to pending, and then completed. This ensures transactional emails fire for pending_to_complete events.
$this->order->update_status( 'pending' );
$this->order->payment_complete();
// Mark the payment as successful.
$payment_result->set_status( 'success' );
$payment_result->set_redirect_url( $this->order->get_checkout_order_received_url() );
}
/**
* Fires an action hook instructing active payment gateways to process the payment for an order and provide a result.
*
* @throws RouteException On error.
*
* @param \WP_REST_Request $request Request object.
* @param PaymentResult $payment_result Payment result object.
*/
private function process_payment( \WP_REST_Request $request, PaymentResult $payment_result ) {
try {
// Transition the order to pending before making payment.
$this->order->update_status( 'pending' );
// Prepare the payment context object to pass through payment hooks.
$context = new PaymentContext();
$context->set_payment_method( $this->get_request_payment_method_id( $request ) );
$context->set_payment_data( $this->get_request_payment_data( $request ) );
$context->set_order( $this->order );
/**
* Process payment with context.
*
* @hook woocommerce_rest_checkout_process_payment_with_context
*
* @throws \Exception If there is an error taking payment, an \Exception object can be thrown with an error message.
*
* @param PaymentContext $context Holds context for the payment, including order ID and payment method.
* @param PaymentResult $payment_result Result object for the transaction.
*/
do_action_ref_array( 'woocommerce_rest_checkout_process_payment_with_context', [ $context, &$payment_result ] );
if ( ! $payment_result instanceof PaymentResult ) {
throw new RouteException( 'woocommerce_rest_checkout_invalid_payment_result', __( 'Invalid payment result received from payment method.', 'woocommerce' ), 500 );
}
} catch ( \Exception $e ) {
throw new RouteException( 'woocommerce_rest_checkout_process_payment_error', $e->getMessage(), 402 );
}
}
/**
* Gets the chosen payment method ID from the request.
*
* @throws RouteException On error.
* @param \WP_REST_Request $request Request object.
* @return string
*/
private function get_request_payment_method_id( \WP_REST_Request $request ) {
$payment_method = $this->get_request_payment_method( $request );
return is_null( $payment_method ) ? '' : $payment_method->id;
}
/**
* Gets the chosen payment method from the request.
*
@@ -604,12 +468,14 @@ class Checkout extends AbstractCartRoute {
}
if ( ! isset( $available_gateways[ $request_payment_method ] ) ) {
$all_payment_gateways = WC()->payment_gateways->payment_gateways();
$gateway_title = isset( $all_payment_gateways[ $request_payment_method ] ) ? $all_payment_gateways[ $request_payment_method ]->get_title() : $request_payment_method;
throw new RouteException(
'woocommerce_rest_checkout_payment_method_disabled',
sprintf(
// Translators: %s Payment method ID.
__( 'The %s payment gateway is not available.', 'woocommerce' ),
esc_html( $request_payment_method )
__( '%s is not available for this order—please choose a different payment method', 'woocommerce' ),
esc_html( $gateway_title )
),
400
);
@@ -618,26 +484,6 @@ class Checkout extends AbstractCartRoute {
return $available_gateways[ $request_payment_method ];
}
/**
* Gets and formats payment request data.
*
* @param \WP_REST_Request $request Request object.
* @return array
*/
private function get_request_payment_data( \WP_REST_Request $request ) {
static $payment_data = [];
if ( ! empty( $payment_data ) ) {
return $payment_data;
}
if ( ! empty( $request['payment_data'] ) ) {
foreach ( $request['payment_data'] as $data ) {
$payment_data[ sanitize_key( $data['key'] ) ] = wc_clean( $data['value'] );
}
}
return $payment_data;
}
/**
* Order processing relating to customer account.
*

View File

@@ -0,0 +1,266 @@
<?php
namespace Automattic\WooCommerce\StoreApi\Routes\V1;
use Automattic\WooCommerce\StoreApi\Payments\PaymentResult;
use Automattic\WooCommerce\StoreApi\Exceptions\InvalidStockLevelsInCartException;
use Automattic\WooCommerce\StoreApi\Exceptions\RouteException;
use Automattic\WooCommerce\StoreApi\Utilities\OrderAuthorizationTrait;
use Automattic\WooCommerce\StoreApi\Utilities\CheckoutTrait;
/**
* CheckoutOrder class.
*/
class CheckoutOrder extends AbstractCartRoute {
use OrderAuthorizationTrait;
use CheckoutTrait;
/**
* The route identifier.
*
* @var string
*/
const IDENTIFIER = 'checkout-order';
/**
* The routes schema.
*
* @var string
*/
const SCHEMA_TYPE = 'checkout-order';
/**
* Holds the current order being processed.
*
* @var \WC_Order
*/
private $order = null;
/**
* Get the path of this REST route.
*
* @return string
*/
public function get_path() {
return '/checkout/(?P<id>[\d]+)';
}
/**
* Get method arguments for this REST route.
*
* @return array An array of endpoints.
*/
public function get_args() {
return [
[
'methods' => \WP_REST_Server::CREATABLE,
'callback' => [ $this, 'get_response' ],
'permission_callback' => [ $this, 'is_authorized' ],
'args' => array_merge(
[
'payment_data' => [
'description' => __( 'Data to pass through to the payment method when processing payment.', 'woocommerce' ),
'type' => 'array',
'items' => [
'type' => 'object',
'properties' => [
'key' => [
'type' => 'string',
],
'value' => [
'type' => [ 'string', 'boolean' ],
],
],
],
],
],
$this->schema->get_endpoint_args_for_item_schema( \WP_REST_Server::CREATABLE )
),
],
'schema' => [ $this->schema, 'get_public_item_schema' ],
'allow_batch' => [ 'v1' => true ],
];
}
/**
* Process an order.
*
* 1. Process Request
* 2. Process Customer
* 3. Validate Order
* 4. Process Payment
*
* @throws RouteException On error.
* @throws InvalidStockLevelsInCartException On error.
*
* @param \WP_REST_Request $request Request object.
*
* @return \WP_REST_Response
*/
protected function get_route_post_response( \WP_REST_Request $request ) {
$order_id = absint( $request['id'] );
$this->order = wc_get_order( $order_id );
if ( $this->order->get_status() !== 'pending' && $this->order->get_status() !== 'failed' ) {
return new \WP_Error(
'invalid_order_update_status',
__( 'This order cannot be paid for.', 'woocommerce' )
);
}
/**
* Process request data.
*
* Note: Customer data is persisted from the request first so that OrderController::update_addresses_from_cart
* uses the up to date customer address.
*/
$this->update_billing_address( $request );
$this->update_order_from_request( $request );
/**
* Process customer data.
*
* Update order with customer details, and sign up a user account as necessary.
*/
$this->process_customer( $request );
/**
* Validate order.
*
* This logic ensures the order is valid before payment is attempted.
*/
$this->order_controller->validate_order_before_payment( $this->order );
/**
* Fires before an order is processed by the Checkout Block/Store API.
*
* This hook informs extensions that $order has completed processing and is ready for payment.
*
* This is similar to existing core hook woocommerce_checkout_order_processed. We're using a new action:
* - To keep the interface focused (only pass $order, not passing request data).
* - This also explicitly indicates these orders are from checkout block/StoreAPI.
*
* @since 7.2.0
*
* @see https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3238
* @example See docs/examples/checkout-order-processed.md
* @param \WC_Order $order Order object.
*/
do_action( 'woocommerce_store_api_checkout_order_processed', $this->order );
/**
* Process the payment and return the results.
*/
$payment_result = new PaymentResult();
if ( $this->order->needs_payment() ) {
$this->process_payment( $request, $payment_result );
} else {
$this->process_without_payment( $request, $payment_result );
}
return $this->prepare_item_for_response(
(object) [
'order' => wc_get_order( $this->order ),
'payment_result' => $payment_result,
],
$request
);
}
/**
* Updates the current customer session using data from the request (e.g. address data).
*
* Address session data is synced to the order itself later on by OrderController::update_order_from_cart()
*
* @param \WP_REST_Request $request Full details about the request.
*/
private function update_billing_address( \WP_REST_Request $request ) {
$customer = wc()->customer;
$billing = $request['billing_address'];
$shipping = $request['shipping_address'];
// Billing address is a required field.
foreach ( $billing as $key => $value ) {
if ( is_callable( [ $customer, "set_billing_$key" ] ) ) {
$customer->{"set_billing_$key"}( $value );
}
}
// If shipping address (optional field) was not provided, set it to the given billing address (required field).
$shipping_address_values = $shipping ?? $billing;
foreach ( $shipping_address_values as $key => $value ) {
if ( is_callable( [ $customer, "set_shipping_$key" ] ) ) {
$customer->{"set_shipping_$key"}( $value );
} elseif ( 'phone' === $key ) {
$customer->update_meta_data( 'shipping_phone', $value );
}
}
/**
* Fires when the Checkout Block/Store API updates a customer from the API request data.
*
* @since 8.2.0
*
* @param \WC_Customer $customer Customer object.
* @param \WP_REST_Request $request Full details about the request.
*/
do_action( 'woocommerce_store_api_checkout_update_customer_from_request', $customer, $request );
$customer->save();
$this->order->set_billing_address( $billing );
$this->order->set_shipping_address( $shipping );
$this->order->save();
$this->order->calculate_totals();
}
/**
* Gets the chosen payment method from the request.
*
* @throws RouteException On error.
* @param \WP_REST_Request $request Request object.
* @return \WC_Payment_Gateway|null
*/
private function get_request_payment_method( \WP_REST_Request $request ) {
$available_gateways = WC()->payment_gateways->get_available_payment_gateways();
$request_payment_method = wc_clean( wp_unslash( $request['payment_method'] ?? '' ) );
$requires_payment_method = $this->order->needs_payment();
if ( empty( $request_payment_method ) ) {
if ( $requires_payment_method ) {
throw new RouteException(
'woocommerce_rest_checkout_missing_payment_method',
__( 'No payment method provided.', 'woocommerce' ),
400
);
}
return null;
}
if ( ! isset( $available_gateways[ $request_payment_method ] ) ) {
throw new RouteException(
'woocommerce_rest_checkout_payment_method_disabled',
sprintf(
// Translators: %s Payment method ID.
__( 'The %s payment gateway is not available.', 'woocommerce' ),
esc_html( $request_payment_method )
),
400
);
}
return $available_gateways[ $request_payment_method ];
}
/**
* Updates the order with user details (e.g. address).
*
* @throws RouteException API error object with error details.
* @param \WP_REST_Request $request Request object.
*/
private function process_customer( \WP_REST_Request $request ) {
$this->order_controller->sync_customer_data_with_order( $this->order );
}
}

View File

@@ -0,0 +1,87 @@
<?php
namespace Automattic\WooCommerce\StoreApi\Routes\V1;
use Automattic\WooCommerce\StoreApi\SchemaController;
use Automattic\WooCommerce\StoreApi\Schemas\v1\AbstractSchema;
use Automattic\WooCommerce\StoreApi\Utilities\OrderController;
use Automattic\WooCommerce\StoreApi\Exceptions\RouteException;
use Automattic\WooCommerce\StoreApi\Utilities\OrderAuthorizationTrait;
/**
* Order class.
*/
class Order extends AbstractRoute {
use OrderAuthorizationTrait;
/**
* The route identifier.
*
* @var string
*/
const IDENTIFIER = 'order';
/**
* The schema item identifier.
*
* @var string
*/
const SCHEMA_TYPE = 'order';
/**
* Order controller class instance.
*
* @var OrderController
*/
protected $order_controller;
/**
* Constructor.
*
* @param SchemaController $schema_controller Schema Controller instance.
* @param AbstractSchema $schema Schema class for this route.
*/
public function __construct( SchemaController $schema_controller, AbstractSchema $schema ) {
parent::__construct( $schema_controller, $schema );
$this->order_controller = new OrderController();
}
/**
* Get the path of this REST route.
*
* @return string
*/
public function get_path() {
return '/order/(?P<id>[\d]+)';
}
/**
* Get method arguments for this REST route.
*
* @return array An array of endpoints.
*/
public function get_args() {
return [
[
'methods' => \WP_REST_Server::READABLE,
'callback' => [ $this, 'get_response' ],
'permission_callback' => [ $this, 'is_authorized' ],
'args' => [
'context' => $this->get_context_param( [ 'default' => 'view' ] ),
],
],
'schema' => [ $this->schema, 'get_public_item_schema' ],
];
}
/**
* Handle the request and return a valid response for this endpoint.
*
* @param \WP_REST_Request $request Request object.
* @return \WP_REST_Response
*/
protected function get_route_response( \WP_REST_Request $request ) {
$order_id = absint( $request['id'] );
return rest_ensure_response( $this->schema->get_item_response( wc_get_order( $order_id ) ) );
}
}

View File

@@ -91,49 +91,12 @@ class ProductCollectionData extends AbstractRoute {
}
if ( ! empty( $request['calculate_attribute_counts'] ) ) {
$taxonomy__or_queries = [];
$taxonomy__and_queries = [];
foreach ( $request['calculate_attribute_counts'] as $attributes_to_count ) {
if ( ! empty( $attributes_to_count['taxonomy'] ) ) {
if ( empty( $attributes_to_count['query_type'] ) || 'or' === $attributes_to_count['query_type'] ) {
$taxonomy__or_queries[] = $attributes_to_count['taxonomy'];
} else {
$taxonomy__and_queries[] = $attributes_to_count['taxonomy'];
}
if ( ! isset( $attributes_to_count['taxonomy'] ) ) {
continue;
}
}
$data['attribute_counts'] = [];
// Or type queries need special handling because the attribute, if set, needs removing from the query first otherwise counts would not be correct.
if ( $taxonomy__or_queries ) {
foreach ( $taxonomy__or_queries as $taxonomy ) {
$filter_request = clone $request;
$filter_attributes = $filter_request->get_param( 'attributes' );
if ( ! empty( $filter_attributes ) ) {
$filter_attributes = array_filter(
$filter_attributes,
function( $query ) use ( $taxonomy ) {
return $query['attribute'] !== $taxonomy;
}
);
}
$filter_request->set_param( 'attributes', $filter_attributes );
$counts = $filters->get_attribute_counts( $filter_request, [ $taxonomy ] );
foreach ( $counts as $key => $value ) {
$data['attribute_counts'][] = (object) [
'term' => $key,
'count' => $value,
];
}
}
}
if ( $taxonomy__and_queries ) {
$counts = $filters->get_attribute_counts( $request, $taxonomy__and_queries );
$counts = $filters->get_attribute_counts( $request, $attributes_to_count['taxonomy'] );
foreach ( $counts as $key => $value ) {
$data['attribute_counts'][] = (object) [

View File

@@ -141,6 +141,13 @@ class Products extends AbstractRoute {
'validate_callback' => 'rest_validate_request_arg',
);
$params['slug'] = array(
'description' => __( 'Limit result set to products with specific slug(s). Use commas to separate.', 'woocommerce' ),
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => 'rest_validate_request_arg',
);
$params['after'] = array(
'description' => __( 'Limit response to resources created after a given ISO8601 compliant date.', 'woocommerce' ),
'type' => 'string',

View File

@@ -0,0 +1,117 @@
<?php
namespace Automattic\WooCommerce\StoreApi\Routes\V1;
use Automattic\WooCommerce\StoreApi\Exceptions\RouteException;
/**
* ProductsBySlug class.
*/
class ProductsBySlug extends AbstractRoute {
/**
* The route identifier.
*
* @var string
*/
const IDENTIFIER = 'products-by-slug';
/**
* The routes schema.
*
* @var string
*/
const SCHEMA_TYPE = 'product';
/**
* Get the path of this REST route.
*
* @return string
*/
public function get_path() {
return '/products/(?P<slug>[\S]+)';
}
/**
* Get method arguments for this REST route.
*
* @return array An array of endpoints.
*/
public function get_args() {
return [
'args' => array(
'slug' => array(
'description' => __( 'Slug of the resource.', 'woocommerce' ),
'type' => 'string',
),
),
[
'methods' => \WP_REST_Server::READABLE,
'callback' => [ $this, 'get_response' ],
'permission_callback' => '__return_true',
'args' => array(
'context' => $this->get_context_param(
array(
'default' => 'view',
)
),
),
],
'schema' => [ $this->schema, 'get_public_item_schema' ],
];
}
/**
* Get a single item.
*
* @throws RouteException On error.
* @param \WP_REST_Request $request Request object.
* @return \WP_REST_Response
*/
protected function get_route_response( \WP_REST_Request $request ) {
$slug = sanitize_title( $request['slug'] );
$object = $this->get_product_by_slug( $slug );
if ( ! $object ) {
$object = $this->get_product_variation_by_slug( $slug );
}
if ( ! $object || 0 === $object->get_id() ) {
throw new RouteException( 'woocommerce_rest_product_invalid_slug', __( 'Invalid product slug.', 'woocommerce' ), 404 );
}
return rest_ensure_response( $this->schema->get_item_response( $object ) );
}
/**
* Get a product by slug.
*
* @param string $slug The slug of the product.
*/
public function get_product_by_slug( $slug ) {
return wc_get_product( get_page_by_path( $slug, OBJECT, 'product' ) );
}
/**
* Get a product variation by slug.
*
* @param string $slug The slug of the product variation.
*/
private function get_product_variation_by_slug( $slug ) {
global $wpdb;
$result = $wpdb->get_results(
$wpdb->prepare(
"SELECT ID, post_name, post_parent, post_type
FROM $wpdb->posts
WHERE post_name = %s
AND post_type = 'product_variation'",
$slug
)
);
if ( ! $result ) {
return null;
}
return wc_get_product( $result[0]->ID );
}
}

View File

@@ -1,6 +1,7 @@
<?php
namespace Automattic\WooCommerce\StoreApi;
use Automattic\WooCommerce\Blocks\Package;
use Automattic\WooCommerce\StoreApi\SchemaController;
use Exception;
use Routes\AbstractRoute;
@@ -57,8 +58,14 @@ class RoutesController {
Routes\V1\ProductTags::IDENTIFIER => Routes\V1\ProductTags::class,
Routes\V1\Products::IDENTIFIER => Routes\V1\Products::class,
Routes\V1\ProductsById::IDENTIFIER => Routes\V1\ProductsById::class,
Routes\V1\ProductsBySlug::IDENTIFIER => Routes\V1\ProductsBySlug::class,
],
];
if ( Package::is_experimental_build() ) {
$this->routes['v1'][ Routes\V1\Order::IDENTIFIER ] = Routes\V1\Order::class;
$this->routes['v1'][ Routes\V1\CheckoutOrder::IDENTIFIER ] = Routes\V1\CheckoutOrder::class;
}
}
/**

View File

@@ -43,7 +43,12 @@ class SchemaController {
Schemas\V1\CartItemSchema::IDENTIFIER => Schemas\V1\CartItemSchema::class,
Schemas\V1\CartSchema::IDENTIFIER => Schemas\V1\CartSchema::class,
Schemas\V1\CartExtensionsSchema::IDENTIFIER => Schemas\V1\CartExtensionsSchema::class,
Schemas\V1\CheckoutOrderSchema::IDENTIFIER => Schemas\V1\CheckoutOrderSchema::class,
Schemas\V1\CheckoutSchema::IDENTIFIER => Schemas\V1\CheckoutSchema::class,
Schemas\V1\OrderItemSchema::IDENTIFIER => Schemas\V1\OrderItemSchema::class,
Schemas\V1\OrderCouponSchema::IDENTIFIER => Schemas\V1\OrderCouponSchema::class,
Schemas\V1\OrderFeeSchema::IDENTIFIER => Schemas\V1\OrderFeeSchema::class,
Schemas\V1\OrderSchema::IDENTIFIER => Schemas\V1\OrderSchema::class,
Schemas\V1\ProductSchema::IDENTIFIER => Schemas\V1\ProductSchema::class,
Schemas\V1\ProductAttributeSchema::IDENTIFIER => Schemas\V1\ProductAttributeSchema::class,
Schemas\V1\ProductCategorySchema::IDENTIFIER => Schemas\V1\ProductCategorySchema::class,

View File

@@ -92,16 +92,16 @@ abstract class AbstractAddressSchema extends AbstractSchema {
$validation_util = new ValidationUtils();
$address = array_merge( array_fill_keys( array_keys( $this->get_properties() ), '' ), (array) $address );
$address['country'] = wc_strtoupper( wc_clean( wp_unslash( $address['country'] ) ) );
$address['first_name'] = wc_clean( wp_unslash( $address['first_name'] ) );
$address['last_name'] = wc_clean( wp_unslash( $address['last_name'] ) );
$address['company'] = wc_clean( wp_unslash( $address['company'] ) );
$address['address_1'] = wc_clean( wp_unslash( $address['address_1'] ) );
$address['address_2'] = wc_clean( wp_unslash( $address['address_2'] ) );
$address['city'] = wc_clean( wp_unslash( $address['city'] ) );
$address['state'] = $validation_util->format_state( wc_clean( wp_unslash( $address['state'] ) ), $address['country'] );
$address['postcode'] = $address['postcode'] ? wc_format_postcode( wc_clean( wp_unslash( $address['postcode'] ) ), $address['country'] ) : '';
$address['phone'] = wc_clean( wp_unslash( $address['phone'] ) );
$address['country'] = wc_strtoupper( sanitize_text_field( wp_unslash( $address['country'] ) ) );
$address['first_name'] = sanitize_text_field( wp_unslash( $address['first_name'] ) );
$address['last_name'] = sanitize_text_field( wp_unslash( $address['last_name'] ) );
$address['company'] = sanitize_text_field( wp_unslash( $address['company'] ) );
$address['address_1'] = sanitize_text_field( wp_unslash( $address['address_1'] ) );
$address['address_2'] = sanitize_text_field( wp_unslash( $address['address_2'] ) );
$address['city'] = sanitize_text_field( wp_unslash( $address['city'] ) );
$address['state'] = $validation_util->format_state( sanitize_text_field( wp_unslash( $address['state'] ) ), $address['country'] );
$address['postcode'] = $address['postcode'] ? wc_format_postcode( sanitize_text_field( wp_unslash( $address['postcode'] ) ), $address['country'] ) : '';
$address['phone'] = sanitize_text_field( wp_unslash( $address['phone'] ) );
return $address;
}

View File

@@ -63,6 +63,16 @@ abstract class AbstractSchema {
);
}
/**
* Returns the full item response.
*
* @param mixed $item Item to get response for.
* @return array|stdClass
*/
public function get_item_response( $item ) {
return [];
}
/**
* Return schema properties.
*

View File

@@ -54,7 +54,7 @@ class BillingAddressSchema extends AbstractAddressSchema {
*/
public function sanitize_callback( $address, $request, $param ) {
$address = parent::sanitize_callback( $address, $request, $param );
$address['email'] = wc_clean( wp_unslash( $address['email'] ) );
$address['email'] = sanitize_text_field( wp_unslash( $address['email'] ) );
return $address;
}
@@ -87,7 +87,7 @@ class BillingAddressSchema extends AbstractAddressSchema {
* @param \WC_Order|\WC_Customer $address An object with billing address.
*
* @throws RouteException When the invalid object types are provided.
* @return stdClass
* @return array
*/
public function get_item_response( $address ) {
$validation_util = new ValidationUtils();
@@ -99,7 +99,7 @@ class BillingAddressSchema extends AbstractAddressSchema {
$billing_state = '';
}
return (object) $this->prepare_html_response(
return $this->prepare_html_response(
[
'first_name' => $address->get_billing_first_name(),
'last_name' => $address->get_billing_last_name(),

View File

@@ -1,14 +1,14 @@
<?php
namespace Automattic\WooCommerce\StoreApi\Schemas\V1;
use Automattic\WooCommerce\StoreApi\Utilities\DraftOrderTrait;
use Automattic\WooCommerce\StoreApi\Utilities\ProductItemTrait;
use Automattic\WooCommerce\StoreApi\Utilities\QuantityLimits;
/**
* CartItemSchema class.
*/
class CartItemSchema extends ProductSchema {
use DraftOrderTrait;
class CartItemSchema extends ItemSchema {
use ProductItemTrait;
/**
* The schema item name.
@@ -24,308 +24,6 @@ class CartItemSchema extends ProductSchema {
*/
const IDENTIFIER = 'cart-item';
/**
* Cart schema properties.
*
* @return array
*/
public function get_properties() {
return [
'key' => [
'description' => __( 'Unique identifier for the item within the cart.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'id' => [
'description' => __( 'The cart item product or variation ID.', 'woocommerce' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'quantity' => [
'description' => __( 'Quantity of this item in the cart.', 'woocommerce' ),
'type' => 'number',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'quantity_limits' => [
'description' => __( 'How the quantity of this item should be controlled, for example, any limits in place.', 'woocommerce' ),
'type' => 'object',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => [
'minimum' => [
'description' => __( 'The minimum quantity allowed in the cart for this line item.', 'woocommerce' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'maximum' => [
'description' => __( 'The maximum quantity allowed in the cart for this line item.', 'woocommerce' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'multiple_of' => [
'description' => __( 'The amount that quantities increment by. Quantity must be an multiple of this value.', 'woocommerce' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'default' => 1,
],
'editable' => [
'description' => __( 'If the quantity in the cart is editable or fixed.', 'woocommerce' ),
'type' => 'boolean',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'default' => true,
],
],
],
'name' => [
'description' => __( 'Product name.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'short_description' => [
'description' => __( 'Product short description in HTML format.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'description' => [
'description' => __( 'Product full description in HTML format.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'sku' => [
'description' => __( 'Stock keeping unit, if applicable.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'low_stock_remaining' => [
'description' => __( 'Quantity left in stock if stock is low, or null if not applicable.', 'woocommerce' ),
'type' => [ 'integer', 'null' ],
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'backorders_allowed' => [
'description' => __( 'True if backorders are allowed past stock availability.', 'woocommerce' ),
'type' => [ 'boolean' ],
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'show_backorder_badge' => [
'description' => __( 'True if the product is on backorder.', 'woocommerce' ),
'type' => [ 'boolean' ],
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'sold_individually' => [
'description' => __( 'If true, only one item of this product is allowed for purchase in a single order.', 'woocommerce' ),
'type' => 'boolean',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'permalink' => [
'description' => __( 'Product URL.', 'woocommerce' ),
'type' => 'string',
'format' => 'uri',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'images' => [
'description' => __( 'List of images.', 'woocommerce' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'items' => [
'type' => 'object',
'properties' => $this->image_attachment_schema->get_properties(),
],
],
'variation' => [
'description' => __( 'Chosen attributes (for variations).', 'woocommerce' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'items' => [
'type' => 'object',
'properties' => [
'attribute' => [
'description' => __( 'Variation attribute name.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'value' => [
'description' => __( 'Variation attribute value.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
],
],
],
'item_data' => [
'description' => __( 'Metadata related to the cart item', 'woocommerce' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'items' => [
'type' => 'object',
'properties' => [
'name' => [
'description' => __( 'Name of the metadata.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'value' => [
'description' => __( 'Value of the metadata.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'display' => [
'description' => __( 'Optionally, how the metadata value should be displayed to the user.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
],
],
],
'prices' => [
'description' => __( 'Price data for the product in the current line item, including or excluding taxes based on the "display prices during cart and checkout" setting. Provided using the smallest unit of the currency.', 'woocommerce' ),
'type' => 'object',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => array_merge(
$this->get_store_currency_properties(),
[
'price' => [
'description' => __( 'Current product price.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'regular_price' => [
'description' => __( 'Regular product price.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'sale_price' => [
'description' => __( 'Sale product price, if applicable.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'price_range' => [
'description' => __( 'Price range, if applicable.', 'woocommerce' ),
'type' => [ 'object', 'null' ],
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => [
'min_amount' => [
'description' => __( 'Price amount.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'max_amount' => [
'description' => __( 'Price amount.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
],
],
'raw_prices' => [
'description' => __( 'Raw unrounded product prices used in calculations. Provided using a higher unit of precision than the currency.', 'woocommerce' ),
'type' => [ 'object', 'null' ],
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => [
'precision' => [
'description' => __( 'Decimal precision of the returned prices.', 'woocommerce' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'price' => [
'description' => __( 'Current product price.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'regular_price' => [
'description' => __( 'Regular product price.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'sale_price' => [
'description' => __( 'Sale product price, if applicable.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
],
],
]
),
],
'totals' => [
'description' => __( 'Item total amounts provided using the smallest unit of the currency.', 'woocommerce' ),
'type' => 'object',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => array_merge(
$this->get_store_currency_properties(),
[
'line_subtotal' => [
'description' => __( 'Line subtotal (the price of the product before coupon discounts have been applied).', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'line_subtotal_tax' => [
'description' => __( 'Line subtotal tax.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'line_total' => [
'description' => __( 'Line total (the price of the product after coupon discounts have been applied).', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'line_total_tax' => [
'description' => __( 'Line total tax.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
]
),
],
'catalog_visibility' => [
'description' => __( 'Whether the product is visible in the catalog', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
self::EXTENDING_KEY => $this->get_extended_schema( self::IDENTIFIER ),
];
}
/**
* Convert a WooCommerce cart item to an object suitable for the response.
*
@@ -380,82 +78,6 @@ class CartItemSchema extends ProductSchema {
];
}
/**
* Get an array of pricing data.
*
* @param \WC_Product $product Product instance.
* @param string $tax_display_mode If returned prices are incl or excl of tax.
* @return array
*/
protected function prepare_product_price_response( \WC_Product $product, $tax_display_mode = '' ) {
$tax_display_mode = $this->get_tax_display_mode( $tax_display_mode );
$price_function = $this->get_price_function_from_tax_display_mode( $tax_display_mode );
$prices = parent::prepare_product_price_response( $product, $tax_display_mode );
// Add raw prices (prices with greater precision).
$prices['raw_prices'] = [
'precision' => wc_get_rounding_precision(),
'price' => $this->prepare_money_response( $price_function( $product ), wc_get_rounding_precision() ),
'regular_price' => $this->prepare_money_response( $price_function( $product, [ 'price' => $product->get_regular_price() ] ), wc_get_rounding_precision() ),
'sale_price' => $this->prepare_money_response( $price_function( $product, [ 'price' => $product->get_sale_price() ] ), wc_get_rounding_precision() ),
];
return $prices;
}
/**
* Format variation data, for example convert slugs such as attribute_pa_size to Size.
*
* @param array $variation_data Array of data from the cart.
* @param \WC_Product $product Product data.
* @return array
*/
protected function format_variation_data( $variation_data, $product ) {
$return = [];
if ( ! is_iterable( $variation_data ) ) {
return $return;
}
foreach ( $variation_data as $key => $value ) {
$taxonomy = wc_attribute_taxonomy_name( str_replace( 'attribute_pa_', '', urldecode( $key ) ) );
if ( taxonomy_exists( $taxonomy ) ) {
// If this is a term slug, get the term's nice name.
$term = get_term_by( 'slug', $value, $taxonomy );
if ( ! is_wp_error( $term ) && $term && $term->name ) {
$value = $term->name;
}
$label = wc_attribute_label( $taxonomy );
} else {
/**
* Filters the variation option name.
*
* Filters the variation option name for custom option slugs.
*
* @since 2.5.0
*
* @internal Matches filter name in WooCommerce core.
*
* @param string $value The name to display.
* @param null $unused Unused because this is not a variation taxonomy.
* @param string $taxonomy Taxonomy or product attribute name.
* @param \WC_Product $product Product data.
* @return string
*/
$value = apply_filters( 'woocommerce_variation_option_name', $value, null, $taxonomy, $product );
$label = wc_attribute_label( str_replace( 'attribute_', '', $key ), $product );
}
$return[] = [
'attribute' => $this->prepare_html_response( $label ),
'value' => $this->prepare_html_response( $value ),
];
}
return $return;
}
/**
* Format cart item data removing any HTML tag.
*

View File

@@ -349,42 +349,52 @@ class CartSchema extends AbstractSchema {
$cross_sells = array_filter( array_map( 'wc_get_product', $cart->get_cross_sells() ), 'wc_products_array_filter_visible' );
return [
'coupons' => $this->get_item_responses_from_schema( $this->coupon_schema, $cart->get_applied_coupons() ),
'shipping_rates' => $this->get_item_responses_from_schema( $this->shipping_rate_schema, $shipping_packages ),
'shipping_address' => $this->shipping_address_schema->get_item_response( wc()->customer ),
'billing_address' => $this->billing_address_schema->get_item_response( wc()->customer ),
'items' => $this->get_item_responses_from_schema( $this->item_schema, $cart->get_cart() ),
'coupons' => $this->get_item_responses_from_schema( $this->coupon_schema, $cart->get_applied_coupons() ),
'fees' => $this->get_item_responses_from_schema( $this->fee_schema, $cart->get_fees() ),
'totals' => (object) $this->prepare_currency_response( $this->get_totals( $cart ) ),
'shipping_address' => (object) $this->shipping_address_schema->get_item_response( wc()->customer ),
'billing_address' => (object) $this->billing_address_schema->get_item_response( wc()->customer ),
'needs_payment' => $cart->needs_payment(),
'needs_shipping' => $cart->needs_shipping(),
'payment_requirements' => $this->extend->get_payment_requirements(),
'has_calculated_shipping' => $has_calculated_shipping,
'shipping_rates' => $this->get_item_responses_from_schema( $this->shipping_rate_schema, $shipping_packages ),
'items_count' => $cart->get_cart_contents_count(),
'items_weight' => wc_get_weight( $cart->get_cart_contents_weight(), 'g' ),
'cross_sells' => $this->get_item_responses_from_schema( $this->cross_sells_item_schema, $cross_sells ),
'needs_payment' => $cart->needs_payment(),
'needs_shipping' => $cart->needs_shipping(),
'has_calculated_shipping' => $has_calculated_shipping,
'fees' => $this->get_item_responses_from_schema( $this->fee_schema, $cart->get_fees() ),
'totals' => (object) $this->prepare_currency_response(
[
'total_items' => $this->prepare_money_response( $cart->get_subtotal(), wc_get_price_decimals() ),
'total_items_tax' => $this->prepare_money_response( $cart->get_subtotal_tax(), wc_get_price_decimals() ),
'total_fees' => $this->prepare_money_response( $cart->get_fee_total(), wc_get_price_decimals() ),
'total_fees_tax' => $this->prepare_money_response( $cart->get_fee_tax(), wc_get_price_decimals() ),
'total_discount' => $this->prepare_money_response( $cart->get_discount_total(), wc_get_price_decimals() ),
'total_discount_tax' => $this->prepare_money_response( $cart->get_discount_tax(), wc_get_price_decimals() ),
'total_shipping' => $has_calculated_shipping ? $this->prepare_money_response( $cart->get_shipping_total(), wc_get_price_decimals() ) : null,
'total_shipping_tax' => $has_calculated_shipping ? $this->prepare_money_response( $cart->get_shipping_tax(), wc_get_price_decimals() ) : null,
// Explicitly request context='edit'; default ('view') will render total as markup.
'total_price' => $this->prepare_money_response( $cart->get_total( 'edit' ), wc_get_price_decimals() ),
'total_tax' => $this->prepare_money_response( $cart->get_total_tax(), wc_get_price_decimals() ),
'tax_lines' => $this->get_tax_lines( $cart ),
]
),
'errors' => $cart_errors,
'payment_methods' => array_values( wp_list_pluck( WC()->payment_gateways->get_available_payment_gateways(), 'id' ) ),
'payment_requirements' => $this->extend->get_payment_requirements(),
self::EXTENDING_KEY => $this->get_extended_data( self::IDENTIFIER ),
];
}
/**
* Get total data.
*
* @param \WC_Cart $cart Cart class instance.
* @return array
*/
protected function get_totals( $cart ) {
$has_calculated_shipping = $cart->show_shipping();
$decimals = wc_get_price_decimals();
return [
'total_items' => $this->prepare_money_response( $cart->get_subtotal(), $decimals ),
'total_items_tax' => $this->prepare_money_response( $cart->get_subtotal_tax(), $decimals ),
'total_fees' => $this->prepare_money_response( $cart->get_fee_total(), $decimals ),
'total_fees_tax' => $this->prepare_money_response( $cart->get_fee_tax(), $decimals ),
'total_discount' => $this->prepare_money_response( $cart->get_discount_total(), $decimals ),
'total_discount_tax' => $this->prepare_money_response( $cart->get_discount_tax(), $decimals ),
'total_shipping' => $has_calculated_shipping ? $this->prepare_money_response( $cart->get_shipping_total(), $decimals ) : null,
'total_shipping_tax' => $has_calculated_shipping ? $this->prepare_money_response( $cart->get_shipping_tax(), $decimals ) : null,
// Explicitly request context='edit'; default ('view') will render total as markup.
'total_price' => $this->prepare_money_response( $cart->get_total( 'edit' ), $decimals ),
'total_tax' => $this->prepare_money_response( $cart->get_total_tax(), $decimals ),
'tax_lines' => $this->get_tax_lines( $cart ),
];
}
/**
* Get tax lines from the cart and format to match schema.
*
@@ -399,11 +409,12 @@ class CartSchema extends AbstractSchema {
}
$cart_tax_totals = $cart->get_tax_totals();
$decimals = wc_get_price_decimals();
foreach ( $cart_tax_totals as $cart_tax_total ) {
$tax_lines[] = array(
'name' => $cart_tax_total->label,
'price' => $this->prepare_money_response( $cart_tax_total->amount, wc_get_price_decimals() ),
'price' => $this->prepare_money_response( $cart_tax_total->amount, $decimals ),
'rate' => WC_Tax::get_rate_percent( $cart_tax_total->tax_rate_id ),
);
}

View File

@@ -0,0 +1,37 @@
<?php
namespace Automattic\WooCommerce\StoreApi\Schemas\V1;
use Automattic\WooCommerce\StoreApi\SchemaController;
use Automattic\WooCommerce\StoreApi\Payments\PaymentResult;
use Automattic\WooCommerce\StoreApi\Schemas\ExtendSchema;
/**
* CheckoutOrderSchema class.
*/
class CheckoutOrderSchema extends CheckoutSchema {
/**
* The schema item name.
*
* @var string
*/
protected $title = 'checkout-order';
/**
* The schema item identifier.
*
* @var string
*/
const IDENTIFIER = 'checkout-order';
/**
* Checkout schema properties.
*
* @return array
*/
public function get_properties() {
$parent_properties = parent::get_properties();
unset( $parent_properties['create_account'] );
return $parent_properties;
}
}

View File

@@ -83,6 +83,12 @@ class CheckoutSchema extends AbstractSchema {
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'order_number' => [
'description' => __( 'Order number used for display.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'customer_note' => [
'description' => __( 'Note added to the order by the customer during checkout.', 'woocommerce' ),
'type' => 'string',
@@ -119,7 +125,9 @@ class CheckoutSchema extends AbstractSchema {
'description' => __( 'The ID of the payment method being used to process the payment.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'enum' => array_values( wp_list_pluck( WC()->payment_gateways->get_available_payment_gateways(), 'id' ) ),
// Validation may be based on cart contents which is not available here; this returns all enabled
// gateways. Further validation occurs during the request.
'enum' => array_values( WC()->payment_gateways->get_payment_gateway_ids() ),
],
'create_account' => [
'description' => __( 'Whether to create a new user account as part of order processing.', 'woocommerce' ),
@@ -186,10 +194,11 @@ class CheckoutSchema extends AbstractSchema {
'order_id' => $order->get_id(),
'status' => $order->get_status(),
'order_key' => $order->get_order_key(),
'order_number' => $order->get_order_number(),
'customer_note' => $order->get_customer_note(),
'customer_id' => $order->get_customer_id(),
'billing_address' => $this->billing_address_schema->get_item_response( $order ),
'shipping_address' => $this->shipping_address_schema->get_item_response( $order ),
'billing_address' => (object) $this->billing_address_schema->get_item_response( $order ),
'shipping_address' => (object) $this->shipping_address_schema->get_item_response( $order ),
'payment_method' => $order->get_payment_method(),
'payment_result' => [
'payment_status' => $payment_result->status,

View File

@@ -47,7 +47,7 @@ class ErrorSchema extends AbstractSchema {
* @param \WP_Error $error Error object.
* @return array
*/
public function get_item_response( \WP_Error $error ) {
public function get_item_response( $error ) {
return [
'code' => $this->prepare_html_response( $error->get_error_code() ),
'message' => $this->prepare_html_response( $error->get_error_message() ),

View File

@@ -70,7 +70,7 @@ class ImageAttachmentSchema extends AbstractSchema {
* Convert a WooCommerce product into an object suitable for the response.
*
* @param int $attachment_id Image attachment ID.
* @return array|null
* @return object|null
*/
public function get_item_response( $attachment_id ) {
if ( ! $attachment_id ) {
@@ -80,12 +80,12 @@ class ImageAttachmentSchema extends AbstractSchema {
$attachment = wp_get_attachment_image_src( $attachment_id, 'full' );
if ( ! is_array( $attachment ) ) {
return [];
return null;
}
$thumbnail = wp_get_attachment_image_src( $attachment_id, 'woocommerce_thumbnail' );
return [
return (object) [
'id' => (int) $attachment_id,
'src' => current( $attachment ),
'thumbnail' => current( $thumbnail ),

View File

@@ -0,0 +1,310 @@
<?php
namespace Automattic\WooCommerce\StoreApi\Schemas\V1;
/**
* ItemSchema class.
*/
abstract class ItemSchema extends ProductSchema {
/**
* Item schema properties.
*
* @return array
*/
public function get_properties() {
return [
'key' => [
'description' => __( 'Unique identifier for the item.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'id' => [
'description' => __( 'The item product or variation ID.', 'woocommerce' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'quantity' => [
'description' => __( 'Quantity of this item.', 'woocommerce' ),
'type' => 'number',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'quantity_limits' => [
'description' => __( 'How the quantity of this item should be controlled, for example, any limits in place.', 'woocommerce' ),
'type' => 'object',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => [
'minimum' => [
'description' => __( 'The minimum quantity allowed for this line item.', 'woocommerce' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'maximum' => [
'description' => __( 'The maximum quantity allowed for this line item.', 'woocommerce' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'multiple_of' => [
'description' => __( 'The amount that quantities increment by. Quantity must be an multiple of this value.', 'woocommerce' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'default' => 1,
],
'editable' => [
'description' => __( 'If the quantity is editable or fixed.', 'woocommerce' ),
'type' => 'boolean',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'default' => true,
],
],
],
'name' => [
'description' => __( 'Product name.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'short_description' => [
'description' => __( 'Product short description in HTML format.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'description' => [
'description' => __( 'Product full description in HTML format.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'sku' => [
'description' => __( 'Stock keeping unit, if applicable.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'low_stock_remaining' => [
'description' => __( 'Quantity left in stock if stock is low, or null if not applicable.', 'woocommerce' ),
'type' => [ 'integer', 'null' ],
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'backorders_allowed' => [
'description' => __( 'True if backorders are allowed past stock availability.', 'woocommerce' ),
'type' => [ 'boolean' ],
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'show_backorder_badge' => [
'description' => __( 'True if the product is on backorder.', 'woocommerce' ),
'type' => [ 'boolean' ],
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'sold_individually' => [
'description' => __( 'If true, only one item of this product is allowed for purchase in a single order.', 'woocommerce' ),
'type' => 'boolean',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'permalink' => [
'description' => __( 'Product URL.', 'woocommerce' ),
'type' => 'string',
'format' => 'uri',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'images' => [
'description' => __( 'List of images.', 'woocommerce' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'items' => [
'type' => 'object',
'properties' => $this->image_attachment_schema->get_properties(),
],
],
'variation' => [
'description' => __( 'Chosen attributes (for variations).', 'woocommerce' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'items' => [
'type' => 'object',
'properties' => [
'attribute' => [
'description' => __( 'Variation attribute name.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'value' => [
'description' => __( 'Variation attribute value.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
],
],
],
'item_data' => [
'description' => __( 'Metadata related to the item', 'woocommerce' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'items' => [
'type' => 'object',
'properties' => [
'name' => [
'description' => __( 'Name of the metadata.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'value' => [
'description' => __( 'Value of the metadata.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'display' => [
'description' => __( 'Optionally, how the metadata value should be displayed to the user.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
],
],
],
'prices' => [
'description' => __( 'Price data for the product in the current line item, including or excluding taxes based on the "display prices during cart and checkout" setting. Provided using the smallest unit of the currency.', 'woocommerce' ),
'type' => 'object',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => array_merge(
$this->get_store_currency_properties(),
[
'price' => [
'description' => __( 'Current product price.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'regular_price' => [
'description' => __( 'Regular product price.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'sale_price' => [
'description' => __( 'Sale product price, if applicable.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'price_range' => [
'description' => __( 'Price range, if applicable.', 'woocommerce' ),
'type' => [ 'object', 'null' ],
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => [
'min_amount' => [
'description' => __( 'Price amount.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'max_amount' => [
'description' => __( 'Price amount.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
],
],
'raw_prices' => [
'description' => __( 'Raw unrounded product prices used in calculations. Provided using a higher unit of precision than the currency.', 'woocommerce' ),
'type' => [ 'object', 'null' ],
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => [
'precision' => [
'description' => __( 'Decimal precision of the returned prices.', 'woocommerce' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'price' => [
'description' => __( 'Current product price.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'regular_price' => [
'description' => __( 'Regular product price.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'sale_price' => [
'description' => __( 'Sale product price, if applicable.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
],
],
]
),
],
'totals' => [
'description' => __( 'Item total amounts provided using the smallest unit of the currency.', 'woocommerce' ),
'type' => 'object',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => array_merge(
$this->get_store_currency_properties(),
[
'line_subtotal' => [
'description' => __( 'Line subtotal (the price of the product before coupon discounts have been applied).', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'line_subtotal_tax' => [
'description' => __( 'Line subtotal tax.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'line_total' => [
'description' => __( 'Line total (the price of the product after coupon discounts have been applied).', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'line_total_tax' => [
'description' => __( 'Line total tax.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
]
),
],
'catalog_visibility' => [
'description' => __( 'Whether the product is visible in the catalog', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
self::EXTENDING_KEY => $this->get_extended_schema( self::IDENTIFIER ),
];
}
}

View File

@@ -26,13 +26,19 @@ class OrderCouponSchema extends AbstractSchema {
*/
public function get_properties() {
return [
'code' => [
'code' => [
'description' => __( 'The coupons unique code.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'totals' => [
'discount_type' => [
'description' => __( 'The discount type for the coupon (e.g. percentage or fixed amount)', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'totals' => [
'description' => __( 'Total amounts provided using the smallest unit of the currency.', 'woocommerce' ),
'type' => 'object',
'context' => [ 'view', 'edit' ],
@@ -61,13 +67,15 @@ class OrderCouponSchema extends AbstractSchema {
/**
* Convert an order coupon to an object suitable for the response.
*
* @param \WC_Order_Item_Coupon $coupon Order coupon array.
* @param \WC_Order_Item_Coupon $coupon Order coupon object.
* @return array
*/
public function get_item_response( \WC_Order_Item_Coupon $coupon ) {
public function get_item_response( $coupon ) {
$coupon_object = new \WC_Coupon( $coupon->get_code() );
return [
'code' => $coupon->get_code(),
'totals' => (object) $this->prepare_currency_response(
'code' => $coupon->get_code(),
'discount_type' => $coupon_object ? $coupon_object->get_discount_type() : '',
'totals' => (object) $this->prepare_currency_response(
[
'total_discount' => $this->prepare_money_response( $coupon->get_discount(), wc_get_price_decimals() ),
'total_discount_tax' => $this->prepare_money_response( $coupon->get_discount_tax(), wc_get_price_decimals(), PHP_ROUND_HALF_DOWN ),

View File

@@ -0,0 +1,88 @@
<?php
namespace Automattic\WooCommerce\StoreApi\Schemas\V1;
/**
* OrderFeeSchema class.
*/
class OrderFeeSchema extends AbstractSchema {
/**
* The schema item name.
*
* @var string
*/
protected $title = 'order_fee';
/**
* The schema item identifier.
*
* @var string
*/
const IDENTIFIER = 'order-fee';
/**
* Cart schema properties.
*
* @return array
*/
public function get_properties() {
return [
'id' => [
'description' => __( 'Unique identifier for the fee within the cart', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'name' => [
'description' => __( 'Fee name', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'totals' => [
'description' => __( 'Fee total amounts provided using the smallest unit of the currency.', 'woocommerce' ),
'type' => 'object',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => array_merge(
$this->get_store_currency_properties(),
[
'total' => [
'description' => __( 'Total amount for this fee.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'total_tax' => [
'description' => __( 'Total tax amount for this fee.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
]
),
],
];
}
/**
* Convert a WooCommerce cart fee to an object suitable for the response.
*
* @param \WC_Order_Item_Fee $fee Order fee object.
* @return array
*/
public function get_item_response( $fee ) {
if ( ! $fee ) {
return [];
}
return [
'key' => $fee->get_id(),
'name' => $this->prepare_html_response( $fee->get_name() ),
'totals' => (object) $this->prepare_currency_response(
[
'total' => $this->prepare_money_response( $fee->get_total(), wc_get_price_decimals() ),
'total_tax' => $this->prepare_money_response( $fee->get_total_tax(), wc_get_price_decimals(), PHP_ROUND_HALF_DOWN ),
]
),
];
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace Automattic\WooCommerce\StoreApi\Schemas\V1;
use Automattic\WooCommerce\StoreApi\Utilities\ProductItemTrait;
/**
* OrderItemSchema class.
*/
class OrderItemSchema extends ItemSchema {
use ProductItemTrait;
/**
* The schema item name.
*
* @var string
*/
protected $title = 'order_item';
/**
* The schema item identifier.
*
* @var string
*/
const IDENTIFIER = 'order-item';
/**
* Get order items data.
*
* @param \WC_Order_Item_Product $order_item Order item instance.
* @return array
*/
public function get_item_response( $order_item ) {
$order = $order_item->get_order();
$product = $order_item->get_product();
return [
'key' => $order->get_order_key(),
'id' => $order_item->get_id(),
'quantity' => $order_item->get_quantity(),
'quantity_limits' => array(
'minimum' => $order_item->get_quantity(),
'maximum' => $order_item->get_quantity(),
'multiple_of' => 1,
'editable' => false,
),
'name' => $order_item->get_name(),
'short_description' => $this->prepare_html_response( wc_format_content( wp_kses_post( $product->get_short_description() ) ) ),
'description' => $this->prepare_html_response( wc_format_content( wp_kses_post( $product->get_description() ) ) ),
'sku' => $this->prepare_html_response( $product->get_sku() ),
'low_stock_remaining' => null,
'backorders_allowed' => false,
'show_backorder_badge' => false,
'sold_individually' => $product->is_sold_individually(),
'permalink' => $product->get_permalink(),
'images' => $this->get_images( $product ),
'variation' => $this->format_variation_data( $product->get_attributes(), $product ),
'item_data' => $order_item->get_all_formatted_meta_data(),
'prices' => (object) $this->prepare_product_price_response( $product, get_option( 'woocommerce_tax_display_cart' ) ),
'totals' => (object) $this->prepare_currency_response( $this->get_totals( $order_item ) ),
'catalog_visibility' => $product->get_catalog_visibility(),
];
}
/**
* Get totals data.
*
* @param \WC_Order_Item_Product $order_item Order item instance.
* @return array
*/
public function get_totals( $order_item ) {
return [
'line_subtotal' => $this->prepare_money_response( $order_item->get_subtotal(), wc_get_price_decimals() ),
'line_subtotal_tax' => $this->prepare_money_response( $order_item->get_subtotal_tax(), wc_get_price_decimals() ),
'line_total' => $this->prepare_money_response( $order_item->get_total(), wc_get_price_decimals() ),
'line_total_tax' => $this->prepare_money_response( $order_item->get_total_tax(), wc_get_price_decimals() ),
];
}
}

View File

@@ -0,0 +1,391 @@
<?php
namespace Automattic\WooCommerce\StoreApi\Schemas\V1;
use Automattic\WooCommerce\StoreApi\SchemaController;
use Automattic\WooCommerce\StoreApi\Schemas\ExtendSchema;
use Automattic\WooCommerce\StoreApi\Utilities\OrderController;
/**
* OrderSchema class.
*/
class OrderSchema extends AbstractSchema {
/**
* The schema item name.
*
* @var string
*/
protected $title = 'order';
/**
* The schema item identifier.
*
* @var string
*/
const IDENTIFIER = 'order';
/**
* Item schema instance.
*
* @var OrderItemSchema
*/
public $item_schema;
/**
* Order controller class instance.
*
* @var OrderController
*/
protected $order_controller;
/**
* Coupon schema instance.
*
* @var OrderCouponSchema
*/
public $coupon_schema;
/**
* Product item schema instance representing cross-sell items.
*
* @var ProductSchema
*/
public $cross_sells_item_schema;
/**
* Fee schema instance.
*
* @var OrderFeeSchema
*/
public $fee_schema;
/**
* Shipping rates schema instance.
*
* @var CartShippingRateSchema
*/
public $shipping_rate_schema;
/**
* Shipping address schema instance.
*
* @var ShippingAddressSchema
*/
public $shipping_address_schema;
/**
* Billing address schema instance.
*
* @var BillingAddressSchema
*/
public $billing_address_schema;
/**
* Error schema instance.
*
* @var ErrorSchema
*/
public $error_schema;
/**
* Constructor.
*
* @param ExtendSchema $extend Rest Extending instance.
* @param SchemaController $controller Schema Controller instance.
*/
public function __construct( ExtendSchema $extend, SchemaController $controller ) {
parent::__construct( $extend, $controller );
$this->item_schema = $this->controller->get( OrderItemSchema::IDENTIFIER );
$this->coupon_schema = $this->controller->get( OrderCouponSchema::IDENTIFIER );
$this->fee_schema = $this->controller->get( OrderFeeSchema::IDENTIFIER );
$this->shipping_rate_schema = $this->controller->get( CartShippingRateSchema::IDENTIFIER );
$this->shipping_address_schema = $this->controller->get( ShippingAddressSchema::IDENTIFIER );
$this->billing_address_schema = $this->controller->get( BillingAddressSchema::IDENTIFIER );
$this->error_schema = $this->controller->get( ErrorSchema::IDENTIFIER );
$this->order_controller = new OrderController();
}
/**
* Order schema properties.
*
* @return array
*/
public function get_properties() {
return [
'id' => [
'description' => __( 'The order ID.', 'woocommerce' ),
'type' => 'integer',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'items' => [
'description' => __( 'Line items data.', 'woocommerce' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
'items' => [
'type' => 'object',
'properties' => $this->force_schema_readonly( $this->item_schema->get_properties() ),
],
],
'totals' => [
'description' => __( 'Order totals.', 'woocommerce' ),
'type' => 'object',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => array_merge(
$this->get_store_currency_properties(),
[
'subtotal' => [
'description' => __( 'Subtotal of the order.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'total_discount' => [
'description' => __( 'Total discount from applied coupons.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'total_shipping' => [
'description' => __( 'Total price of shipping.', 'woocommerce' ),
'type' => [ 'string', 'null' ],
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'total_fees' => [
'description' => __( 'Total price of any applied fees.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'total_tax' => [
'description' => __( 'Total tax applied to the order.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'total_refund' => [
'description' => __( 'Total refund applied to the order.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'total_price' => [
'description' => __( 'Total price the customer will pay.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'total_items' => [
'description' => __( 'Total price of items in the order.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'total_items_tax' => [
'description' => __( 'Total tax on items in the order.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'total_fees_tax' => [
'description' => __( 'Total tax on fees.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'total_discount_tax' => [
'description' => __( 'Total tax removed due to discount from applied coupons.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'total_shipping_tax' => [
'description' => __( 'Total tax on shipping. If shipping has not been calculated, a null response will be sent.', 'woocommerce' ),
'type' => [ 'string', 'null' ],
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'tax_lines' => [
'description' => __( 'Lines of taxes applied to items and shipping.', 'woocommerce' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'items' => [
'type' => 'object',
'properties' => [
'name' => [
'description' => __( 'The name of the tax.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'price' => [
'description' => __( 'The amount of tax charged.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'rate' => [
'description' => __( 'The rate at which tax is applied.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
],
],
],
]
),
],
'coupons' => [
'description' => __( 'List of applied cart coupons.', 'woocommerce' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'items' => [
'type' => 'object',
'properties' => $this->force_schema_readonly( $this->coupon_schema->get_properties() ),
],
],
'shipping_address' => [
'description' => __( 'Current set shipping address for the customer.', 'woocommerce' ),
'type' => 'object',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => $this->force_schema_readonly( $this->shipping_address_schema->get_properties() ),
],
'billing_address' => [
'description' => __( 'Current set billing address for the customer.', 'woocommerce' ),
'type' => 'object',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'properties' => $this->force_schema_readonly( $this->billing_address_schema->get_properties() ),
],
'needs_payment' => [
'description' => __( 'True if the cart needs payment. False for carts with only free products and no shipping costs.', 'woocommerce' ),
'type' => 'boolean',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'needs_shipping' => [
'description' => __( 'True if the cart needs shipping. False for carts with only digital goods or stores with no shipping methods set-up.', 'woocommerce' ),
'type' => 'boolean',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'errors' => [
'description' => __( 'List of cart item errors, for example, items in the cart which are out of stock.', 'woocommerce' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
'readonly' => true,
'items' => [
'type' => 'object',
'properties' => $this->force_schema_readonly( $this->error_schema->get_properties() ),
],
],
'payment_requirements' => [
'description' => __( 'List of required payment gateway features to process the order.', 'woocommerce' ),
'type' => 'array',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
'status' => [
'description' => __( 'Status of the order.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
'readonly' => true,
],
];
}
/**
* Get an order for response.
*
* @param \WC_Order $order Order instance.
* @return array
*/
public function get_item_response( $order ) {
$order_id = $order->get_id();
$errors = [];
$failed_order_stock_error = $this->order_controller->get_failed_order_stock_error( $order_id );
if ( $failed_order_stock_error ) {
$errors[] = $failed_order_stock_error;
}
return [
'id' => $order_id,
'status' => $order->get_status(),
'items' => $this->get_item_responses_from_schema( $this->item_schema, $order->get_items() ),
'coupons' => $this->get_item_responses_from_schema( $this->coupon_schema, $order->get_items( 'coupon' ) ),
'fees' => $this->get_item_responses_from_schema( $this->fee_schema, $order->get_items( 'fee' ) ),
'totals' => (object) $this->prepare_currency_response( $this->get_totals( $order ) ),
'shipping_address' => (object) $this->shipping_address_schema->get_item_response( $order ),
'billing_address' => (object) $this->billing_address_schema->get_item_response( $order ),
'needs_payment' => $order->needs_payment(),
'needs_shipping' => $order->needs_shipping_address(),
'payment_requirements' => $this->extend->get_payment_requirements(),
'errors' => $errors,
];
}
/**
* Get total data.
*
* @param \WC_Order $order Order instance.
* @return array
*/
protected function get_totals( $order ) {
return [
'subtotal' => $this->prepare_money_response( $order->get_subtotal() ),
'total_discount' => $this->prepare_money_response( $order->get_total_discount() ),
'total_shipping' => $this->prepare_money_response( $order->get_total_shipping() ),
'total_fees' => $this->prepare_money_response( $order->get_total_fees() ),
'total_tax' => $this->prepare_money_response( $order->get_total_tax() ),
'total_refund' => $this->prepare_money_response( $order->get_total_refunded() ),
'total_price' => $this->prepare_money_response( $order->get_total() ),
'total_items' => $this->prepare_money_response(
array_sum(
array_map(
function( $item ) {
return $item->get_total();
},
array_values( $order->get_items( 'line_item' ) )
)
)
),
'total_items_tax' => $this->prepare_money_response(
array_sum(
array_map(
function( $item ) {
return $item->get_tax_total();
},
array_values( $order->get_items( 'tax' ) )
)
)
),
'total_fees_tax' => $this->prepare_money_response(
array_sum(
array_map(
function( $item ) {
return $item->get_total_tax();
},
array_values( $order->get_items( 'fee' ) )
)
)
),
'total_discount_tax' => $this->prepare_money_response( $order->get_discount_tax() ),
'total_shipping_tax' => $this->prepare_money_response( $order->get_shipping_tax() ),
'tax_lines' => array_map(
function( $item ) {
return [
'name' => $item->get_name(),
'price' => $item->get_tax_total(),
'rate' => strval( $item->get_rate_percent() ),
];
},
array_values( $order->get_items( 'tax' ) )
),
];
}
}

View File

@@ -158,7 +158,7 @@ class ProductReviewSchema extends AbstractSchema {
* @param \WP_Comment $review Product review object.
* @return array
*/
public function get_item_response( \WP_Comment $review ) {
public function get_item_response( $review ) {
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$rating = get_comment_meta( $review->comment_ID, 'rating', true ) === '' ? null : (int) get_comment_meta( $review->comment_ID, 'rating', true );
$data = [

View File

@@ -59,6 +59,11 @@ class ProductSchema extends AbstractSchema {
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
'slug' => [
'description' => __( 'Product slug.', 'woocommerce' ),
'type' => 'string',
'context' => [ 'view', 'edit' ],
],
'parent' => [
'description' => __( 'ID of the parent product, if applicable.', 'woocommerce' ),
'type' => 'integer',
@@ -449,6 +454,7 @@ class ProductSchema extends AbstractSchema {
return [
'id' => $product->get_id(),
'name' => $this->prepare_html_response( $product->get_title() ),
'slug' => $product->get_slug(),
'parent' => $product->get_parent_id(),
'type' => $product->get_type(),
'variation' => $this->prepare_html_response( $product->is_type( 'variation' ) ? wc_get_formatted_variation( $product, true, true, false ) : '' ),

View File

@@ -30,7 +30,7 @@ class ShippingAddressSchema extends AbstractAddressSchema {
* @param \WC_Order|\WC_Customer $address An object with shipping address.
*
* @throws RouteException When the invalid object types are provided.
* @return stdClass
* @return array
*/
public function get_item_response( $address ) {
$validation_util = new ValidationUtils();
@@ -42,7 +42,7 @@ class ShippingAddressSchema extends AbstractAddressSchema {
$shipping_state = '';
}
return (object) $this->prepare_html_response(
return $this->prepare_html_response(
[
'first_name' => $address->get_shipping_first_name(),
'last_name' => $address->get_shipping_last_name(),

Some files were not shown because too many files have changed in this diff Show More