rebase code on oct-10-2023

This commit is contained in:
Rachit Bhargava
2023-10-10 17:51:46 -04:00
parent b16ad94b69
commit 8f1a2c3a66
2197 changed files with 184921 additions and 35568 deletions

View File

@@ -18,34 +18,6 @@ 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
*
@@ -59,18 +31,7 @@ class Api {
* @param Package $package An instance of Package.
*/
public function __construct( 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 );
$this->package = $package;
}
/**
@@ -115,64 +76,6 @@ 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.
*
@@ -182,37 +85,31 @@ class Api {
* @return array src, version and dependencies of the script.
*/
public function get_script_data( $relative_src, $dependencies = [] ) {
if ( ! $relative_src ) {
return array(
'src' => '',
'version' => '1',
'dependencies' => $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 ( file_exists( $asset_path ) ) {
// 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 = 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 );
}
}
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 ),
];
return array(
'src' => $src,
'version' => $version,
'dependencies' => $dependencies,
);
}
/**

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;
@@ -293,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. 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 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 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.
*
@@ -317,40 +317,16 @@ class AssetDataRegistry {
}
/**
* Hydrate from the API.
* Hydrate from 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[ $path ] = Package::container()->get( Hydration::class )->get_rest_api_response_data( $path );
$this->preloaded_api_requests = rest_preload_api_request( $this->preloaded_api_requests, $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

@@ -46,17 +46,10 @@ final class AssetsController {
* Register block scripts & styles.
*/
public function register_assets() {
$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 );
$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 );
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 );

View File

@@ -1,7 +1,6 @@
<?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;
@@ -11,7 +10,6 @@ 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;
/**
@@ -70,7 +68,6 @@ 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 );
@@ -134,23 +131,6 @@ class BlockTemplatesController {
}
}
/**
* 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.
*
@@ -343,7 +323,7 @@ class BlockTemplatesController {
* @return array
*/
public function add_block_templates( $query_result, $query, $template_type ) {
if ( ! BlockTemplateUtils::supports_block_templates( $template_type ) ) {
if ( ! BlockTemplateUtils::supports_block_templates() ) {
return $query_result;
}
@@ -620,13 +600,7 @@ class BlockTemplatesController {
if (
is_singular( 'product' ) && $this->block_template_is_available( '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 ) );
$templates = get_block_templates( array( 'slug__in' => array( 'single-product' ) ) );
if ( isset( $templates[0] ) && BlockTemplateUtils::template_has_legacy_template_block( $templates[0] ) ) {
add_filter( 'woocommerce_disable_compatibility_layer', '__return_true' );
@@ -768,18 +742,117 @@ class BlockTemplatesController {
* 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' ) ) {
if ( ! $this->has_migrated_page( 'cart' ) ) {
$this->migrate_page( 'cart', CartTemplate::get_placeholder_page() );
}
if ( ! $this->has_migrated_page( 'checkout' ) ) {
$this->migrate_page( 'checkout', CheckoutTemplate::get_placeholder_page() );
}
}
/**
* Check if a page has been migrated to a template.
*
* @param string $page_id Page ID.
* @return boolean
*/
protected function has_migrated_page( $page_id ) {
return (bool) get_option( 'has_migrated_' . $page_id, false );
}
/**
* Prepare default page template.
*
* @param \WP_Post $page Page object.
* @return string
*/
protected function get_default_migrate_page_template( $page ) {
$default_template_content = $this->get_block_template_part( 'header' );
$default_template_content .= '
<!-- wp:group {"layout":{"inherit":true}} -->
<div class="wp-block-group">
<!-- wp:heading {"level":1} -->
<h1 class="wp-block-heading">' . wp_kses_post( $page->post_title ) . '</h1>
<!-- /wp:heading -->
' . wp_kses_post( $page->post_content ) . '
</div>
<!-- /wp:group -->
';
$default_template_content .= $this->get_block_template_part( 'footer' );
return $default_template_content;
}
/**
* Migrates a page to a template if needed.
*
* @param string $page_id Page ID.
* @param \WP_Post $page Page object.
*/
protected function migrate_page( $page_id, $page ) {
if ( ! $page || empty( $page->post_content ) ) {
update_option( 'has_migrated_' . $page_id, '1' );
return;
}
if ( ! BlockTemplateMigrationUtils::has_migrated_page( 'cart' ) ) {
BlockTemplateMigrationUtils::migrate_page( 'cart', CartTemplate::get_placeholder_page() );
// Use the page template if it exists, which we'll use over our default template if found.
$existing_page_template = BlockTemplateUtils::get_block_template( get_stylesheet() . '//page', 'wp_template' );
if ( $existing_page_template && ! empty( $existing_page_template->content ) && strstr( $existing_page_template->content, 'wp:post-content' ) ) {
// Massage the original content into something we can use. Replace post content with a group block.
$pattern = '/(<!--\s*)wp:post-content(.*?)(\/-->)/';
$replacement = '
<!-- wp:group $2 -->
<div class="wp-block-group">' . wp_kses_post( $page->post_content ) . '</div>
<!-- /wp:group -->
';
$template_content = preg_replace( $pattern, $replacement, $existing_page_template->content );
} else {
$template_content = $this->get_default_migrate_page_template( $page );
}
if ( ! BlockTemplateMigrationUtils::has_migrated_page( 'checkout' ) ) {
BlockTemplateMigrationUtils::migrate_page( 'checkout', CheckoutTemplate::get_placeholder_page() );
$new_page_template = BlockTemplateUtils::get_block_template( 'woocommerce/woocommerce//' . $page_id, 'wp_template' );
// Check template validity--template must exist, and custom template must not be present already.
if ( ! $new_page_template || $new_page_template->wp_id ) {
update_option( 'has_migrated_' . $page_id, '1' );
return;
}
$new_page_template_id = wp_insert_post(
[
'post_name' => $new_page_template->slug,
'post_type' => 'wp_template',
'post_status' => 'publish',
'tax_input' => array(
'wp_theme' => $new_page_template->theme,
),
'meta_input' => array(
'origin' => $new_page_template->source,
),
'post_content' => $template_content,
],
true
);
if ( ! is_wp_error( $new_page_template_id ) ) {
update_option( 'has_migrated_' . $page_id, '1' );
}
}
/**
* Returns the requested template part.
*
* @param string $part The part to return.
*
* @return string
*/
protected function get_block_template_part( $part ) {
$template_part = BlockTemplateUtils::get_block_template( get_stylesheet() . '//' . $part, 'wp_template_part' );
if ( ! $template_part || empty( $template_part->content ) ) {
return '';
}
return $template_part->content;
}
/**

View File

@@ -303,12 +303,9 @@ abstract class AbstractBlock {
* @return string[]|null
*/
protected function get_block_type_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 ];
}
$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-all-blocks-style' ];
return [ 'wc-blocks-style', 'wc-blocks-style-' . $this->block_name ];
}
/**

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,13 @@ abstract class AbstractProductGrid extends AbstractDynamicBlock {
*/
protected function enqueue_data( array $attributes = [] ) {
parent::enqueue_data( $attributes );
$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 );
$this->asset_data_registry->add( 'stock_status_options', wc_get_product_stock_status_options(), true );
}
/**

View File

@@ -96,7 +96,7 @@ class AddToCartForm extends AbstractBlock {
$classname = $attributes['className'] ?? '';
$classes_and_styles = StyleAttributesUtils::get_classes_and_styles_by_attributes( $attributes );
$product_classname = $is_descendent_of_single_product_block ? 'product' : '';
$product_classname = $is_descendent_of_single_product_block ? 'product' : '';
$form = sprintf(
'<div class="wp-block-add-to-cart-form %1$s %2$s %3$s" style="%4$s">%5$s</div>',

View File

@@ -22,20 +22,29 @@ 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( '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( 'has_filterable_products', true, true );
// 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.
$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.
if ( ! is_admin() && ! WC()->is_rest_api_request() ) {
$this->asset_data_registry->hydrate_api_request( '/wc/store/v1/cart' );
$this->hydrate_from_api();
}
}
/**
* 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

@@ -40,14 +40,6 @@ class Cart extends AbstractBlock {
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(
@@ -239,7 +231,7 @@ class Cart extends AbstractBlock {
// Hydrate the following data depending on admin or frontend context.
if ( ! is_admin() && ! WC()->is_rest_api_request() ) {
$this->asset_data_registry->hydrate_api_request( '/wc/store/v1/cart' );
$this->hydrate_from_api();
}
/**
@@ -250,6 +242,19 @@ class Cart extends AbstractBlock {
do_action( 'woocommerce_blocks_cart_enqueue_data' );
}
/**
* Hydrate the cart block with data from the API.
*/
protected function hydrate_from_api() {
// Cache existing notices now, otherwise they are caught by the Cart Controller and converted to exceptions.
$old_notices = WC()->session->get( 'wc_notices', array() );
wc_clear_notices();
$this->asset_data_registry->hydrate_api_request( '/wc/store/v1/cart' );
// Restore notices.
WC()->session->set( 'wc_notices', $old_notices );
}
/**
* Register script and style assets for the block type before it is registered.
*

View File

@@ -24,31 +24,6 @@ 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.
*
@@ -323,8 +298,7 @@ class Checkout extends AbstractBlock {
}
if ( ! is_admin() && ! WC()->is_rest_api_request() ) {
$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_from_api();
$this->hydrate_customer_payment_methods();
}
@@ -392,6 +366,25 @@ 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() {
// Cache existing notices now, otherwise they are caught by the Cart Controller and converted to exceptions.
$old_notices = WC()->session->get( 'wc_notices', array() );
wc_clear_notices();
$this->asset_data_registry->hydrate_api_request( '/wc/store/v1/cart' );
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' );
// Restore notices.
WC()->session->set( 'wc_notices', $old_notices );
}
/**
* Callback for woocommerce_payment_methods_list_item filter to add token id
* to the generated list.

View File

@@ -6,10 +6,9 @@ use Automattic\WooCommerce\Blocks\Templates\ProductSearchResultsTemplate;
use Automattic\WooCommerce\Blocks\Templates\OrderConfirmationTemplate;
use Automattic\WooCommerce\Blocks\Utils\StyleAttributesUtils;
use WC_Shortcode_Checkout;
use WC_Frontend_Scripts;
/**
* Classic Template class
* Classic Single Product class
*
* @internal
*/
@@ -48,17 +47,10 @@ class ClassicTemplate extends AbstractDynamicBlock {
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();
$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']
);
wp_enqueue_style( $handle, $style['src'], $style['deps'], $style['version'], $style['media'] );
}
}
}
@@ -83,7 +75,7 @@ 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();
}
@@ -91,7 +83,7 @@ class ClassicTemplate extends AbstractDynamicBlock {
return $this->render_order_received();
}
if ( is_product() ) {
if ( 'single-product' === $attributes['template'] ) {
return $this->render_single_product();
}
@@ -105,13 +97,13 @@ class ClassicTemplate extends AbstractDynamicBlock {
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( 'isRenderingPhpTemplate', true, true );
$this->asset_data_registry->add( 'is_rendering_php_template', true, true );
// Set this so filter blocks being used as widgets know when to render.
$this->asset_data_registry->add( 'hasFilterableProducts', true, true );
$this->asset_data_registry->add( 'has_filterable_products', true, true );
$this->asset_data_registry->add(
'pageUrl',
'page_url',
html_entity_decode( get_pagenum_link() ),
''
);

View File

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

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 ); // phpcs:ignore WordPress.WP.PostsPerPage.posts_per_page_posts_per_page
$query_args['posts_per_page'] = count( $ids );
}
/**

View File

@@ -9,7 +9,6 @@ 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.
@@ -157,7 +156,7 @@ class MiniCart extends AbstractBlock {
if (
current_user_can( 'edit_theme_options' ) &&
( wc_current_theme_is_fse_theme() || current_theme_supports( 'block-template-parts' ) )
wc_current_theme_is_fse_theme()
) {
$theme_slug = BlockTemplateUtils::theme_has_template_part( 'mini-cart' ) ? wp_get_theme()->get_stylesheet() : BlockTemplateUtils::PLUGIN_SLUG;
@@ -352,7 +351,7 @@ class MiniCart extends AbstractBlock {
if ( isset( $attributes['hasHiddenPrice'] ) && false !== $attributes['hasHiddenPrice'] ) {
return;
}
$price_color = array_key_exists( 'priceColor', $attributes ) ? $attributes['priceColor']['color'] : '';
$price_color = array_key_exists( 'priceColorValue', $attributes ) ? $attributes['priceColorValue'] : '';
return '<span class="wc-block-mini-cart__amount" style="color:' . $price_color . ' "></span>' . $this->get_include_tax_label_markup( $attributes );
}
@@ -368,7 +367,7 @@ class MiniCart extends AbstractBlock {
if ( empty( $this->tax_label ) ) {
return '';
}
$price_color = array_key_exists( 'priceColor', $attributes ) ? $attributes['priceColor']['color'] : '';
$price_color = array_key_exists( 'priceColorValue', $attributes ) ? $attributes['priceColorValue'] : '';
return '<small class="wc-block-mini-cart__tax-label" style="color:' . $price_color . ' " hidden>' . esc_html( $this->tax_label ) . '</small>';
}
@@ -382,7 +381,7 @@ class MiniCart extends AbstractBlock {
* @return string Rendered block type output.
*/
protected function render( $attributes, $content, $block ) {
return $content . $this->get_markup( MiniCartUtils::migrate_attributes_to_color_panel( $attributes ) );
return $content . $this->get_markup( $attributes );
}
/**
@@ -406,8 +405,8 @@ class MiniCart extends AbstractBlock {
}
$wrapper_styles = $classes_styles['styles'];
$icon_color = array_key_exists( 'iconColor', $attributes ) ? $attributes['iconColor']['color'] : 'currentColor';
$product_count_color = array_key_exists( 'productCountColor', $attributes ) ? $attributes['productCountColor']['color'] : '';
$icon_color = array_key_exists( 'iconColorValue', $attributes ) ? $attributes['iconColorValue'] : 'currentColor';
$product_count_color = array_key_exists( 'productCountColorValue', $attributes ) ? $attributes['productCountColorValue'] : '';
// 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">

View File

@@ -1,103 +0,0 @@
<?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,20 +15,11 @@ class ProductButton extends AbstractBlock {
*/
protected $block_name = 'product-button';
/**
* Get the frontend script handle for this block type.
*
* @param string $key Data to get, or default to everything.
* 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 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' ],
];
return $key ? $script[ $key ] : $script;
protected function register_block_type_assets() {
return null;
}
/**
@@ -38,31 +29,6 @@ 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.
*
@@ -72,17 +38,10 @@ class ProductButton extends AbstractBlock {
* @return string Rendered block type output.
*/
protected function render( $attributes, $content, $block ) {
$post_id = isset( $block->context['postId'] ) ? $block->context['postId'] : '';
$post_id = $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();
@@ -105,40 +64,6 @@ 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.
*
@@ -162,25 +87,6 @@ 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.
*
@@ -190,83 +96,22 @@ class ProductButton extends AbstractBlock {
*/
return apply_filters(
'woocommerce_loop_add_to_cart_link',
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}
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>
</div>',
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() : '',
)
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() )
),
$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

@@ -16,13 +16,6 @@ class ProductCollection extends AbstractBlock {
*/
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.
*
@@ -91,102 +84,6 @@ class ProductCollection extends AbstractBlock {
// 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 );
}
/**
@@ -238,14 +135,12 @@ class ProductCollection extends AbstractBlock {
return;
}
$this->parsed_block = $parsed_block;
$this->asset_data_registry->add( 'hasFilterableProducts', true, true );
$this->asset_data_registry->add( 'has_filterable_products', 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 );
$this->asset_data_registry->add( 'is_rendering_php_template', true, true );
}
/**
@@ -265,9 +160,6 @@ class ProductCollection extends AbstractBlock {
}
$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 );
}

View File

@@ -39,13 +39,10 @@ class ProductDetails extends AbstractBlock {
return sprintf(
'<div class="wp-block-woocommerce-product-details %1$s %2$s">
<div style="%3$s">
%4$s
</div>
%3$s
</div>',
esc_attr( $classes_and_styles['classes'] ),
esc_attr( $classname ),
esc_attr( $classes_and_styles['styles'] ),
$tabs
);
}

View File

@@ -1,80 +0,0 @@
<?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

@@ -1,93 +0,0 @@
<?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

@@ -190,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( 'isBlockThemeEnabled', wc_current_theme_is_fse_theme(), false );
$this->asset_data_registry->add( 'is_block_theme_enabled', wc_current_theme_is_fse_theme(), false );
}
@@ -210,9 +210,11 @@ 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 = isset( $block->context['postId'] ) ? $block->context['postId'] : '';
$post_id = $block->context['postId'];
$product = wc_get_product( $post_id );
if ( $product ) {

View File

@@ -87,7 +87,7 @@ class ProductPrice extends AbstractBlock {
return $content;
}
$post_id = isset( $block->context['postId'] ) ? $block->context['postId'] : '';
$post_id = $block->context['postId'];
$product = wc_get_product( $post_id );
if ( $product ) {

View File

@@ -72,12 +72,6 @@ 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 );
}
@@ -126,13 +120,9 @@ class ProductQuery extends AbstractBlock {
$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',
$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 );
}
/**
@@ -146,22 +136,6 @@ 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.
*
@@ -177,8 +151,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( 'hasFilterableProducts', true, true );
$this->asset_data_registry->add( 'isRenderingPhpTemplate', true, true );
$this->asset_data_registry->add( 'has_filterable_products', true, true );
$this->asset_data_registry->add( 'is_rendering_php_template', true, true );
add_filter(
'query_loop_block_query_vars',
array( $this, 'build_query' ),

View File

@@ -86,7 +86,7 @@ class ProductRating extends AbstractBlock {
* @return null
*/
protected function get_block_type_style() {
return array_merge( parent::get_block_type_style(), [ 'wc-blocks-packages-style' ] );
return null;
}
/**

View File

@@ -1,209 +0,0 @@
<?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

@@ -3,7 +3,7 @@ namespace Automattic\WooCommerce\Blocks\BlockTypes;
use Automattic\WooCommerce\Blocks\Utils\StyleAttributesUtils;
/**
* ProductRatingStars class.
* ProductRating class.
*/
class ProductRatingStars extends AbstractBlock {
@@ -47,7 +47,7 @@ class ProductRatingStars extends AbstractBlock {
'padding' => true,
'__experimentalSkipSerialization' => true,
),
'__experimentalSelector' => '.wc-block-components-product-rating-stars',
'__experimentalSelector' => '.wc-block-components-product-rating',
);
}

View File

@@ -75,10 +75,14 @@ class SingleProduct extends AbstractBlock {
* @return string Rendered block output.
*/
protected function render( $attributes, $content, $block ) {
$classname = $attributes['className'] ?? '';
$wrapper_attributes = get_block_wrapper_attributes( array( 'class' => trim( sprintf( 'woocommerce %1$s', $classname ) ) ) );
$html = sprintf(
'<div class="woocommerce">
%1$s
'<div %1$s>
%2$s
</div>',
$wrapper_attributes,
$content
);

View File

@@ -1,6 +1,7 @@
<?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;
@@ -60,10 +61,10 @@ final class BlockTypesController {
$block_types = $this->get_block_types();
foreach ( $block_types as $block_type ) {
$block_type_class = __NAMESPACE__ . '\\BlockTypes\\' . $block_type;
new $block_type_class( $this->asset_api, $this->asset_data_registry, new IntegrationRegistry() );
$block_type_class = __NAMESPACE__ . '\\BlockTypes\\' . $block_type;
$block_type_instance = new $block_type_class( $this->asset_api, $this->asset_data_registry, new IntegrationRegistry() );
}
}
/**
@@ -189,16 +190,14 @@ final class BlockTypesController {
'ProductButton',
'ProductCategories',
'ProductCategory',
'ProductGallery',
'ProductImage',
'ProductImageGallery',
'ProductNew',
'ProductOnSale',
'ProductPrice',
'ProductQuery',
'ProductAverageRating',
'ProductRating',
'ProductRatingCounter',
'ProductRatingStars',
'ProductResultsCount',
'ProductReviews',
'ProductSaleBadge',
@@ -228,10 +227,8 @@ final class BlockTypesController {
if ( Package::feature()->is_experimental_build() ) {
$block_types[] = 'ProductCollection';
$block_types[] = 'ProductRatingStars';
$block_types[] = 'ProductTemplate';
$block_types[] = 'ProductGallery';
$block_types[] = 'ProductGalleryLargeImage';
$block_types[] = 'ProductGalleryThumbnails';
}
/**

View File

@@ -12,7 +12,6 @@ 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;
@@ -100,7 +99,6 @@ class Bootstrap {
protected function init() {
$this->register_dependencies();
$this->register_payment_methods();
$this->load_interactivity_api();
// 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() ) {
@@ -120,43 +118,33 @@ 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( 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( BlockPatterns::class );
$this->container->get( PaymentsApi::class );
$this->container->get( ShippingController::class )->init();
}
/**
@@ -226,13 +214,6 @@ 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.
*/
@@ -358,6 +339,10 @@ 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 );
}
@@ -368,12 +353,6 @@ 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,16 +22,13 @@ class GoogleAnalytics {
*/
public function __construct( AssetApi $asset_api ) {
$this->asset_api = $asset_api;
$this->init();
}
/**
* Hook into WP.
*/
public function init() {
// Require Google Analytics Integration to be activated.
if ( ! class_exists( 'WC_Google_Analytics_Integration', false ) ) {
return;
}
protected function init() {
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

@@ -1,97 +0,0 @@
<?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

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

View File

@@ -0,0 +1,9 @@
<?php
/**
* Print client-side navigation meta tag (hard-coded for now).
*/
function woocommerce_interactivity_add_client_side_navigation_meta_tag() {
echo '<meta itemprop="wc-client-side-navigation" content="active">';
}
add_action( 'wp_head', 'woocommerce_interactivity_add_client_side_navigation_meta_tag' );

View File

@@ -2,3 +2,4 @@
require __DIR__ . '/class-wc-interactivity-store.php';
require __DIR__ . '/store.php';
require __DIR__ . '/scripts.php';
require __DIR__ . '/client-side-navigation.php';

View File

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

View File

@@ -37,12 +37,13 @@ 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.
*/
public function init() {
protected 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' ) );
@@ -79,13 +80,13 @@ class Api {
* Add payment method data to Asset Registry.
*/
public function add_payment_method_script_data() {
// Enqueue the order of enabled gateways.
if ( ! $this->asset_registry->exists( 'paymentMethodSortOrder' ) ) {
// Enqueue the order of enabled gateways as `paymentGatewaySortOrder`.
if ( ! $this->asset_registry->exists( 'paymentGatewaySortOrder' ) ) {
// 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( 'paymentMethodSortOrder', array_keys( $enabled_gateways ) );
$this->asset_registry->add( 'paymentGatewaySortOrder', 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() ] = $payment_method->get_payment_method_data();
$script_data[ $payment_method->get_name() . '_data' ] = $payment_method->get_payment_method_data();
}
return array( 'paymentMethodData' => array_filter( $script_data ) );
return array_filter( $script_data );
}
}

View File

@@ -114,17 +114,15 @@ abstract class AbstractCartRoute extends AbstractRoute {
}
}
// 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 );
}
// Format error responses.
if ( is_wp_error( $response ) ) {
$response = $this->error_to_response( $response );
}
return $this->add_response_headers( rest_ensure_response( $response ) );
if ( $this->is_update_request( $request ) ) {
$this->cart_updated( $request );
}
return $this->add_response_headers( $response );
}
/**
@@ -234,9 +232,7 @@ abstract class AbstractCartRoute extends AbstractRoute {
$draft_order = $this->get_draft_order();
if ( $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 );
$this->order_controller->update_order_from_cart( $draft_order );
wc_do_deprecated_action(
'woocommerce_blocks_cart_update_order_from_request',

View File

@@ -163,10 +163,9 @@ 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 ) {
return $this->get_route_error_response( 'woocommerce_rest_invalid_endpoint', __( 'Method not implemented', 'woocommerce' ), 404 );
throw new RouteException( 'woocommerce_rest_invalid_endpoint', __( 'Method not implemented', 'woocommerce' ), 404 );
}
/**
@@ -176,10 +175,9 @@ 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 ) {
return $this->get_route_error_response( 'woocommerce_rest_invalid_endpoint', __( 'Method not implemented', 'woocommerce' ), 404 );
throw new RouteException( 'woocommerce_rest_invalid_endpoint', __( 'Method not implemented', 'woocommerce' ), 404 );
}
/**
@@ -189,10 +187,9 @@ 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 ) {
return $this->get_route_error_response( 'woocommerce_rest_invalid_endpoint', __( 'Method not implemented', 'woocommerce' ), 404 );
throw new RouteException( 'woocommerce_rest_invalid_endpoint', __( 'Method not implemented', 'woocommerce' ), 404 );
}
/**
@@ -202,10 +199,9 @@ 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 ) {
return $this->get_route_error_response( 'woocommerce_rest_invalid_endpoint', __( 'Method not implemented', 'woocommerce' ), 404 );
throw new RouteException( 'woocommerce_rest_invalid_endpoint', __( 'Method not implemented', 'woocommerce' ), 404 );
}
/**

View File

@@ -69,8 +69,8 @@ class CartSelectShippingRate extends AbstractCartRoute {
}
$cart = $this->cart_controller->get_cart_instance();
$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'] ) );
$package_id = isset( $request['package_id'] ) ? wc_clean( wp_unslash( $request['package_id'] ) ) : null;
$rate_id = wc_clean( 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 addresses, recalculates the cart totals, and returns an updated cart.
* Updates the customer billing and shipping address and returns an updated cart--things such as taxes may be recalculated.
*/
class CartUpdateCustomer extends AbstractCartRoute {
use DraftOrderTrait;

View File

@@ -1,6 +1,7 @@
<?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;
@@ -8,14 +9,12 @@ 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.
@@ -139,6 +138,29 @@ 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.
*
@@ -330,7 +352,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, true );
$this->order_controller->update_order_from_cart( $this->order );
}
wc_do_deprecated_action(
@@ -444,6 +466,133 @@ 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 ) );
$this->order->set_payment_method_title( $this->get_request_payment_method_title( $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 title from the request.
*
* @throws RouteException On error.
* @param \WP_REST_Request $request Request object.
* @return string
*/
private function get_request_payment_method_title( \WP_REST_Request $request ) {
$payment_method = $this->get_request_payment_method( $request );
return is_null( $payment_method ) ? '' : $payment_method->get_title();
}
/**
* Gets the chosen payment method from the request.
*
@@ -484,6 +633,26 @@ 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

@@ -1,266 +0,0 @@
<?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

@@ -1,87 +0,0 @@
<?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

@@ -1,7 +1,6 @@
<?php
namespace Automattic\WooCommerce\StoreApi;
use Automattic\WooCommerce\Blocks\Package;
use Automattic\WooCommerce\StoreApi\SchemaController;
use Exception;
use Routes\AbstractRoute;
@@ -61,11 +60,6 @@ class RoutesController {
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,12 +43,7 @@ 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

@@ -63,16 +63,6 @@ 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

@@ -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 array
* @return stdClass
*/
public function get_item_response( $address ) {
$validation_util = new ValidationUtils();
@@ -99,7 +99,7 @@ class BillingAddressSchema extends AbstractAddressSchema {
$billing_state = '';
}
return $this->prepare_html_response(
return (object) $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\ProductItemTrait;
use Automattic\WooCommerce\StoreApi\Utilities\DraftOrderTrait;
use Automattic\WooCommerce\StoreApi\Utilities\QuantityLimits;
/**
* CartItemSchema class.
*/
class CartItemSchema extends ItemSchema {
use ProductItemTrait;
class CartItemSchema extends ProductSchema {
use DraftOrderTrait;
/**
* The schema item name.
@@ -24,6 +24,308 @@ class CartItemSchema extends ItemSchema {
*/
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.
*
@@ -78,6 +380,82 @@ class CartItemSchema extends ItemSchema {
];
}
/**
* 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,52 +349,42 @@ class CartSchema extends AbstractSchema {
$cross_sells = array_filter( array_map( 'wc_get_product', $cart->get_cross_sells() ), 'wc_products_array_filter_visible' );
return [
'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 ),
'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() ),
'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.
*
@@ -409,12 +399,11 @@ 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, $decimals ),
'price' => $this->prepare_money_response( $cart_tax_total->amount, wc_get_price_decimals() ),
'rate' => WC_Tax::get_rate_percent( $cart_tax_total->tax_rate_id ),
);
}

View File

@@ -1,37 +0,0 @@
<?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

@@ -197,8 +197,8 @@ class CheckoutSchema extends AbstractSchema {
'order_number' => $order->get_order_number(),
'customer_note' => $order->get_customer_note(),
'customer_id' => $order->get_customer_id(),
'billing_address' => (object) $this->billing_address_schema->get_item_response( $order ),
'shipping_address' => (object) $this->shipping_address_schema->get_item_response( $order ),
'billing_address' => $this->billing_address_schema->get_item_response( $order ),
'shipping_address' => $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( $error ) {
public function get_item_response( \WP_Error $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 object|null
* @return array|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 null;
return [];
}
$thumbnail = wp_get_attachment_image_src( $attachment_id, 'woocommerce_thumbnail' );
return (object) [
return [
'id' => (int) $attachment_id,
'src' => current( $attachment ),
'thumbnail' => current( $thumbnail ),

View File

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

@@ -1,88 +0,0 @@
<?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

@@ -1,78 +0,0 @@
<?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

@@ -1,391 +0,0 @@
<?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( $review ) {
public function get_item_response( \WP_Comment $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

@@ -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 array
* @return stdClass
*/
public function get_item_response( $address ) {
$validation_util = new ValidationUtils();
@@ -42,7 +42,7 @@ class ShippingAddressSchema extends AbstractAddressSchema {
$shipping_state = '';
}
return $this->prepare_html_response(
return (object) $this->prepare_html_response(
[
'first_name' => $address->get_shipping_first_name(),
'last_name' => $address->get_shipping_last_name(),

View File

@@ -1,182 +0,0 @@
<?php
namespace Automattic\WooCommerce\StoreApi\Utilities;
use Automattic\WooCommerce\StoreApi\Payments\PaymentContext;
use Automattic\WooCommerce\StoreApi\Payments\PaymentResult;
/**
* CheckoutTrait
*
* Shared functionality for checkout route.
*/
trait CheckoutTrait {
/**
* 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;
}
/**
* 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 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;
}
/**
* 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 ) );
$this->order->set_payment_method_title( $this->get_request_payment_method_title( $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();
}
/**
* Gets the chosen payment method title from the request.
*
* @throws RouteException On error.
* @param \WP_REST_Request $request Request object.
* @return string
*/
private function get_request_payment_method_title( \WP_REST_Request $request ) {
$payment_method = $this->get_request_payment_method( $request );
return is_null( $payment_method ) ? '' : $payment_method->get_title();
}
}

View File

@@ -1,63 +0,0 @@
<?php
namespace Automattic\WooCommerce\StoreApi\Utilities;
use Automattic\WooCommerce\StoreApi\Utilities\OrderController;
use Automattic\WooCommerce\StoreApi\Exceptions\RouteException;
/**
* OrderAuthorizationTrait
*
* Shared functionality for getting order authorization.
*/
trait OrderAuthorizationTrait {
/**
* Check if authorized to get the order.
*
* @throws RouteException If the order is not found or the order key is invalid.
*
* @param \WP_REST_Request $request Request object.
* @return boolean|WP_Error
*/
public function is_authorized( \WP_REST_Request $request ) {
$order_id = absint( $request['id'] );
$order_key = sanitize_text_field( wp_unslash( $request->get_param( 'key' ) ) );
$billing_email = sanitize_text_field( wp_unslash( $request->get_param( 'billing_email' ) ) );
try {
// In this context, pay_for_order capability checks that the current user ID matches the customer ID stored
// within the order, or if the order was placed by a guest.
// See https://github.com/woocommerce/woocommerce/blob/abcedbefe02f9e89122771100c42ff588da3e8e0/plugins/woocommerce/includes/wc-user-functions.php#L458.
if ( ! current_user_can( 'pay_for_order', $order_id ) ) {
throw new RouteException( 'woocommerce_rest_invalid_user', __( 'This order belongs to a different customer.', 'woocommerce' ), 403 );
}
if ( get_current_user_id() === 0 ) {
$this->order_controller->validate_order_key( $order_id, $order_key );
$this->validate_billing_email_matches_order( $order_id, $billing_email );
}
} catch ( RouteException $error ) {
return new \WP_Error(
$error->getErrorCode(),
$error->getMessage(),
array( 'status' => $error->getCode() )
);
}
return true;
}
/**
* Validate a given billing email against an existing order.
*
* @throws RouteException Exception if invalid data is detected.
* @param integer $order_id Order ID.
* @param string $billing_email Billing email.
*/
public function validate_billing_email_matches_order( $order_id, $billing_email ) {
$order = wc_get_order( $order_id );
if ( ! $order || ! $billing_email || $order->get_billing_email() !== $billing_email ) {
throw new RouteException( 'woocommerce_rest_invalid_billing_email', __( 'Invalid billing email provided.', 'woocommerce' ), 401 );
}
}
}

View File

@@ -42,9 +42,8 @@ class OrderController {
* Update an order using data from the current cart.
*
* @param \WC_Order $order The order object to update.
* @param boolean $update_totals Whether to update totals or not.
*/
public function update_order_from_cart( \WC_Order $order, $update_totals = true ) {
public function update_order_from_cart( \WC_Order $order ) {
/**
* This filter ensures that local pickup locations are still used for order taxes by forcing the address used to
* calculate tax for an order to match the current address of the customer.
@@ -82,10 +81,8 @@ class OrderController {
);
// Ensure cart is current.
if ( $update_totals ) {
wc()->cart->calculate_shipping();
wc()->cart->calculate_totals();
}
wc()->cart->calculate_shipping();
wc()->cart->calculate_totals();
// Update the current order to match the current cart.
$this->update_line_items_from_cart( $order );
@@ -464,107 +461,6 @@ class OrderController {
}
}
/**
* Validate a given order key against an existing order.
*
* @throws RouteException Exception if invalid data is detected.
* @param integer $order_id Order ID.
* @param string $order_key Order key.
*/
public function validate_order_key( $order_id, $order_key ) {
$order = wc_get_order( $order_id );
if ( ! $order || ! $order_key || $order->get_id() !== $order_id || ! hash_equals( $order->get_order_key(), $order_key ) ) {
throw new RouteException( 'woocommerce_rest_invalid_order', __( 'Invalid order ID or key provided.', 'woocommerce' ), 401 );
}
}
/**
* Get errors for order stock on failed orders.
*
* @throws RouteException Exception if invalid data is detected.
* @param integer $order_id Order ID.
*/
public function get_failed_order_stock_error( $order_id ) {
$order = wc_get_order( $order_id );
// Ensure order items are still stocked if paying for a failed order. Pending orders do not need this check because stock is held.
if ( ! $order->has_status( wc_get_is_pending_statuses() ) ) {
$quantities = array();
foreach ( $order->get_items() as $item_key => $item ) {
if ( $item && is_callable( array( $item, 'get_product' ) ) ) {
$product = $item->get_product();
if ( ! $product ) {
continue;
}
$quantities[ $product->get_stock_managed_by_id() ] = isset( $quantities[ $product->get_stock_managed_by_id() ] ) ? $quantities[ $product->get_stock_managed_by_id() ] + $item->get_quantity() : $item->get_quantity();
}
}
// Stock levels may already have been adjusted for this order (in which case we don't need to worry about checking for low stock).
if ( ! $order->get_data_store()->get_stock_reduced( $order->get_id() ) ) {
foreach ( $order->get_items() as $item_key => $item ) {
if ( $item && is_callable( array( $item, 'get_product' ) ) ) {
$product = $item->get_product();
if ( ! $product ) {
continue;
}
/**
* Filters whether or not the product is in stock for this pay for order.
*
* @param boolean True if in stock.
* @param \WC_Product $product Product.
* @param \WC_Order $order Order.
*
* @since 9.8.0-dev
*/
if ( ! apply_filters( 'woocommerce_pay_order_product_in_stock', $product->is_in_stock(), $product, $order ) ) {
return array(
'code' => 'woocommerce_rest_out_of_stock',
/* translators: %s: product name */
'message' => sprintf( __( 'Sorry, "%s" is no longer in stock so this order cannot be paid for. We apologize for any inconvenience caused.', 'woocommerce' ), $product->get_name() ),
);
}
// We only need to check products managing stock, with a limited stock qty.
if ( ! $product->managing_stock() || $product->backorders_allowed() ) {
continue;
}
// Check stock based on all items in the cart and consider any held stock within pending orders.
$held_stock = wc_get_held_stock_quantity( $product, $order->get_id() );
$required_stock = $quantities[ $product->get_stock_managed_by_id() ];
/**
* Filters whether or not the product has enough stock.
*
* @param boolean True if has enough stock.
* @param \WC_Product $product Product.
* @param \WC_Order $order Order.
*
* @since 9.8.0-dev
*/
if ( ! apply_filters( 'woocommerce_pay_order_product_has_enough_stock', ( $product->get_stock_quantity() >= ( $held_stock + $required_stock ) ), $product, $order ) ) {
/* translators: 1: product name 2: quantity in stock */
return array(
'code' => 'woocommerce_rest_out_of_stock',
/* translators: %s: product name */
'message' => sprintf( __( 'Sorry, we do not have enough "%1$s" in stock to fulfill your order (%2$s available). We apologize for any inconvenience caused.', 'woocommerce' ), $product->get_name(), wc_format_stock_quantity_for_display( $product->get_stock_quantity() - $held_stock, $product ) ),
);
}
}
}
}
}
return null;
}
/**
* Changes default order status to draft for orders created via this API.
*

View File

@@ -1,85 +0,0 @@
<?php
namespace Automattic\WooCommerce\StoreApi\Utilities;
/**
* ProductItemTrait
*
* Shared functionality for formating product item data.
*/
trait ProductItemTrait {
/**
* 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;
}
}

View File

@@ -103,7 +103,7 @@ abstract class AbstractTemplateCompatibility {
* The array format:
* [
* <hook-name> => [
* block_names => [ <block-name>, ... ],
* block_name => <block-name>,
* position => before|after,
* hooked => [
* <function-name> => <priority>,
@@ -113,7 +113,7 @@ abstract class AbstractTemplateCompatibility {
* ]
* Where:
* - hook-name is the name of the hook that will be replaced.
* - block-names is the array block names that hook will be attached to.
* - block-name is the name of the block that will replace the hook.
* - position is the position of the block relative to the hook.
* - hooked is an array of functions hooked to the hook that will be
* replaced. The key is the function name and the value is the

View File

@@ -47,7 +47,7 @@ class ArchiveProductTemplatesCompatibility extends AbstractTemplateCompatibility
return $parsed_block;
}
$this->inner_blocks_walker( $parsed_block );
array_walk( $parsed_block['innerBlocks'], array( $this, 'inner_blocks_walker' ) );
return $parsed_block;
}
@@ -72,20 +72,16 @@ class ArchiveProductTemplatesCompatibility extends AbstractTemplateCompatibility
$block_name = $block['blockName'];
if ( $this->is_null_post_template( $block ) ) {
$block_name = self::LOOP_ITEM_ID;
}
$block_hooks = array_filter(
$this->hook_data,
function( $hook ) use ( $block_name ) {
return in_array( $block_name, $hook['block_names'], true );
return $hook['block_name'] === $block_name;
}
);
// We want to inject hooks to the core/post-template or product template block only when the products exist:
// We want to inject hooks to the core/post-template block only when the products exist:
// https://github.com/woocommerce/woocommerce-blocks/issues/9463.
if ( $this->is_post_or_product_template( $block_name ) && ! empty( $block_content ) ) {
if ( 'core/post-template' === $block_name && ! empty( $block_content ) ) {
$this->restore_default_hooks();
$content = sprintf(
'%1$s%2$s%3$s',
@@ -97,14 +93,24 @@ class ArchiveProductTemplatesCompatibility extends AbstractTemplateCompatibility
return $content;
}
$supported_blocks = array_merge(
[],
...array_map(
function( $hook ) {
return $hook['block_names'];
},
array_values( $this->hook_data )
)
/**
* The core/post-template has two different block names:
* - core/post-template when the wrapper is rendered.
* - core/null when the loop item is rendered.
*/
if (
'core/null' === $block_name &&
isset( $block['attrs']['__woocommerceNamespace'] ) &&
'woocommerce/product-query/product-template' === $block['attrs']['__woocommerceNamespace']
) {
$block_name = self::LOOP_ITEM_ID;
}
$supported_blocks = array_map(
function( $hook ) {
return $hook['block_name'];
},
array_values( $this->hook_data )
);
if ( ! in_array( $block_name, $supported_blocks, true ) ) {
@@ -138,10 +144,6 @@ class ArchiveProductTemplatesCompatibility extends AbstractTemplateCompatibility
return $content;
}
if ( empty( $block_content ) ) {
return $block_content;
}
return sprintf(
'%1$s%2$s%3$s',
$this->get_hooks_buffer( $block_hooks, 'before' ),
@@ -180,60 +182,60 @@ class ArchiveProductTemplatesCompatibility extends AbstractTemplateCompatibility
protected function set_hook_data() {
$this->hook_data = array(
'woocommerce_before_main_content' => array(
'block_names' => array( 'core/query', 'woocommerce/product-collection' ),
'position' => 'before',
'hooked' => array(
'block_name' => 'core/query',
'position' => 'before',
'hooked' => array(
'woocommerce_output_content_wrapper' => 10,
'woocommerce_breadcrumb' => 20,
),
),
'woocommerce_after_main_content' => array(
'block_names' => array( 'core/query', 'woocommerce/product-collection' ),
'position' => 'after',
'hooked' => array(
'block_name' => 'core/query',
'position' => 'after',
'hooked' => array(
'woocommerce_output_content_wrapper_end' => 10,
),
),
'woocommerce_before_shop_loop_item_title' => array(
'block_names' => array( 'core/post-title' ),
'position' => 'before',
'hooked' => array(
'block_name' => 'core/post-title',
'position' => 'before',
'hooked' => array(
'woocommerce_show_product_loop_sale_flash' => 10,
'woocommerce_template_loop_product_thumbnail' => 10,
),
),
'woocommerce_shop_loop_item_title' => array(
'block_names' => array( 'core/post-title' ),
'position' => 'after',
'hooked' => array(
'block_name' => 'core/post-title',
'position' => 'after',
'hooked' => array(
'woocommerce_template_loop_product_title' => 10,
),
),
'woocommerce_after_shop_loop_item_title' => array(
'block_names' => array( 'core/post-title' ),
'position' => 'after',
'hooked' => array(
'block_name' => 'core/post-title',
'position' => 'after',
'hooked' => array(
'woocommerce_template_loop_rating' => 5,
'woocommerce_template_loop_price' => 10,
),
),
'woocommerce_before_shop_loop_item' => array(
'block_names' => array( self::LOOP_ITEM_ID ),
'position' => 'before',
'hooked' => array(
'block_name' => self::LOOP_ITEM_ID,
'position' => 'before',
'hooked' => array(
'woocommerce_template_loop_product_link_open' => 10,
),
),
'woocommerce_after_shop_loop_item' => array(
'block_names' => array( self::LOOP_ITEM_ID ),
'position' => 'after',
'hooked' => array(
'block_name' => self::LOOP_ITEM_ID,
'position' => 'after',
'hooked' => array(
'woocommerce_template_loop_product_link_close' => 5,
'woocommerce_template_loop_add_to_cart' => 10,
),
),
'woocommerce_before_shop_loop' => array(
'block_names' => array( 'core/post-template', 'woocommerce/product-template' ),
'block_name' => 'core/post-template',
'position' => 'before',
'hooked' => array(
'woocommerce_output_all_notices' => 10,
@@ -247,7 +249,7 @@ class ArchiveProductTemplatesCompatibility extends AbstractTemplateCompatibility
),
),
'woocommerce_after_shop_loop' => array(
'block_names' => array( 'core/post-template', 'woocommerce/product-template' ),
'block_name' => 'core/post-template',
'position' => 'after',
'hooked' => array(
'woocommerce_pagination' => 10,
@@ -257,7 +259,7 @@ class ArchiveProductTemplatesCompatibility extends AbstractTemplateCompatibility
),
),
'woocommerce_no_products_found' => array(
'block_names' => array( 'core/query-no-results' ),
'block_name' => 'core/query-no-results',
'position' => 'before',
'hooked' => array(
'wc_no_products_found' => 10,
@@ -267,9 +269,9 @@ class ArchiveProductTemplatesCompatibility extends AbstractTemplateCompatibility
),
),
'woocommerce_archive_description' => array(
'block_names' => array( 'core/term-description' ),
'position' => 'before',
'hooked' => array(
'block_name' => 'core/term-description',
'position' => 'before',
'hooked' => array(
'woocommerce_taxonomy_archive_description' => 10,
'woocommerce_product_archive_description' => 10,
),
@@ -292,7 +294,7 @@ class ArchiveProductTemplatesCompatibility extends AbstractTemplateCompatibility
*/
private function inner_blocks_walker( &$block ) {
if (
$this->is_products_block_with_inherit_query( $block ) || $this->is_product_collection_block_with_inherit_query( $block )
$this->is_products_block_with_inherit_query( $block )
) {
$this->inject_attribute( $block );
$this->remove_default_hooks();
@@ -319,69 +321,6 @@ class ArchiveProductTemplatesCompatibility extends AbstractTemplateCompatibility
}
}
/**
* Check if block is within the product-query namespace
*
* @param array $block Parsed block data.
*/
private function is_block_within_namespace( $block ) {
$attributes = $block['attrs'];
return isset( $attributes['__woocommerceNamespace'] ) && 'woocommerce/product-query/product-template' === $attributes['__woocommerceNamespace'];
}
/**
* Check if block has isInherited attribute asigned
*
* @param array $block Parsed block data.
*/
private function is_block_inherited( $block ) {
$attributes = $block['attrs'];
$outcome = isset( $attributes['isInherited'] ) && 1 === $attributes['isInherited'];
return $outcome;
}
/**
* The core/post-template has two different block names:
* - core/post-template when the wrapper is rendered.
* - core/null when the loop item is rendered.
*
* @param array $block Parsed block data.
*/
private function is_null_post_template( $block ) {
$block_name = $block['blockName'];
return 'core/null' === $block_name && ( $this->is_block_inherited( $block ) || $this->is_block_within_namespace( $block ) );
}
/**
* Check if block is a Post template
*
* @param string $block_name Block name.
*/
private function is_post_template( $block_name ) {
return 'core/post-template' === $block_name;
}
/**
* Check if block is a Product Template
*
* @param string $block_name Block name.
*/
private function is_product_template( $block_name ) {
return 'woocommerce/product-template' === $block_name;
}
/**
* Check if block is eaither a Post template or Product Template
*
* @param string $block_name Block name.
*/
private function is_post_or_product_template( $block_name ) {
return $this->is_post_template( $block_name ) || $this->is_product_template( $block_name );
}
/**
* Check if the block is a Products block that inherits query from template.
@@ -396,18 +335,6 @@ class ArchiveProductTemplatesCompatibility extends AbstractTemplateCompatibility
$block['attrs']['query']['inherit'];
}
/**
* Check if the block is a Product Collection block that inherits query from template.
*
* @param array $block Parsed block data.
*/
private function is_product_collection_block_with_inherit_query( $block ) {
return 'woocommerce/product-collection' === $block['blockName'] &&
isset( $block['attrs']['query']['inherit'] ) &&
$block['attrs']['query']['inherit'];
}
/**
* Recursively inject the custom attribute to all nested blocks.
*

View File

@@ -1,8 +1,6 @@
<?php
namespace Automattic\WooCommerce\Blocks\Templates;
use Automattic\WooCommerce\Blocks\Utils\BlockTemplateMigrationUtils;
/**
* CartTemplate class.
*
@@ -34,11 +32,6 @@ class CartTemplate extends AbstractPageTemplate {
* @return boolean
*/
protected function is_active_template() {
if ( ! BlockTemplateMigrationUtils::has_migrated_page( 'cart' ) ) {
return false;
}
global $post;
$placeholder = $this->get_placeholder_page();
return null !== $placeholder && $post instanceof \WP_Post && $placeholder->post_name === $post->post_name;

View File

@@ -1,8 +1,6 @@
<?php
namespace Automattic\WooCommerce\Blocks\Templates;
use Automattic\WooCommerce\Blocks\Utils\BlockTemplateMigrationUtils;
/**
* CheckoutTemplate class.
*
@@ -43,11 +41,6 @@ class CheckoutTemplate extends AbstractPageTemplate {
* @return boolean
*/
public function is_active_template() {
if ( ! BlockTemplateMigrationUtils::has_migrated_page( 'checkout' ) ) {
return false;
}
global $post;
$placeholder = $this->get_placeholder_page();
return null !== $placeholder && $post instanceof \WP_Post && $placeholder->post_name === $post->post_name;

View File

@@ -60,7 +60,7 @@ class ClassicTemplatesCompatibility {
global $pagenow;
if ( is_shop() || is_product_taxonomy() || 'widgets.php' === $pagenow ) {
$this->asset_data_registry->add( 'hasFilterableProducts', true, true );
$this->asset_data_registry->add( 'has_filterable_products', true, true );
}
}
@@ -75,7 +75,7 @@ class ClassicTemplatesCompatibility {
*/
public function set_php_template_data() {
if ( is_shop() || is_product_taxonomy() ) {
$this->asset_data_registry->add( 'isRenderingPhpTemplate', true, true );
$this->asset_data_registry->add( 'is_rendering_php_template', true, true );
}
}
}

View File

@@ -32,7 +32,7 @@ class SingleProductTemplateCompatibility extends AbstractTemplateCompatibility {
$block_hooks = array_filter(
$this->hook_data,
function( $hook ) use ( $block_name ) {
return in_array( $block_name, $hook['block_names'], true );
return $hook['block_name'] === $block_name;
}
);
@@ -170,46 +170,46 @@ class SingleProductTemplateCompatibility extends AbstractTemplateCompatibility {
protected function set_hook_data() {
$this->hook_data = array(
'woocommerce_before_main_content' => array(
'block_names' => array(),
'position' => 'before',
'hooked' => array(
'block_name' => '',
'position' => 'before',
'hooked' => array(
'woocommerce_output_content_wrapper' => 10,
'woocommerce_breadcrumb' => 20,
),
),
'woocommerce_after_main_content' => array(
'block_names' => array(),
'position' => 'after',
'hooked' => array(
'block_name' => '',
'position' => 'after',
'hooked' => array(
'woocommerce_output_content_wrapper_end' => 10,
),
),
'woocommerce_sidebar' => array(
'block_names' => array(),
'position' => 'after',
'hooked' => array(
'block_name' => '',
'position' => 'after',
'hooked' => array(
'woocommerce_get_sidebar' => 10,
),
),
'woocommerce_before_single_product' => array(
'block_names' => array(),
'position' => 'before',
'hooked' => array(
'block_name' => '',
'position' => 'before',
'hooked' => array(
'woocommerce_output_all_notices' => 10,
),
),
'woocommerce_before_single_product_summary' => array(
'block_names' => array(),
'position' => 'before',
'hooked' => array(
'block_name' => '',
'position' => 'before',
'hooked' => array(
'woocommerce_show_product_sale_flash' => 10,
'woocommerce_show_product_images' => 20,
),
),
'woocommerce_single_product_summary' => array(
'block_names' => array(),
'position' => 'before',
'hooked' => array(
'block_name' => '',
'position' => 'before',
'hooked' => array(
'woocommerce_template_single_title' => 5,
'woocommerce_template_single_rating' => 10,
'woocommerce_template_single_price' => 10,
@@ -220,29 +220,29 @@ class SingleProductTemplateCompatibility extends AbstractTemplateCompatibility {
),
),
'woocommerce_after_single_product' => array(
'block_names' => array(),
'position' => 'after',
'hooked' => array(),
'block_name' => '',
'position' => 'after',
'hooked' => array(),
),
'woocommerce_product_meta_start' => array(
'block_names' => array( 'woocommerce/product-meta' ),
'position' => 'before',
'hooked' => array(),
'block_name' => 'woocommerce/product-meta',
'position' => 'before',
'hooked' => array(),
),
'woocommerce_product_meta_end' => array(
'block_names' => array( 'woocommerce/product-meta' ),
'position' => 'after',
'hooked' => array(),
'block_name' => 'woocommerce/product-meta',
'position' => 'after',
'hooked' => array(),
),
'woocommerce_share' => array(
'block_names' => array( 'woocommerce/product-details' ),
'position' => 'before',
'hooked' => array(),
'block_name' => 'woocommerce/product-details',
'position' => 'before',
'hooked' => array(),
),
'woocommerce_after_single_product_summary' => array(
'block_names' => array( 'woocommerce/product-details' ),
'position' => 'after',
'hooked' => array(
'block_name' => 'woocommerce/product-details',
'position' => 'after',
'hooked' => array(
'woocommerce_output_product_data_tabs' => 10,
// We want to display the upsell products after the last block that belongs to the Single Product.
// 'woocommerce_upsell_display' => 15.
@@ -269,6 +269,7 @@ class SingleProductTemplateCompatibility extends AbstractTemplateCompatibility {
$wrapped_blocks = self::wrap_single_product_template( $template_content );
$template = self::inject_custom_attributes_to_first_and_last_block_single_product_template( $wrapped_blocks );
return self::serialize_blocks( $template );
}
/**

View File

@@ -1,177 +0,0 @@
<?php
namespace Automattic\WooCommerce\Blocks\Utils;
/**
* Utility methods used for migrating pages to block templates.
* {@internal This class and its methods should only be used within the BlockTemplateController.php and is not intended for public use.}
*/
class BlockTemplateMigrationUtils {
/**
* Check if a page has been migrated to a template.
*
* @param string $page_id Page ID.
* @return boolean
*/
public static function has_migrated_page( $page_id ) {
return (bool) get_option( 'has_migrated_' . $page_id, false );
}
/**
* Stores an option to indicate that a template has been migrated.
*
* @param string $page_id Page ID.
* @param string $status Status of the migration.
*/
public static function set_has_migrated_page( $page_id, $status = 'success' ) {
update_option( 'has_migrated_' . $page_id, $status );
}
/**
* Migrates a page to a template if needed.
*
* @param string $template_slug Template slug.
* @param \WP_Post $page Page object.
*/
public static function migrate_page( $template_slug, $page ) {
// Get the block template for this page. If it exists, we won't migrate because the user already has custom content.
$block_template = BlockTemplateUtils::get_block_template( 'woocommerce/woocommerce//' . $template_slug, 'wp_template' );
// If we were unable to get the block template, bail. Try again later.
if ( ! $block_template ) {
return;
}
// If a custom template is present already, no need to migrate.
if ( $block_template->wp_id ) {
return self::set_has_migrated_page( $template_slug, 'custom-template-exists' );
}
// Use the page template if it exists, which we'll use over our default template if found.
$page_template = self::get_page_template( $page );
$default_template = self::get_default_template( $page );
$template_content = $page_template ?: $default_template;
// If at this point we have no content to migrate, bail.
if ( ! $template_content ) {
return self::set_has_migrated_page( $template_slug, 'no-content' );
}
if ( self::create_custom_template( $block_template, $template_content ) ) {
return self::set_has_migrated_page( $template_slug );
}
}
/**
* Get template for a page following the page hierarchy.
*
* @param \WP_Post|null $page Page object.
* @return string
*/
protected static function get_page_template( $page ) {
$templates = array();
if ( $page && $page->ID ) {
$template = get_page_template_slug( $page->ID );
if ( $template && 0 === validate_file( $template ) ) {
$templates[] = $template;
}
$pagename = $page->post_name;
if ( $pagename ) {
$pagename_decoded = urldecode( $pagename );
if ( $pagename_decoded !== $pagename ) {
$templates[] = "page-{$pagename_decoded}";
}
$templates[] = "page-{$pagename}";
}
}
$block_template = false;
foreach ( $templates as $template ) {
$block_template = BlockTemplateUtils::get_block_template( get_stylesheet() . '//' . $template, 'wp_template' );
if ( $block_template && ! empty( $block_template->content ) ) {
break;
}
}
return $block_template ? $block_template->content : '';
}
/**
* Prepare default page template.
*
* @param \WP_Post $page Page object.
* @return string
*/
protected static function get_default_template( $page ) {
if ( ! $page || empty( $page->post_content ) ) {
return '';
}
$default_template_content = '
<!-- wp:group {"layout":{"inherit":true}} -->
<div class="wp-block-group">
<!-- wp:heading {"level":1} -->
<h1 class="wp-block-heading">' . wp_kses_post( $page->post_title ) . '</h1>
<!-- /wp:heading -->
' . wp_kses_post( $page->post_content ) . '
</div>
<!-- /wp:group -->
';
return self::get_block_template_part( 'header' ) . $default_template_content . self::get_block_template_part( 'footer' );
}
/**
* Create a custom template with given content.
*
* @param \WP_Block_Template|null $template Template object.
* @param string $content Template content.
* @return boolean Success.
*/
protected static function create_custom_template( $template, $content ) {
$term = get_term_by( 'slug', $template->theme, 'wp_theme', ARRAY_A );
if ( ! $term ) {
$term = wp_insert_term( $template->theme, 'wp_theme' );
}
$template_id = wp_insert_post(
[
'post_name' => $template->slug,
'post_type' => 'wp_template',
'post_status' => 'publish',
'tax_input' => array(
'wp_theme' => $template->theme,
),
'meta_input' => array(
'origin' => $template->source,
),
'post_content' => $content,
],
true
);
wp_set_post_terms( $template_id, array( $term['term_id'] ), 'wp_theme' );
return $template_id && ! is_wp_error( $template_id );
}
/**
* Returns the requested template part.
*
* @param string $part The part to return.
* @return string
*/
protected static function get_block_template_part( $part ) {
$template_part = BlockTemplateUtils::get_block_template( get_stylesheet() . '//' . $part, 'wp_template_part' );
if ( ! $template_part || empty( $template_part->content ) ) {
return '';
}
return $template_part->content;
}
}

View File

@@ -219,11 +219,6 @@ class BlockTemplateUtils {
$template->is_custom = false; // Templates loaded from the filesystem aren't custom, ones that have been edited and loaded from the DB are.
$template->post_types = array(); // Don't appear in any Edit Post template selector dropdown.
$template->area = 'uncategorized';
// Force the Mini-Cart template part to be in the Mini-Cart template part area.
if ( 'wp_template_part' === $template_type && 'mini-cart' === $template_file->slug ) {
$template->area = 'mini-cart';
}
return $template;
}
@@ -445,25 +440,25 @@ class BlockTemplateUtils {
/**
* Checks to see if they are using a compatible version of WP, or if not they have a compatible version of the Gutenberg plugin installed.
*
* @param string $template_type Optional. Template type: `wp_template` or `wp_template_part`.
* Default `wp_template`.
* @return boolean
*/
public static function supports_block_templates( $template_type = 'wp_template' ) {
if ( 'wp_template_part' === $template_type && ( wc_current_theme_is_fse_theme() || current_theme_supports( 'block-template-parts' ) ) ) {
return true;
} elseif ( 'wp_template' === $template_type && wc_current_theme_is_fse_theme() ) {
return true;
public static function supports_block_templates() {
if (
! wc_current_theme_is_fse_theme() &&
( ! function_exists( 'gutenberg_supports_block_templates' ) || ! gutenberg_supports_block_templates() )
) {
return false;
}
return false;
return true;
}
/**
* Retrieves a single unified template object using its id.
*
* @param string $id Template unique identifier (example: theme_slug//template_slug).
* @param string $template_type Optional. Template type: `wp_template` or 'wp_template_part`.
* Default `wp_template`.
* @param string $template_type Optional. Template type: `'wp_template'` or '`wp_template_part'`.
* Default `'wp_template'`.
*
* @return WP_Block_Template|null Template.
*/

View File

@@ -1,38 +0,0 @@
<?php
namespace Automattic\WooCommerce\Blocks\Utils;
/**
* Utility methods used for the Mini Cart block.
*/
class MiniCartUtils {
/**
* Migrate attributes to color panel component format.
*
* @param array $attributes Any attributes that currently are available from the block.
* @return array Reformatted attributes that are compatible with the color panel component.
*/
public static function migrate_attributes_to_color_panel( $attributes ) {
if ( isset( $attributes['priceColorValue'] ) && ! isset( $attributes['priceColor'] ) ) {
$attributes['priceColor'] = array(
'color' => $attributes['priceColorValue'],
);
unset( $attributes['priceColorValue'] );
}
if ( isset( $attributes['iconColorValue'] ) && ! isset( $attributes['iconColor'] ) ) {
$attributes['iconColor'] = array(
'color' => $attributes['iconColorValue'],
);
unset( $attributes['iconColorValue'] );
}
if ( isset( $attributes['productCountColorValue'] ) && ! isset( $attributes['productCountColor'] ) ) {
$attributes['productCountColor'] = array(
'color' => $attributes['productCountColorValue'],
);
unset( $attributes['productCountColorValue'] );
}
return $attributes;
}
}