Plugin Updates
This commit is contained in:
@@ -11,7 +11,6 @@ defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use ActionScheduler;
|
||||
use Automattic\Jetpack\Connection\Manager;
|
||||
use Automattic\Jetpack\Constants;
|
||||
use Automattic\WooCommerce\Admin\PluginsHelper;
|
||||
use Automattic\WooCommerce\Admin\PluginsInstallLoggers\AsynPluginsInstallLogger;
|
||||
use WC_REST_Data_Controller;
|
||||
@@ -125,30 +124,6 @@ class OnboardingPlugins extends WC_REST_Data_Controller {
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
/*
|
||||
* This is a temporary solution to override /jetpack/v4/connection/data endpoint
|
||||
* registered by Jetpack Connection when Jetpack is not installed.
|
||||
*
|
||||
* For more details, see https://github.com/woocommerce/woocommerce/issues/38979
|
||||
*/
|
||||
if ( Constants::get_constant( 'JETPACK__VERSION' ) === null && wp_is_mobile() ) {
|
||||
register_rest_route(
|
||||
'jetpack/v4',
|
||||
'/connection/data',
|
||||
array(
|
||||
array(
|
||||
'methods' => 'GET',
|
||||
'permission_callback' => '__return_true',
|
||||
'callback' => function() {
|
||||
return new WP_REST_Response( null, 404 );
|
||||
},
|
||||
),
|
||||
),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
add_action( 'woocommerce_plugins_install_error', array( $this, 'log_plugins_install_error' ), 10, 4 );
|
||||
add_action( 'woocommerce_plugins_install_api_error', array( $this, 'log_plugins_install_api_error' ), 10, 2 );
|
||||
}
|
||||
|
||||
@@ -365,7 +365,7 @@ class OnboardingTasks extends \WC_REST_Data_Controller {
|
||||
|
||||
$import = self::import_sample_products_from_csv( $template_path );
|
||||
|
||||
if ( is_wp_error( $import ) || 0 === count( $import['imported'] ) ) {
|
||||
if ( is_wp_error( $import ) || ! is_array( $import['imported'] ) || 0 === count( $import['imported'] ) ) {
|
||||
return new \WP_Error(
|
||||
'woocommerce_rest_product_creation_error',
|
||||
/* translators: %s is template name */
|
||||
@@ -740,6 +740,14 @@ class OnboardingTasks extends \WC_REST_Data_Controller {
|
||||
|
||||
$lists = is_array( $task_list_ids ) && count( $task_list_ids ) > 0 ? TaskLists::get_lists_by_ids( $task_list_ids ) : TaskLists::get_lists();
|
||||
|
||||
// We have no use for hidden lists, it's expensive to compute individual tasks completion.
|
||||
$lists = array_filter(
|
||||
$lists,
|
||||
function( $list ) {
|
||||
return ! $list->is_hidden();
|
||||
}
|
||||
);
|
||||
|
||||
$json = array_map(
|
||||
function( $list ) {
|
||||
return $list->sort_tasks()->get_json();
|
||||
|
||||
@@ -548,7 +548,7 @@ class Plugins extends \WC_REST_Data_Controller {
|
||||
if ( ! class_exists( '\WooCommerce\Square\Handlers\Connection' ) ) {
|
||||
return new \WP_Error( 'woocommerce_rest_helper_connect', __( 'There was an error connecting to Square.', 'woocommerce' ), 500 );
|
||||
}
|
||||
|
||||
$has_cbd_industry = false;
|
||||
if ( 'US' === WC()->countries->get_base_country() ) {
|
||||
$profile = get_option( OnboardingProfile::DATA_OPTION, array() );
|
||||
if ( ! empty( $profile['industry'] ) ) {
|
||||
|
||||
@@ -162,7 +162,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
|
||||
* Returns an array of ids of included categories, based on query arguments from the user.
|
||||
*
|
||||
* @param array $query_args Parameters supplied by the user.
|
||||
* @return string
|
||||
* @return array
|
||||
*/
|
||||
protected function get_included_categories_array( $query_args ) {
|
||||
if ( isset( $query_args['category_includes'] ) && is_array( $query_args['category_includes'] ) && count( $query_args['category_includes'] ) > 0 ) {
|
||||
|
||||
@@ -62,13 +62,20 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
|
||||
);
|
||||
}
|
||||
|
||||
// This method was already available as non-final, marking it as final now would make it backwards-incompatible.
|
||||
// phpcs:disable WooCommerce.Functions.InternalInjectionMethod.MissingFinal
|
||||
|
||||
/**
|
||||
* Set up all the hooks for maintaining and populating table data.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public static function init() {
|
||||
add_action( 'woocommerce_analytics_delete_order_stats', array( __CLASS__, 'sync_on_order_delete' ), 5 );
|
||||
}
|
||||
|
||||
// phpcs:enable WooCommerce.Functions.InternalInjectionMethod.MissingFinal
|
||||
|
||||
/**
|
||||
* Returns an array of ids of included coupons, based on query arguments from the user.
|
||||
*
|
||||
@@ -339,6 +346,11 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
|
||||
*/
|
||||
public static function get_coupon_id( \WC_Order_Item_Coupon $coupon_item ) {
|
||||
// First attempt to get coupon ID from order item data.
|
||||
$coupon_info = $coupon_item->get_meta( 'coupon_info', true );
|
||||
if ( $coupon_info ) {
|
||||
return json_decode( $coupon_info, true )[0];
|
||||
}
|
||||
|
||||
$coupon_data = $coupon_item->get_meta( 'coupon_data', true );
|
||||
|
||||
// Normal checkout orders should have this data.
|
||||
|
||||
@@ -258,6 +258,7 @@ class Segmenter extends ReportsSegmenter {
|
||||
return array();
|
||||
}
|
||||
|
||||
$segments = null;
|
||||
$product_segmenting_table = $wpdb->prefix . 'wc_order_product_lookup';
|
||||
$unique_orders_table = '';
|
||||
$segmenting_where = '';
|
||||
@@ -279,7 +280,7 @@ class Segmenter extends ReportsSegmenter {
|
||||
|
||||
$segments = $this->get_product_related_segments( $type, $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $query_params, $unique_orders_table );
|
||||
} elseif ( 'variation' === $this->query_args['segmentby'] ) {
|
||||
if ( ! isset( $this->query_args['product_includes'] ) || count( $this->query_args['product_includes'] ) !== 1 ) {
|
||||
if ( ! isset( $this->query_args['product_includes'] ) || ! is_array( $this->query_args['product_includes'] ) || count( $this->query_args['product_includes'] ) !== 1 ) {
|
||||
throw new ParameterException( 'wc_admin_reports_invalid_segmenting_variation', __( 'product_includes parameter need to specify exactly one product when segmenting by variation.', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
|
||||
@@ -374,7 +374,7 @@ class Segmenter extends ReportsSegmenter {
|
||||
|
||||
$segments = $this->get_product_related_segments( $type, $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $query_params, $unique_orders_table );
|
||||
} elseif ( 'variation' === $this->query_args['segmentby'] ) {
|
||||
if ( ! isset( $this->query_args['product_includes'] ) || count( $this->query_args['product_includes'] ) !== 1 ) {
|
||||
if ( ! isset( $this->query_args['product_includes'] ) || ! is_array( $this->query_args['product_includes'] ) || count( $this->query_args['product_includes'] ) !== 1 ) {
|
||||
throw new ParameterException( 'wc_admin_reports_invalid_segmenting_variation', __( 'product_includes parameter need to specify exactly one product when segmenting by variation.', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
|
||||
@@ -165,7 +165,7 @@ class Segmenter extends ReportsSegmenter {
|
||||
|
||||
$segments = $this->get_product_related_segments( $type, $segmenting_selections, $segmenting_from, $segmenting_where, $segmenting_groupby, $segmenting_dimension_name, $table_name, $query_params, $unique_orders_table );
|
||||
} elseif ( 'variation' === $this->query_args['segmentby'] ) {
|
||||
if ( ! isset( $this->query_args['product_includes'] ) || count( $this->query_args['product_includes'] ) !== 1 ) {
|
||||
if ( ! isset( $this->query_args['product_includes'] ) || ! is_array( count( $this->query_args['product_includes'] ) ) || count( $this->query_args['product_includes'] ) !== 1 ) {
|
||||
throw new ParameterException( 'wc_admin_reports_invalid_segmenting_variation', __( 'product_includes parameter need to specify exactly one product when segmenting by variation.', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
|
||||
@@ -368,6 +368,7 @@ class Segmenter {
|
||||
|
||||
if (
|
||||
isset( $this->query_args['product_includes'] ) &&
|
||||
is_array( $this->query_args['product_includes'] ) &&
|
||||
count( $this->query_args['product_includes'] ) === 1
|
||||
) {
|
||||
$args['parent'] = $this->query_args['product_includes'][0];
|
||||
|
||||
@@ -175,7 +175,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
|
||||
$this->add_sql_query_params( $query_args );
|
||||
$params = $this->get_limit_params( $query_args );
|
||||
|
||||
if ( isset( $query_args['taxes'] ) && ! empty( $query_args['taxes'] ) ) {
|
||||
if ( isset( $query_args['taxes'] ) && is_array( $query_args['taxes'] ) && ! empty( $query_args['taxes'] ) ) {
|
||||
$total_results = count( $query_args['taxes'] );
|
||||
$total_pages = (int) ceil( $total_results / $params['per_page'] );
|
||||
|
||||
|
||||
@@ -77,6 +77,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
|
||||
$order_tax_lookup_table = self::get_db_table_name();
|
||||
|
||||
if ( isset( $query_args['taxes'] ) && ! empty( $query_args['taxes'] ) ) {
|
||||
$query_args['taxes'] = (array) $query_args['taxes'];
|
||||
$tax_id_placeholders = implode( ',', array_fill( 0, count( $query_args['taxes'] ), '%d' ) );
|
||||
/* phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared */
|
||||
$taxes_where_clause .= $wpdb->prepare( " AND {$order_tax_lookup_table}.tax_rate_id IN ({$tax_id_placeholders})", $query_args['taxes'] );
|
||||
@@ -115,6 +116,7 @@ class DataStore extends ReportsDataStore implements DataStoreInterface {
|
||||
FROM {$wpdb->prefix}woocommerce_tax_rates
|
||||
";
|
||||
if ( ! empty( $args['include'] ) ) {
|
||||
$args['include'] = (array) $args['include'];
|
||||
/* phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared */
|
||||
$tax_placeholders = implode( ',', array_fill( 0, count( $args['include'] ), '%d' ) );
|
||||
$query .= $wpdb->prepare( " WHERE tax_rate_id IN ({$tax_placeholders})", $args['include'] );
|
||||
|
||||
@@ -586,6 +586,7 @@ class TimeInterval {
|
||||
}
|
||||
|
||||
if (
|
||||
! is_array( $value ) ||
|
||||
2 !== count( $value ) ||
|
||||
! is_numeric( $value[0] ) ||
|
||||
! is_numeric( $value[1] )
|
||||
@@ -618,6 +619,7 @@ class TimeInterval {
|
||||
}
|
||||
|
||||
if (
|
||||
! is_array( $value ) ||
|
||||
2 !== count( $value ) ||
|
||||
! rest_parse_date( $value[0] ) ||
|
||||
! rest_parse_date( $value[1] )
|
||||
|
||||
@@ -140,7 +140,7 @@ class Segmenter extends ReportsSegmenter {
|
||||
* @param array $query_params SQL query parameter array.
|
||||
* @param string $table_name Name of main SQL table for the data store (used as basis for JOINS).
|
||||
*
|
||||
* @return array
|
||||
* @return array|null
|
||||
* @throws \Automattic\WooCommerce\Admin\API\Reports\ParameterException In case of segmenting by variations, when no parent product is specified.
|
||||
*/
|
||||
protected function get_segments( $type, $query_params, $table_name ) {
|
||||
@@ -148,7 +148,7 @@ class Segmenter extends ReportsSegmenter {
|
||||
if ( ! isset( $this->query_args['segmentby'] ) || '' === $this->query_args['segmentby'] ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$segments = null;
|
||||
$product_segmenting_table = $wpdb->prefix . 'wc_order_product_lookup';
|
||||
$unique_orders_table = 'uniq_orders';
|
||||
$segmenting_where = '';
|
||||
|
||||
@@ -192,7 +192,7 @@ class Themes extends \WC_REST_Data_Controller {
|
||||
* @return array
|
||||
*/
|
||||
public function get_collection_params() {
|
||||
$params['context'] = $this->get_context_param( array( 'default' => 'view' ) );
|
||||
$params = array( 'context' => $this->get_context_param( array( 'default' => 'view' ) ) );
|
||||
$params['pluginzip'] = array(
|
||||
'description' => __( 'A zip file of the theme to be uploaded.', 'woocommerce' ),
|
||||
'type' => 'file',
|
||||
|
||||
@@ -133,12 +133,12 @@ abstract class DataSourcePoller {
|
||||
$this->merge_specs( $specs_from_data_source, $specs, $url );
|
||||
}
|
||||
|
||||
$specs_group = get_transient( $this->args['transient_name'] ) ?? array();
|
||||
$specs_group = get_transient( $this->args['transient_name'] );
|
||||
$specs_group = is_array( $specs_group ) ? $specs_group : array();
|
||||
$locale = get_user_locale();
|
||||
$specs_group[ $locale ] = $specs;
|
||||
// Persist the specs as a transient.
|
||||
set_transient(
|
||||
$this->args['transient_name'],
|
||||
$this->set_specs_transient(
|
||||
$specs_group,
|
||||
$this->args['transient_expiry']
|
||||
);
|
||||
@@ -154,6 +154,20 @@ abstract class DataSourcePoller {
|
||||
return delete_transient( $this->args['transient_name'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the specs transient.
|
||||
*
|
||||
* @param array $specs The specs to set in the transient.
|
||||
* @param int $expiration The expiration time for the transient.
|
||||
*/
|
||||
public function set_specs_transient( $specs, $expiration = 0 ) {
|
||||
set_transient(
|
||||
$this->args['transient_name'],
|
||||
$specs,
|
||||
$expiration,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a single data source and return the read specs
|
||||
*
|
||||
@@ -183,7 +197,7 @@ abstract class DataSourcePoller {
|
||||
// phpcs:ignore
|
||||
$logger->error( print_r( $response, true ), $logger_context );
|
||||
|
||||
return [];
|
||||
return array();
|
||||
}
|
||||
|
||||
$body = $response['body'];
|
||||
@@ -195,7 +209,7 @@ abstract class DataSourcePoller {
|
||||
$logger_context
|
||||
);
|
||||
|
||||
return [];
|
||||
return array();
|
||||
}
|
||||
|
||||
if ( ! is_array( $specs ) ) {
|
||||
@@ -204,7 +218,7 @@ abstract class DataSourcePoller {
|
||||
$logger_context
|
||||
);
|
||||
|
||||
return [];
|
||||
return array();
|
||||
}
|
||||
|
||||
return $specs;
|
||||
|
||||
@@ -2,13 +2,15 @@
|
||||
|
||||
namespace Automattic\WooCommerce\Admin\Features\MarketingRecommendations;
|
||||
|
||||
use Automattic\WooCommerce\Admin\RemoteSpecs\RemoteSpecsEngine;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Marketing Recommendations engine.
|
||||
* This goes through the specs and gets marketing recommendations.
|
||||
*/
|
||||
class Init {
|
||||
class Init extends RemoteSpecsEngine {
|
||||
/**
|
||||
* Slug of the category specifying marketing extensions on the Woo.com store.
|
||||
*
|
||||
@@ -54,6 +56,29 @@ class Init {
|
||||
return $specs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process specs.
|
||||
*
|
||||
* @param array|null $specs Marketing recommendations spec array.
|
||||
* @return array
|
||||
*/
|
||||
protected static function evaluate_specs( array $specs = null ) {
|
||||
$suggestions = array();
|
||||
$errors = array();
|
||||
|
||||
foreach ( $specs as $spec ) {
|
||||
try {
|
||||
$suggestions[] = self::object_to_array( $spec );
|
||||
} catch ( \Throwable $e ) {
|
||||
$errors[] = $e;
|
||||
}
|
||||
}
|
||||
|
||||
return array(
|
||||
'suggestions' => $suggestions,
|
||||
'errors' => $errors,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load recommended plugins from Woo.com
|
||||
@@ -61,14 +86,30 @@ class Init {
|
||||
* @return array
|
||||
*/
|
||||
public static function get_recommended_plugins(): array {
|
||||
$specs = self::get_specs();
|
||||
$result = array();
|
||||
$specs = self::get_specs();
|
||||
$results = self::evaluate_specs( $specs );
|
||||
|
||||
foreach ( $specs as $spec ) {
|
||||
$result[] = self::object_to_array( $spec );
|
||||
$specs_to_return = $results['suggestions'];
|
||||
$specs_to_save = null;
|
||||
|
||||
if ( empty( $specs_to_return ) ) {
|
||||
// When suggestions is empty, replace it with defaults and save for 3 hours.
|
||||
$specs_to_save = DefaultMarketingRecommendations::get_all();
|
||||
$specs_to_return = self::evaluate_specs( $specs_to_save )['suggestions'];
|
||||
} elseif ( count( $results['errors'] ) > 0 ) {
|
||||
// When suggestions is not empty but has errors, save it for 3 hours.
|
||||
$specs_to_save = $specs;
|
||||
}
|
||||
|
||||
return $result;
|
||||
if ( $specs_to_save ) {
|
||||
MarketingRecommendationsDataSourcePoller::get_instance()->set_specs_transient( $specs_to_save, 3 * HOUR_IN_SECONDS );
|
||||
}
|
||||
$errors = $results['errors'];
|
||||
if ( ! empty( $errors ) ) {
|
||||
self::log_errors( $errors );
|
||||
}
|
||||
|
||||
return $specs_to_return;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -139,16 +180,22 @@ class Init {
|
||||
* This is used to convert the specs to an array so that they can be returned by the API.
|
||||
*
|
||||
* @param mixed $obj Object to convert.
|
||||
* @param array &$visited Reference to an array keeping track of all seen objects to detect circular references.
|
||||
* @return array
|
||||
*/
|
||||
protected static function object_to_array( $obj ) {
|
||||
public static function object_to_array( $obj, &$visited = array() ) {
|
||||
if ( is_object( $obj ) ) {
|
||||
$obj = (array) $obj;
|
||||
if ( in_array( $obj, $visited, true ) ) {
|
||||
// Circular reference detected.
|
||||
return null;
|
||||
}
|
||||
$visited[] = $obj;
|
||||
$obj = (array) $obj;
|
||||
}
|
||||
if ( is_array( $obj ) ) {
|
||||
$new = array();
|
||||
foreach ( $obj as $key => $val ) {
|
||||
$new[ $key ] = self::object_to_array( $val );
|
||||
$new[ $key ] = self::object_to_array( $val, $visited );
|
||||
}
|
||||
} else {
|
||||
$new = $obj;
|
||||
|
||||
@@ -168,7 +168,7 @@ class TaskList {
|
||||
* @return bool
|
||||
*/
|
||||
public function is_visible() {
|
||||
if ( ! $this->visible || ! count( $this->get_viewable_tasks() ) > 0 ) {
|
||||
if ( ! $this->visible || $this->is_hidden() || ! count( $this->get_viewable_tasks() ) > 0 ) {
|
||||
return false;
|
||||
}
|
||||
return ! $this->is_hidden();
|
||||
@@ -199,6 +199,7 @@ class TaskList {
|
||||
'action' => 'remove_card',
|
||||
'completed_task_count' => $completed_count,
|
||||
'incomplete_task_count' => count( $viewable_tasks ) - $completed_count,
|
||||
'tasklist_id' => $this->id,
|
||||
)
|
||||
);
|
||||
|
||||
@@ -324,11 +325,17 @@ class TaskList {
|
||||
* Track list completion of viewable tasks.
|
||||
*/
|
||||
public function possibly_track_completion() {
|
||||
if ( ! $this->is_complete() ) {
|
||||
if ( $this->has_previously_completed() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $this->has_previously_completed() ) {
|
||||
// If it's hidden, completion is tracked via hide method.
|
||||
if ( $this->is_hidden() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Expensive check, do it last.
|
||||
if ( ! $this->is_complete() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -336,7 +343,12 @@ class TaskList {
|
||||
$completed_lists[] = $this->get_list_id();
|
||||
update_option( self::COMPLETED_OPTION, $completed_lists );
|
||||
$this->maybe_set_default_layout( $completed_lists );
|
||||
$this->record_tracks_event( 'tasks_completed' );
|
||||
$this->record_tracks_event(
|
||||
'tasks_completed',
|
||||
array(
|
||||
'tasklist_id' => $this->id,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -407,7 +407,7 @@ class TaskLists {
|
||||
public static function setup_tasks_remaining() {
|
||||
$setup_list = self::get_list( 'setup' );
|
||||
|
||||
if ( ! $setup_list || $setup_list->is_hidden() || $setup_list->is_complete() ) {
|
||||
if ( ! $setup_list || $setup_list->is_hidden() || $setup_list->has_previously_completed() || $setup_list->is_complete() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -30,4 +30,31 @@ class EvaluateSuggestion {
|
||||
|
||||
return $suggestion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates the specs and returns the visible suggestions.
|
||||
*
|
||||
* @param array $specs payment suggestion spec array.
|
||||
* @return array The visible suggestions and errors.
|
||||
*/
|
||||
public static function evaluate_specs( $specs ) {
|
||||
$suggestions = array();
|
||||
$errors = array();
|
||||
|
||||
foreach ( $specs as $spec ) {
|
||||
try {
|
||||
$suggestion = self::evaluate( $spec );
|
||||
if ( ! property_exists( $suggestion, 'is_visible' ) || $suggestion->is_visible ) {
|
||||
$suggestions[] = $suggestion;
|
||||
}
|
||||
} catch ( \Throwable $e ) {
|
||||
$errors[] = $e;
|
||||
}
|
||||
}
|
||||
|
||||
return array(
|
||||
'suggestions' => $suggestions,
|
||||
'errors' => $errors,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,12 +9,13 @@ defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Features\PaymentGatewaySuggestions\DefaultPaymentGateways;
|
||||
use Automattic\WooCommerce\Admin\Features\PaymentGatewaySuggestions\PaymentGatewaysController;
|
||||
use Automattic\WooCommerce\Admin\RemoteSpecs\RemoteSpecsEngine;
|
||||
|
||||
/**
|
||||
* Remote Payment Methods engine.
|
||||
* This goes through the specs and gets eligible payment gateways.
|
||||
*/
|
||||
class Init {
|
||||
class Init extends RemoteSpecsEngine {
|
||||
/**
|
||||
* Option name for dismissed payment method suggestions.
|
||||
*/
|
||||
@@ -35,30 +36,31 @@ class Init {
|
||||
* @return array
|
||||
*/
|
||||
public static function get_suggestions( array $specs = null ) {
|
||||
$suggestions = array();
|
||||
if ( null === $specs ) {
|
||||
$specs = self::get_specs();
|
||||
$locale = get_user_locale();
|
||||
|
||||
$specs = is_array( $specs ) ? $specs : self::get_specs();
|
||||
$results = EvaluateSuggestion::evaluate_specs( $specs );
|
||||
$specs_to_return = $results['suggestions'];
|
||||
$specs_to_save = null;
|
||||
|
||||
if ( empty( $specs_to_return ) ) {
|
||||
// When suggestions is empty, replace it with defaults and save for 3 hours.
|
||||
$specs_to_save = DefaultPaymentGateways::get_all();
|
||||
$specs_to_return = EvaluateSuggestion::evaluate_specs( $specs_to_save )['suggestions'];
|
||||
} elseif ( count( $results['errors'] ) > 0 ) {
|
||||
// When suggestions is not empty but has errors, save it for 3 hours.
|
||||
$specs_to_save = $specs;
|
||||
}
|
||||
|
||||
foreach ( $specs as $spec ) {
|
||||
try {
|
||||
$suggestion = EvaluateSuggestion::evaluate( $spec );
|
||||
$suggestions[] = $suggestion;
|
||||
// phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
|
||||
} catch ( \Throwable $e ) {
|
||||
// Ignore errors.
|
||||
}
|
||||
if ( count( $results['errors'] ) > 0 ) {
|
||||
self::log_errors( $results['errors'] );
|
||||
}
|
||||
|
||||
return array_values(
|
||||
array_filter(
|
||||
$suggestions,
|
||||
function( $suggestion ) {
|
||||
return ! property_exists( $suggestion, 'is_visible' ) || $suggestion->is_visible;
|
||||
}
|
||||
)
|
||||
);
|
||||
if ( $specs_to_save ) {
|
||||
PaymentGatewaySuggestionsDataSourcePoller::get_instance()->set_specs_transient( array( $locale => $specs_to_save ), 3 * HOUR_IN_SECONDS );
|
||||
}
|
||||
|
||||
return $specs_to_return;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -31,11 +31,14 @@ class BlockRegistry {
|
||||
'woocommerce/product-pricing-field',
|
||||
'woocommerce/product-section',
|
||||
'woocommerce/product-section-description',
|
||||
'woocommerce/product-subsection',
|
||||
'woocommerce/product-subsection-description',
|
||||
'woocommerce/product-details-section-description',
|
||||
'woocommerce/product-tab',
|
||||
'woocommerce/product-toggle-field',
|
||||
'woocommerce/product-taxonomy-field',
|
||||
'woocommerce/product-text-field',
|
||||
'woocommerce/product-text-area-field',
|
||||
'woocommerce/product-number-field',
|
||||
'woocommerce/product-linked-list-field',
|
||||
);
|
||||
@@ -60,7 +63,6 @@ class BlockRegistry {
|
||||
'woocommerce/product-tag-field',
|
||||
'woocommerce/product-inventory-quantity-field',
|
||||
'woocommerce/product-variation-items-field',
|
||||
'woocommerce/product-variations-fields',
|
||||
'woocommerce/product-password-field',
|
||||
'woocommerce/product-list-field',
|
||||
'woocommerce/product-has-variations-notice',
|
||||
|
||||
@@ -34,6 +34,14 @@ interface ProductFormTemplateInterface extends BlockTemplateInterface {
|
||||
*/
|
||||
public function get_section_by_id( string $section_id ): ?SectionInterface;
|
||||
|
||||
/**
|
||||
* Gets subsection block by id.
|
||||
*
|
||||
* @param string $subsection_id subsection id.
|
||||
* @return SubsectionInterface|null
|
||||
*/
|
||||
public function get_subsection_by_id( string $subsection_id ): ?SubsectionInterface;
|
||||
|
||||
/**
|
||||
* Gets Block by id.
|
||||
*
|
||||
|
||||
@@ -15,9 +15,9 @@ interface SectionInterface extends BlockContainerInterface {
|
||||
* Adds a new sub-section to the section.
|
||||
*
|
||||
* @param array $block_config block config.
|
||||
* @return SectionInterface new block section.
|
||||
* @return SubsectionInterface new block sub-section.
|
||||
*/
|
||||
public function add_section( array $block_config ): SectionInterface;
|
||||
public function add_subsection( array $block_config ): SubsectionInterface;
|
||||
|
||||
/**
|
||||
* Adds a new block to the section.
|
||||
@@ -25,4 +25,13 @@ interface SectionInterface extends BlockContainerInterface {
|
||||
* @param array $block_config block config.
|
||||
*/
|
||||
public function add_block( array $block_config ): BlockInterface;
|
||||
|
||||
/**
|
||||
* Adds a new sub-section to the section.
|
||||
*
|
||||
* @deprecated 8.6.0
|
||||
*
|
||||
* @param array $block_config The block data.
|
||||
*/
|
||||
public function add_section( array $block_config ): SubsectionInterface;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Admin\Features\ProductBlockEditor\ProductTemplates;
|
||||
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\BlockContainerInterface;
|
||||
use Automattic\WooCommerce\Admin\BlockTemplates\BlockInterface;
|
||||
|
||||
/**
|
||||
* Interface for subsection containers, which contain sub-sections and blocks.
|
||||
*/
|
||||
interface SubsectionInterface extends BlockContainerInterface {
|
||||
/**
|
||||
* Adds a new block to the sub-section.
|
||||
*
|
||||
* @param array $block_config block config.
|
||||
*/
|
||||
public function add_block( array $block_config ): BlockInterface;
|
||||
}
|
||||
@@ -2,12 +2,13 @@
|
||||
|
||||
namespace Automattic\WooCommerce\Admin\Features\ShippingPartnerSuggestions;
|
||||
|
||||
use Automattic\WooCommerce\Admin\RemoteInboxNotifications\RuleEvaluator;
|
||||
use Automattic\WooCommerce\Admin\Features\PaymentGatewaySuggestions\EvaluateSuggestion;
|
||||
use Automattic\WooCommerce\Admin\RemoteSpecs\RemoteSpecsEngine;
|
||||
|
||||
/**
|
||||
* Class ShippingPartnerSuggestions
|
||||
*/
|
||||
class ShippingPartnerSuggestions {
|
||||
class ShippingPartnerSuggestions extends RemoteSpecsEngine {
|
||||
|
||||
/**
|
||||
* Go through the specs and run them.
|
||||
@@ -15,31 +16,34 @@ class ShippingPartnerSuggestions {
|
||||
* @param array|null $specs shipping partner suggestion spec array.
|
||||
* @return array
|
||||
*/
|
||||
public static function get_suggestions( $specs = null ) {
|
||||
$suggestions = array();
|
||||
if ( null === $specs ) {
|
||||
$specs = self::get_specs_from_datasource();
|
||||
public static function get_suggestions( array $specs = null ) {
|
||||
$locale = get_user_locale();
|
||||
|
||||
$specs = is_array( $specs ) ? $specs : self::get_specs();
|
||||
$results = EvaluateSuggestion::evaluate_specs( $specs );
|
||||
$specs_to_return = $results['suggestions'];
|
||||
$specs_to_save = null;
|
||||
|
||||
if ( empty( $specs_to_return ) ) {
|
||||
// When suggestions is empty, replace it with defaults and save for 3 hours.
|
||||
$specs_to_save = DefaultShippingPartners::get_all();
|
||||
$specs_to_return = EvaluateSuggestion::evaluate_specs( $specs_to_save )['suggestions'];
|
||||
} elseif ( count( $results['errors'] ) > 0 ) {
|
||||
// When suggestions is not empty but has errors, save it for 3 hours.
|
||||
$specs_to_save = $specs;
|
||||
}
|
||||
|
||||
$rule_evaluator = new RuleEvaluator();
|
||||
foreach ( $specs as &$spec ) {
|
||||
$spec = is_array( $spec ) ? (object) $spec : $spec;
|
||||
if ( isset( $spec->is_visible ) ) {
|
||||
$is_visible = $rule_evaluator->evaluate( $spec->is_visible );
|
||||
if ( $is_visible ) {
|
||||
$spec->is_visible = true;
|
||||
$suggestions[] = $spec;
|
||||
}
|
||||
}
|
||||
if ( $specs_to_save ) {
|
||||
ShippingPartnerSuggestionsDataSourcePoller::get_instance()->set_specs_transient( array( $locale => $specs_to_save ), 3 * HOUR_IN_SECONDS );
|
||||
}
|
||||
|
||||
return $suggestions;
|
||||
return $specs_to_return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get specs or fetch remotely if they don't exist.
|
||||
*/
|
||||
public static function get_specs_from_datasource() {
|
||||
public static function get_specs() {
|
||||
if ( 'no' === get_option( 'woocommerce_show_marketplace_suggestions', 'yes' ) ) {
|
||||
/**
|
||||
* It can be used to modify shipping partner suggestions spec.
|
||||
|
||||
@@ -167,6 +167,7 @@ class PageController {
|
||||
return apply_filters( 'woocommerce_navigation_get_breadcrumbs', array( '' ), $current_page );
|
||||
}
|
||||
|
||||
$current_page['title'] = (array) $current_page['title'];
|
||||
if ( 1 === count( $current_page['title'] ) ) {
|
||||
$breadcrumbs = $current_page['title'];
|
||||
} else {
|
||||
|
||||
@@ -25,7 +25,11 @@ class BaseLocationCountryRuleProcessor implements RuleProcessorInterface {
|
||||
*/
|
||||
public function process( $rule, $stored_state ) {
|
||||
$base_location = wc_get_base_location();
|
||||
if ( ! $base_location ) {
|
||||
if (
|
||||
! is_array( $base_location ) ||
|
||||
! array_key_exists( 'country', $base_location ) ||
|
||||
! array_key_exists( 'state', $base_location )
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ class BaseLocationStateRuleProcessor implements RuleProcessorInterface {
|
||||
*/
|
||||
public function process( $rule, $stored_state ) {
|
||||
$base_location = wc_get_base_location();
|
||||
if ( ! $base_location ) {
|
||||
if ( ! is_array( $base_location ) || ! array_key_exists( 'state', $base_location ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ class OnboardingProfileRuleProcessor implements RuleProcessorInterface {
|
||||
public function process( $rule, $stored_state ) {
|
||||
$onboarding_profile = get_option( 'woocommerce_onboarding_profile' );
|
||||
|
||||
if ( empty( $onboarding_profile ) ) {
|
||||
if ( empty( $onboarding_profile ) || ! is_array( $onboarding_profile ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,13 +20,14 @@ class OptionRuleProcessor implements RuleProcessorInterface {
|
||||
* @return bool The result of the operation.
|
||||
*/
|
||||
public function process( $rule, $stored_state ) {
|
||||
$is_contains = $rule->operation && strpos( $rule->operation, 'contains' ) !== false;
|
||||
$default_value = $is_contains ? array() : false;
|
||||
$default = isset( $rule->default ) ? $rule->default : $default_value;
|
||||
$option_value = $this->get_option_value( $rule, $default, $is_contains );
|
||||
$is_contains = $rule->operation && strpos( $rule->operation, 'contains' ) !== false;
|
||||
$default_value = $is_contains ? array() : false;
|
||||
$is_default_set = property_exists( $rule, 'default' );
|
||||
$default = $is_default_set ? $rule->default : $default_value;
|
||||
$option_value = $this->get_option_value( $rule, $default, $is_contains );
|
||||
|
||||
if ( isset( $rule->transformers ) && is_array( $rule->transformers ) ) {
|
||||
$option_value = TransformerService::apply( $option_value, $rule->transformers, $default );
|
||||
$option_value = TransformerService::apply( $option_value, $rule->transformers, $is_default_set, $default );
|
||||
}
|
||||
|
||||
return ComparisonOperation::compare(
|
||||
|
||||
@@ -52,7 +52,7 @@ class PluginVersionRuleProcessor implements RuleProcessorInterface {
|
||||
|
||||
$plugin_data = $this->plugins_provider->get_plugin_data( $rule->plugin );
|
||||
|
||||
if ( ! $plugin_data ) {
|
||||
if ( ! is_array( $plugin_data ) || ! array_key_exists( 'Version', $plugin_data ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,13 +41,21 @@ class PluginsActivatedRuleProcessor implements RuleProcessorInterface {
|
||||
* @return bool Whether the rule passes or not.
|
||||
*/
|
||||
public function process( $rule, $stored_state ) {
|
||||
if ( 0 === count( $rule->plugins ) ) {
|
||||
if ( ! is_countable( $rule->plugins ) || 0 === count( $rule->plugins ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$active_plugin_slugs = $this->plugins_provider->get_active_plugin_slugs();
|
||||
|
||||
foreach ( $rule->plugins as $plugin_slug ) {
|
||||
if ( ! is_string( $plugin_slug ) ) {
|
||||
$logger = wc_get_logger();
|
||||
$logger->warning(
|
||||
__( 'Invalid plugin slug provided in the plugins activated rule.', 'woocommerce' )
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! in_array( $plugin_slug, $active_plugin_slugs, true ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -56,6 +56,12 @@ class PublishAfterTimeRuleProcessor implements RuleProcessorInterface {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
new \DateTime( $rule->publish_after );
|
||||
} catch ( \Throwable $e ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +56,12 @@ class PublishBeforeTimeRuleProcessor implements RuleProcessorInterface {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
new \DateTime( $rule->publish_before );
|
||||
} catch ( \Throwable $e ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,13 +10,14 @@ defined( 'ABSPATH' ) || exit;
|
||||
use Automattic\WooCommerce\Admin\PluginsProvider\PluginsProvider;
|
||||
use Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingProfile;
|
||||
use Automattic\WooCommerce\Admin\Notes\Note;
|
||||
use Automattic\WooCommerce\Admin\RemoteSpecs\RemoteSpecsEngine;
|
||||
|
||||
/**
|
||||
* Remote Inbox Notifications engine.
|
||||
* This goes through the specs and runs (creates admin notes) for those
|
||||
* specs that are able to be triggered.
|
||||
*/
|
||||
class RemoteInboxNotificationsEngine {
|
||||
class RemoteInboxNotificationsEngine extends RemoteSpecsEngine {
|
||||
const STORED_STATE_OPTION_NAME = 'wc_remote_inbox_notifications_stored_state';
|
||||
const WCA_UPDATED_OPTION_NAME = 'wc_remote_inbox_notifications_wca_updated';
|
||||
|
||||
@@ -112,14 +113,22 @@ class RemoteInboxNotificationsEngine {
|
||||
public static function run() {
|
||||
$specs = DataSourcePoller::get_instance()->get_specs_from_data_sources();
|
||||
|
||||
if ( $specs === false || count( $specs ) === 0 ) {
|
||||
if ( false === $specs || ! is_countable( $specs ) || count( $specs ) === 0 ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$stored_state = self::get_stored_state();
|
||||
$errors = array();
|
||||
|
||||
foreach ( $specs as $spec ) {
|
||||
SpecRunner::run_spec( $spec, $stored_state );
|
||||
$error = SpecRunner::run_spec( $spec, $stored_state );
|
||||
if ( isset( $error ) ) {
|
||||
$errors[] = $error;
|
||||
}
|
||||
}
|
||||
|
||||
if ( count( $errors ) > 0 ) {
|
||||
self::log_errors( $errors );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ class SpecRunner {
|
||||
|
||||
// Create or update the note.
|
||||
$existing_note_ids = $data_store->get_notes_with_name( $spec->slug );
|
||||
if ( count( $existing_note_ids ) === 0 ) {
|
||||
if ( ! is_countable( $existing_note_ids ) || count( $existing_note_ids ) === 0 ) {
|
||||
$note = new Note();
|
||||
$note->set_status( Note::E_WC_ADMIN_NOTE_PENDING );
|
||||
} else {
|
||||
@@ -45,7 +45,7 @@ class SpecRunner {
|
||||
new RuleEvaluator()
|
||||
);
|
||||
} catch ( \Throwable $e ) {
|
||||
return;
|
||||
return $e;
|
||||
}
|
||||
|
||||
// If the status is changing, update the created date to now.
|
||||
|
||||
@@ -23,17 +23,22 @@ class TotalPaymentsVolumeProcessor implements RuleProcessorInterface {
|
||||
* @return bool The result of the operation.
|
||||
*/
|
||||
public function process( $rule, $stored_state ) {
|
||||
$dates = TimeInterval::get_timeframe_dates( $rule->timeframe );
|
||||
$reports_revenue = new RevenueQuery(
|
||||
$dates = TimeInterval::get_timeframe_dates( $rule->timeframe );
|
||||
$reports_revenue = $this->get_reports_query(
|
||||
array(
|
||||
'before' => $dates['end'],
|
||||
'after' => $dates['start'],
|
||||
'before' => $dates['end'],
|
||||
'after' => $dates['start'],
|
||||
'interval' => 'year',
|
||||
'fields' => array( 'total_sales' ),
|
||||
'fields' => array( 'total_sales' ),
|
||||
)
|
||||
);
|
||||
$report_data = $reports_revenue->get_data();
|
||||
$value = $report_data->totals->total_sales;
|
||||
$report_data = $reports_revenue->get_data();
|
||||
|
||||
if ( ! $report_data || ! isset( $report_data->totals->total_sales ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$value = $report_data->totals->total_sales;
|
||||
|
||||
return ComparisonOperation::compare(
|
||||
$value,
|
||||
@@ -62,7 +67,7 @@ class TotalPaymentsVolumeProcessor implements RuleProcessorInterface {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! isset( $rule->value ) ) {
|
||||
if ( ! isset( $rule->value ) || ! is_numeric( $rule->value ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -72,4 +77,17 @@ class TotalPaymentsVolumeProcessor implements RuleProcessorInterface {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the report query.
|
||||
*
|
||||
* @param array $args The query args.
|
||||
*
|
||||
* @return RevenueQuery The report query.
|
||||
*/
|
||||
protected function get_reports_query( $args ) {
|
||||
return new RevenueQuery(
|
||||
$args
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,12 +36,13 @@ class TransformerService {
|
||||
*
|
||||
* @param mixed $target_value a value to transform.
|
||||
* @param array $transformer_configs transform configuration.
|
||||
* @param bool $is_default_set flag on is default value set.
|
||||
* @param string $default default value.
|
||||
*
|
||||
* @throws InvalidArgumentException Throws when one of the requried arguments is missing.
|
||||
* @return mixed|null
|
||||
*/
|
||||
public static function apply( $target_value, array $transformer_configs, $default ) {
|
||||
public static function apply( $target_value, array $transformer_configs, $is_default_set, $default ) {
|
||||
foreach ( $transformer_configs as $transformer_config ) {
|
||||
if ( ! isset( $transformer_config->use ) ) {
|
||||
throw new InvalidArgumentException( 'Missing required config value: use' );
|
||||
@@ -56,13 +57,25 @@ class TransformerService {
|
||||
throw new InvalidArgumentException( "Unable to find a transformer by name: {$transformer_config->use}" );
|
||||
}
|
||||
|
||||
$transformed_value = $transformer->transform( $target_value, $transformer_config->arguments, $default );
|
||||
// if the transformer returns null, then return the previously transformed value.
|
||||
if ( null === $transformed_value ) {
|
||||
return $target_value;
|
||||
$target_value = $transformer->transform( $target_value, $transformer_config->arguments, $is_default_set ? $default : null );
|
||||
|
||||
// Break early when there's no more value to traverse.
|
||||
if ( null === $target_value ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $is_default_set ) {
|
||||
// Nulls always return the default value.
|
||||
if ( null === $target_value ) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
$target_value = $transformed_value;
|
||||
// When type of the default value is different from the target value, return the default value
|
||||
// to ensure type safety.
|
||||
if ( gettype( $default ) !== gettype( $target_value ) ) {
|
||||
return $default;
|
||||
}
|
||||
}
|
||||
|
||||
return $target_value;
|
||||
|
||||
@@ -23,7 +23,11 @@ class ArrayColumn implements TransformerInterface {
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function transform( $value, stdClass $arguments = null, $default = null ) {
|
||||
public function transform( $value, stdClass $arguments = null, $default = array() ) {
|
||||
if ( ! is_array( $value ) ) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
return array_column( $value, $arguments->key );
|
||||
}
|
||||
|
||||
@@ -39,6 +43,14 @@ class ArrayColumn implements TransformerInterface {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
null !== $arguments->key &&
|
||||
! is_string( $arguments->key ) &&
|
||||
! is_int( $arguments->key )
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,11 @@ class ArrayFlatten implements TransformerInterface {
|
||||
*
|
||||
* @return mixed|null
|
||||
*/
|
||||
public function transform( $value, stdClass $arguments = null, $default = null ) {
|
||||
public function transform( $value, stdClass $arguments = null, $default = array() ) {
|
||||
if ( ! is_array( $value ) ) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
$return = array();
|
||||
array_walk_recursive(
|
||||
$value,
|
||||
|
||||
@@ -20,7 +20,11 @@ class ArrayKeys implements TransformerInterface {
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function transform( $value, stdClass $arguments = null, $default = null ) {
|
||||
public function transform( $value, stdClass $arguments = null, $default = array() ) {
|
||||
if ( ! is_array( $value ) ) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
return array_keys( $value );
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,10 @@ class ArraySearch implements TransformerInterface {
|
||||
* @return mixed|null
|
||||
*/
|
||||
public function transform( $value, stdClass $arguments = null, $default = null ) {
|
||||
if ( ! is_array( $value ) ) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
$key = array_search( $arguments->value, $value, true );
|
||||
if ( false !== $key ) {
|
||||
return $value[ $key ];
|
||||
|
||||
@@ -20,7 +20,11 @@ class ArrayValues implements TransformerInterface {
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function transform( $value, stdClass $arguments = null, $default = null ) {
|
||||
public function transform( $value, stdClass $arguments = null, $default = array() ) {
|
||||
if ( ! is_array( $value ) ) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
return array_values( $value );
|
||||
}
|
||||
|
||||
|
||||
@@ -6,21 +6,25 @@ use Automattic\WooCommerce\Admin\RemoteInboxNotifications\TransformerInterface;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* Count elements in Array.
|
||||
* Count elements in Array or Countable object.
|
||||
*
|
||||
* @package Automattic\WooCommerce\Admin\RemoteInboxNotifications\Transformers
|
||||
*/
|
||||
class Count implements TransformerInterface {
|
||||
/**
|
||||
* Count elements in Array.
|
||||
* Count elements in Array or Countable object.
|
||||
*
|
||||
* @param array $value an array to count.
|
||||
* @param stdClass|null $arguments arguments.
|
||||
* @param string|null $default default value.
|
||||
* @param array|Countable $value an array to count.
|
||||
* @param stdClass|null $arguments arguments.
|
||||
* @param string|null $default default value.
|
||||
*
|
||||
* @return number
|
||||
*/
|
||||
public function transform( $value, stdClass $arguments = null, $default = null ) {
|
||||
if ( ! is_array( $value ) && ! $value instanceof \Countable ) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
return count( $value );
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,10 @@ class DotNotation implements TransformerInterface {
|
||||
* @return mixed|null
|
||||
*/
|
||||
public function get( $array, $path, $default = null ) {
|
||||
if ( ! is_array( $array ) ) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
if ( isset( $array[ $path ] ) ) {
|
||||
return $array[ $path ];
|
||||
}
|
||||
|
||||
@@ -14,15 +14,32 @@ class PrepareUrl implements TransformerInterface {
|
||||
/**
|
||||
* Prepares the site URL by removing the protocol and trailing slash.
|
||||
*
|
||||
* @param mixed $value a value to transform.
|
||||
* @param string $value a value to transform.
|
||||
* @param stdClass|null $arguments arguments.
|
||||
* @param string|null $default default value.
|
||||
*
|
||||
* @return mixed|null
|
||||
*/
|
||||
public function transform( $value, stdClass $arguments = null, $default = null ) {
|
||||
if ( ! is_string( $value ) ) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
$url_parts = wp_parse_url( rtrim( $value, '/' ) );
|
||||
return isset( $url_parts['path'] ) ? $url_parts['host'] . $url_parts['path'] : $url_parts['host'];
|
||||
|
||||
if ( ! $url_parts ) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
if ( ! isset( $url_parts['host'] ) ) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
if ( isset( $url_parts['path'] ) ) {
|
||||
return $url_parts['host'] . $url_parts['path'];
|
||||
}
|
||||
|
||||
return $url_parts['host'];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -43,7 +43,12 @@ class WCAdminActiveForRuleProcessor implements RuleProcessorInterface {
|
||||
*/
|
||||
public function process( $rule, $stored_state ) {
|
||||
$active_for_seconds = $this->wcadmin_active_for_provider->get_wcadmin_active_for_in_seconds();
|
||||
$rule_seconds = $rule->days * DAY_IN_SECONDS;
|
||||
|
||||
if ( ! $active_for_seconds || ! is_numeric( $active_for_seconds ) || $active_for_seconds < 0 ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$rule_seconds = $rule->days * DAY_IN_SECONDS;
|
||||
|
||||
return ComparisonOperation::compare(
|
||||
$active_for_seconds,
|
||||
@@ -60,7 +65,8 @@ class WCAdminActiveForRuleProcessor implements RuleProcessorInterface {
|
||||
* @return bool Pass/fail.
|
||||
*/
|
||||
public function validate( $rule ) {
|
||||
if ( ! isset( $rule->days ) ) {
|
||||
// Ensure that 'days' property is set and is a valid numeric value.
|
||||
if ( ! isset( $rule->days ) || ! is_numeric( $rule->days ) || $rule->days < 0 ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Admin\RemoteSpecs;
|
||||
|
||||
/**
|
||||
* RemoteSpecsEngine class.
|
||||
*/
|
||||
abstract class RemoteSpecsEngine {
|
||||
/**
|
||||
* Log errors.
|
||||
*
|
||||
* @param array $errors Array of errors from \Throwable interface.
|
||||
*/
|
||||
public static function log_errors( $errors = array() ) {
|
||||
if (
|
||||
true !== defined( 'WP_ENVIRONMENT_TYPE' ) ||
|
||||
! in_array( constant( 'WP_ENVIRONMENT_TYPE' ), array( 'development', 'local' ), true )
|
||||
) {
|
||||
return;
|
||||
}
|
||||
$logger = wc_get_logger();
|
||||
$error_messages = array();
|
||||
|
||||
foreach ( $errors as $error ) {
|
||||
if ( isset( $error ) && method_exists( $error, 'getMessage' ) ) {
|
||||
$error_messages[] = $error->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
$logger->error(
|
||||
'Error while evaluating specs',
|
||||
array(
|
||||
'source' => 'remotespecsengine-errors',
|
||||
'class' => static::class,
|
||||
'errors' => $error_messages,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -193,10 +193,6 @@ class UpdateProducts {
|
||||
$image_alt = $product_data['title'];
|
||||
$product_image_id = $this->product_image_upload( $product->get_id(), $image_src, $image_alt );
|
||||
|
||||
if ( is_wp_error( $product_image_id ) ) {
|
||||
return new \WP_Error( 'error_uploading_image', $product_image_id->get_error_message() );
|
||||
}
|
||||
|
||||
$saved_product = $this->product_update( $product, $product_image_id, $product_data['title'], $product_data['description'], $product_data['price'] );
|
||||
|
||||
if ( is_wp_error( $saved_product ) ) {
|
||||
@@ -294,10 +290,6 @@ class UpdateProducts {
|
||||
|
||||
$product_image_id = $this->product_image_upload( $product->get_id(), $ai_generated_product_content['image']['src'], $ai_generated_product_content['image']['alt'] );
|
||||
|
||||
if ( is_wp_error( $product_image_id ) ) {
|
||||
return new \WP_Error( 'error_uploading_image', $product_image_id->get_error_message() );
|
||||
}
|
||||
|
||||
$this->product_update( $product, $product_image_id, $ai_generated_product_content['title'], $ai_generated_product_content['description'], $ai_generated_product_content['price'] );
|
||||
}
|
||||
|
||||
@@ -470,10 +462,6 @@ class UpdateProducts {
|
||||
$image_alt = self::DUMMY_PRODUCTS[ $i ]['title'];
|
||||
$product_image_id = $this->product_image_upload( $product->get_id(), $image_src, $image_alt );
|
||||
|
||||
if ( is_wp_error( $product_image_id ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->product_update( $product, $product_image_id, self::DUMMY_PRODUCTS[ $i ]['title'], self::DUMMY_PRODUCTS[ $i ]['description'], self::DUMMY_PRODUCTS[ $i ]['price'] );
|
||||
|
||||
$i++;
|
||||
@@ -496,7 +484,17 @@ class UpdateProducts {
|
||||
return new WP_Error( 'invalid_product', __( 'Invalid product.', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
$product->set_image_id( $product_image_id );
|
||||
if ( ! is_wp_error( $product_image_id ) ) {
|
||||
$product->set_image_id( $product_image_id );
|
||||
} else {
|
||||
wc_get_logger()->warning(
|
||||
sprintf(
|
||||
// translators: %s is a generated error message.
|
||||
__( 'The image upload failed: "%s", creating the product without image', 'woocommerce' ),
|
||||
$product_image_id->get_error_message()
|
||||
),
|
||||
);
|
||||
}
|
||||
$product->set_name( $product_title );
|
||||
$product->set_description( $product_description );
|
||||
$product->set_price( $product_price );
|
||||
|
||||
@@ -353,6 +353,18 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Product Gallery",
|
||||
"slug": "woocommerce-blocks/product-query-product-gallery",
|
||||
"content": {
|
||||
"titles": [
|
||||
{
|
||||
"default": "Bestsellers",
|
||||
"ai_prompt": "An impact phrase that advertises the featured products with at least 10 characters"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Featured Products 2 Columns",
|
||||
"slug": "woocommerce-blocks/featured-products-2-cols",
|
||||
|
||||
@@ -11,7 +11,6 @@ use Automattic\WooCommerce\Blocks\Templates\SingleProductTemplateCompatibility;
|
||||
use Automattic\WooCommerce\Blocks\Utils\BlockTemplateUtils;
|
||||
use Automattic\WooCommerce\Blocks\Templates\OrderConfirmationTemplate;
|
||||
use Automattic\WooCommerce\Blocks\Templates\SingleProductTemplate;
|
||||
use Automattic\WooCommerce\Blocks\Utils\BlockTemplateMigrationUtils;
|
||||
|
||||
/**
|
||||
* BlockTypesController class.
|
||||
@@ -59,6 +58,7 @@ class BlockTemplatesController {
|
||||
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 );
|
||||
add_filter( 'get_block_template', array( $this, 'add_block_template_details' ), 10, 1 );
|
||||
add_filter( 'get_block_templates', array( $this, 'add_block_templates' ), 10, 3 );
|
||||
add_filter( 'current_theme_supports-block-templates', array( $this, 'remove_block_template_support_for_shop_page' ) );
|
||||
add_filter( 'taxonomy_template_hierarchy', array( $this, 'add_archive_product_to_eligible_for_fallback_templates' ), 10, 1 );
|
||||
@@ -66,8 +66,6 @@ class BlockTemplatesController {
|
||||
add_action( 'after_switch_theme', array( $this, 'check_should_use_blockified_product_grid_templates' ), 10, 2 );
|
||||
|
||||
if ( wc_current_theme_is_fse_theme() ) {
|
||||
add_action( 'init', array( $this, 'maybe_migrate_content' ) );
|
||||
|
||||
// By default, the Template Part Block only supports template parts that are in the current theme directory.
|
||||
// This render_callback wrapper allows us to add support for plugin-housed template parts.
|
||||
add_filter(
|
||||
@@ -162,7 +160,7 @@ class BlockTemplatesController {
|
||||
*/
|
||||
public function render_woocommerce_template_part( $attributes ) {
|
||||
if ( isset( $attributes['theme'] ) && 'woocommerce/woocommerce' === $attributes['theme'] ) {
|
||||
$template_part = BlockTemplateUtils::get_block_template( $attributes['theme'] . '//' . $attributes['slug'], 'wp_template_part' );
|
||||
$template_part = get_block_template( $attributes['theme'] . '//' . $attributes['slug'], 'wp_template_part' );
|
||||
|
||||
if ( $template_part && ! empty( $template_part->content ) ) {
|
||||
return do_blocks( $template_part->content );
|
||||
@@ -317,7 +315,7 @@ class BlockTemplatesController {
|
||||
if ( BlockTemplateUtils::DEPRECATED_PLUGIN_SLUG === strtolower( $template_id ) ) {
|
||||
// Because we are using get_block_templates we have to unhook this method to prevent a recursive loop where this filter is applied.
|
||||
remove_filter( 'pre_get_block_file_template', array( $this, 'get_block_file_template' ), 10, 3 );
|
||||
$template_with_deprecated_id = BlockTemplateUtils::get_block_template( $id, $template_type );
|
||||
$template_with_deprecated_id = get_block_template( $id, $template_type );
|
||||
// Let's hook this method back now that we have used the function.
|
||||
add_filter( 'pre_get_block_file_template', array( $this, 'get_block_file_template' ), 10, 3 );
|
||||
|
||||
@@ -349,6 +347,25 @@ class BlockTemplatesController {
|
||||
return $template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the template title and description to WooCommerce templates.
|
||||
*
|
||||
* @param WP_Block_Template|null $block_template The found block template, or null if there isn't one.
|
||||
* @return WP_Block_Template|null
|
||||
*/
|
||||
public function add_block_template_details( $block_template ) {
|
||||
if ( ! $block_template ) {
|
||||
return $block_template;
|
||||
}
|
||||
if ( ! BlockTemplateUtils::template_has_title( $block_template ) ) {
|
||||
$block_template->title = BlockTemplateUtils::get_block_template_title( $block_template->slug );
|
||||
}
|
||||
if ( ! $block_template->description ) {
|
||||
$block_template->description = BlockTemplateUtils::get_block_template_description( $block_template->slug );
|
||||
}
|
||||
return $block_template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the block template objects to be used.
|
||||
*
|
||||
@@ -365,6 +382,7 @@ class BlockTemplatesController {
|
||||
$post_type = isset( $query['post_type'] ) ? $query['post_type'] : '';
|
||||
$slugs = isset( $query['slug__in'] ) ? $query['slug__in'] : array();
|
||||
$template_files = $this->get_block_templates( $slugs, $template_type );
|
||||
$theme_slug = wp_get_theme()->get_stylesheet();
|
||||
|
||||
// @todo: Add apply_filters to _gutenberg_get_template_files() in Gutenberg to prevent duplication of logic.
|
||||
foreach ( $template_files as $template_file ) {
|
||||
@@ -391,14 +409,12 @@ class BlockTemplatesController {
|
||||
if ( 'custom' !== $template_file->source ) {
|
||||
$template = BlockTemplateUtils::build_template_result_from_file( $template_file, $template_type );
|
||||
} else {
|
||||
$template_file->title = BlockTemplateUtils::get_block_template_title( $template_file->slug );
|
||||
$template_file->description = BlockTemplateUtils::get_block_template_description( $template_file->slug );
|
||||
$query_result[] = $template_file;
|
||||
$query_result[] = $template_file;
|
||||
continue;
|
||||
}
|
||||
|
||||
$is_not_custom = false === array_search(
|
||||
wp_get_theme()->get_stylesheet() . '//' . $template_file->slug,
|
||||
$theme_slug . '//' . $template_file->slug,
|
||||
array_column( $query_result, 'id' ),
|
||||
true
|
||||
);
|
||||
@@ -416,6 +432,11 @@ class BlockTemplatesController {
|
||||
// This only affects saved templates that were saved BEFORE a theme template with the same slug was added.
|
||||
$query_result = BlockTemplateUtils::remove_theme_templates_with_custom_alternative( $query_result );
|
||||
|
||||
// There is the chance that the user customized the default template, installed a theme with a custom template
|
||||
// and customized that one as well. When that happens, duplicates might appear in the list.
|
||||
// See: https://github.com/woocommerce/woocommerce/issues/42220.
|
||||
$query_result = BlockTemplateUtils::remove_duplicate_customized_templates( $query_result, $theme_slug );
|
||||
|
||||
/**
|
||||
* WC templates from theme aren't included in `$this->get_block_templates()` but are handled by Gutenberg.
|
||||
* We need to do additional search through all templates file to update title and description for WC
|
||||
@@ -453,15 +474,13 @@ class BlockTemplatesController {
|
||||
}
|
||||
}
|
||||
|
||||
if ( 'theme' === $template->origin && BlockTemplateUtils::template_has_title( $template ) ) {
|
||||
return $template;
|
||||
}
|
||||
if ( $template->title === $template->slug ) {
|
||||
if ( ! BlockTemplateUtils::template_has_title( $template ) ) {
|
||||
$template->title = BlockTemplateUtils::get_block_template_title( $template->slug );
|
||||
}
|
||||
if ( ! $template->description ) {
|
||||
$template->description = BlockTemplateUtils::get_block_template_description( $template->slug );
|
||||
}
|
||||
|
||||
return $template;
|
||||
},
|
||||
$query_result
|
||||
@@ -577,9 +596,8 @@ class BlockTemplatesController {
|
||||
public function get_block_templates( $slugs = array(), $template_type = 'wp_template' ) {
|
||||
$templates_from_db = BlockTemplateUtils::get_block_templates_from_db( $slugs, $template_type );
|
||||
$templates_from_woo = $this->get_block_templates_from_woocommerce( $slugs, $templates_from_db, $template_type );
|
||||
$templates = array_merge( $templates_from_db, $templates_from_woo );
|
||||
|
||||
return BlockTemplateUtils::filter_block_templates_by_feature_flag( $templates );
|
||||
return array_merge( $templates_from_db, $templates_from_woo );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -761,22 +779,4 @@ class BlockTemplatesController {
|
||||
|
||||
return $post_type_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates page content to templates if needed.
|
||||
*/
|
||||
public function maybe_migrate_content() {
|
||||
// Migration should occur on a normal request to ensure every requirement is met.
|
||||
// We are postponing it if WP is in maintenance mode, installing, WC installing or if the request is part of a WP-CLI command.
|
||||
if ( wp_is_maintenance_mode() || ! get_option( 'woocommerce_db_version', false ) || Constants::is_defined( 'WP_SETUP_CONFIG' ) || Constants::is_defined( 'WC_INSTALLING' ) || Constants::is_defined( 'WP_CLI' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! BlockTemplateMigrationUtils::has_migrated_page( 'cart' ) ) {
|
||||
BlockTemplateMigrationUtils::migrate_page( 'cart' );
|
||||
}
|
||||
if ( ! BlockTemplateMigrationUtils::has_migrated_page( 'checkout' ) ) {
|
||||
BlockTemplateMigrationUtils::migrate_page( 'checkout' );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -666,6 +666,18 @@ abstract class AbstractProductGrid extends AbstractDynamicBlock {
|
||||
$attributes['class'] .= ' ajax_add_to_cart';
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter to manipulate (add/modify/remove) attributes in the HTML code of the generated add to cart button.
|
||||
*
|
||||
* @since 8.6.0
|
||||
*
|
||||
* @param array $attributes An associative array containing default HTML attributes of the add to cart button.
|
||||
* @param WC_Product $product The WC_Product instance of the product that will be added to the cart once the button is pressed.
|
||||
*
|
||||
* @return array Returns an associative array derived from the default array passed as an argument and added the extra HTML attributes.
|
||||
*/
|
||||
$attributes = apply_filters( 'woocommerce_blocks_product_grid_add_to_cart_attributes', $attributes, $product );
|
||||
|
||||
return sprintf(
|
||||
'<a href="%s" %s>%s</a>',
|
||||
esc_url( $product->add_to_cart_url() ),
|
||||
|
||||
@@ -37,6 +37,21 @@ class ClassicTemplate extends AbstractDynamicBlock {
|
||||
add_action( 'enqueue_block_assets', array( $this, 'enqueue_block_assets' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 );
|
||||
|
||||
// Indicate to interactivity powered components that this block is on the page,
|
||||
// and needs refresh to update data.
|
||||
$this->asset_data_registry->add( 'needsRefreshForInteractivityAPI', true, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue assets used for rendering the block in editor context.
|
||||
*
|
||||
|
||||
@@ -437,7 +437,7 @@ class MiniCart extends AbstractBlock {
|
||||
if ( isset( $attributes['miniCartIcon'] ) ) {
|
||||
if ( 'bag' === $attributes['miniCartIcon'] ) {
|
||||
$icon = '<svg class="wc-block-mini-cart__icon" width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.4444 14.2222C12.9354 14.2222 13.3333 14.6202 13.3333 15.1111C13.3333 15.8183 13.6143 16.4966 14.1144 16.9967C14.6145 17.4968 15.2927 17.7778 16 17.7778C16.7072 17.7778 17.3855 17.4968 17.8856 16.9967C18.3857 16.4966 18.6667 15.8183 18.6667 15.1111C18.6667 14.6202 19.0646 14.2222 19.5555 14.2222C20.0465 14.2222 20.4444 14.6202 20.4444 15.1111C20.4444 16.2898 19.9762 17.4203 19.1427 18.2538C18.3092 19.0873 17.1787 19.5555 16 19.5555C14.8212 19.5555 13.6908 19.0873 12.8573 18.2538C12.0238 17.4203 11.5555 16.2898 11.5555 15.1111C11.5555 14.6202 11.9535 14.2222 12.4444 14.2222Z" fill="' . $icon_color . '""/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.4444 14.2222C12.9354 14.2222 13.3333 14.6202 13.3333 15.1111C13.3333 15.8183 13.6143 16.4966 14.1144 16.9967C14.6145 17.4968 15.2927 17.7778 16 17.7778C16.7072 17.7778 17.3855 17.4968 17.8856 16.9967C18.3857 16.4966 18.6667 15.8183 18.6667 15.1111C18.6667 14.6202 19.0646 14.2222 19.5555 14.2222C20.0465 14.2222 20.4444 14.6202 20.4444 15.1111C20.4444 16.2898 19.9762 17.4203 19.1427 18.2538C18.3092 19.0873 17.1787 19.5555 16 19.5555C14.8212 19.5555 13.6908 19.0873 12.8573 18.2538C12.0238 17.4203 11.5555 16.2898 11.5555 15.1111C11.5555 14.6202 11.9535 14.2222 12.4444 14.2222Z" fill="' . $icon_color . '"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.2408 6.68254C11.4307 6.46089 11.7081 6.33333 12 6.33333H20C20.2919 6.33333 20.5693 6.46089 20.7593 6.68254L24.7593 11.3492C25.0134 11.6457 25.0717 12.0631 24.9085 12.4179C24.7453 12.7727 24.3905 13 24 13H8.00001C7.60948 13 7.25469 12.7727 7.0915 12.4179C6.92832 12.0631 6.9866 11.6457 7.24076 11.3492L11.2408 6.68254ZM12.4599 8.33333L10.1742 11H21.8258L19.5401 8.33333H12.4599Z" fill="' . $icon_color . '"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7 12C7 11.4477 7.44772 11 8 11H24C24.5523 11 25 11.4477 25 12V25.3333C25 25.8856 24.5523 26.3333 24 26.3333H8C7.44772 26.3333 7 25.8856 7 25.3333V12ZM9 13V24.3333H23V13H9Z" fill="' . $icon_color . '"/>
|
||||
</svg>';
|
||||
@@ -476,7 +476,7 @@ class MiniCart extends AbstractBlock {
|
||||
$theme_has_mini_cart = BlockTemplateUtils::theme_has_template_part( 'mini-cart' );
|
||||
$template_slug_to_load = $theme_has_mini_cart ? get_stylesheet() : BlockTemplateUtils::PLUGIN_SLUG;
|
||||
}
|
||||
$template_part = BlockTemplateUtils::get_block_template( $template_slug_to_load . '//mini-cart', 'wp_template_part' );
|
||||
$template_part = get_block_template( $template_slug_to_load . '//mini-cart', 'wp_template_part' );
|
||||
|
||||
if ( $template_part && ! empty( $template_part->content ) ) {
|
||||
$template_part_contents = do_blocks( $template_part->content );
|
||||
|
||||
@@ -33,9 +33,11 @@ class AdditionalFields extends AbstractOrderConfirmationBlock {
|
||||
|
||||
$controller = Package::container()->get( CheckoutFields::class );
|
||||
$content .= $this->render_additional_fields(
|
||||
array_merge(
|
||||
$controller->get_order_additional_fields_with_values( $order, 'contact', '', 'view' ),
|
||||
$controller->get_order_additional_fields_with_values( $order, 'additional', '', 'view' ),
|
||||
$controller->filter_fields_for_order_confirmation(
|
||||
array_merge(
|
||||
$controller->get_order_additional_fields_with_values( $order, 'contact', '', 'view' ),
|
||||
$controller->get_order_additional_fields_with_values( $order, 'additional', '', 'view' ),
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
@@ -45,23 +45,6 @@ class ProductCollection extends AbstractBlock {
|
||||
*/
|
||||
protected $custom_order_opts = array( 'popularity', 'rating' );
|
||||
|
||||
/**
|
||||
* Get the frontend script handle for this block type.
|
||||
*
|
||||
* @param string $key Data to get, or default to everything.
|
||||
*/
|
||||
protected function get_block_type_script( $key = null ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the frontend style handle for this block type.
|
||||
*
|
||||
* @return null
|
||||
*/
|
||||
protected function get_block_type_style() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize this block type.
|
||||
@@ -94,21 +77,24 @@ class ProductCollection extends AbstractBlock {
|
||||
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_woocommerce/product-collection', array( $this, 'enhance_product_collection_with_interactivity' ), 10, 2 );
|
||||
add_filter( 'render_block_core/query-pagination', array( $this, 'add_navigation_link_directives' ), 10, 3 );
|
||||
|
||||
add_filter( 'posts_clauses', array( $this, 'add_price_range_filter_posts_clauses' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the Product Collection as an interactive region so it can be updated
|
||||
* during client-side navigation.
|
||||
* Enhances the Product Collection block with client-side pagination.
|
||||
*
|
||||
* @param string $block_content The block content.
|
||||
* @param array $block The full block, including name and attributes.
|
||||
* @param \WP_Block $instance The block instance.
|
||||
* This function identifies Product Collection blocks and adds necessary data attributes
|
||||
* to enable client-side navigation and animation effects. It also enqueues the Interactivity API runtime.
|
||||
*
|
||||
* @param string $block_content The HTML content of the block.
|
||||
* @param array $block Block details, including its attributes.
|
||||
*
|
||||
* @return string Updated block content with added interactivity attributes.
|
||||
*/
|
||||
public function add_navigation_id_directive( $block_content, $block, $instance ) {
|
||||
public function enhance_product_collection_with_interactivity( $block_content, $block ) {
|
||||
$is_product_collection_block = $block['attrs']['query']['isProductCollectionBlock'] ?? false;
|
||||
if ( $is_product_collection_block ) {
|
||||
// Enqueue the Interactivity API runtime.
|
||||
@@ -116,15 +102,56 @@ class ProductCollection extends AbstractBlock {
|
||||
|
||||
$p = new \WP_HTML_Tag_Processor( $block_content );
|
||||
|
||||
// Add `data-wc-navigation-id to the query block.
|
||||
// Add `data-wc-navigation-id to the product collection 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', wp_json_encode( array( 'namespace' => 'woocommerce/product-collection' ) ) );
|
||||
$p->set_attribute(
|
||||
'data-wc-context',
|
||||
wp_json_encode(
|
||||
array(
|
||||
// The message to be announced by the screen reader when the page is loading or loaded.
|
||||
'accessibilityLoadingMessage' => __( 'Loading page, please wait.', 'woocommerce' ),
|
||||
'accessibilityLoadedMessage' => __( 'Page Loaded.', 'woocommerce' ),
|
||||
// We don't prefetch the links if user haven't clicked on pagination links yet.
|
||||
// This way we avoid prefetching when the page loads.
|
||||
'isPrefetchNextOrPreviousLink' => false,
|
||||
),
|
||||
JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP
|
||||
)
|
||||
);
|
||||
$block_content = $p->get_updated_html();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add two div's:
|
||||
* 1. Pagination animation for visual users.
|
||||
* 2. Accessibility div for screen readers, to announce page load states.
|
||||
*/
|
||||
$last_tag_position = strripos( $block_content, '</div>' );
|
||||
$accessibility_and_animation_html = '
|
||||
<div
|
||||
data-wc-interactive="{"namespace":"woocommerce/product-collection"}"
|
||||
class="wc-block-product-collection__pagination-animation"
|
||||
data-wc-class--start-animation="state.startAnimation"
|
||||
data-wc-class--finish-animation="state.finishAnimation">
|
||||
</div>
|
||||
<div
|
||||
data-wc-interactive="{"namespace":"woocommerce/product-collection"}"
|
||||
class="screen-reader-text"
|
||||
aria-live="polite"
|
||||
data-wc-text="context.accessibilityMessage">
|
||||
</div>
|
||||
';
|
||||
$block_content = substr_replace(
|
||||
$block_content,
|
||||
$accessibility_and_animation_html,
|
||||
$last_tag_position,
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
return $block_content;
|
||||
@@ -132,51 +159,78 @@ class ProductCollection extends AbstractBlock {
|
||||
|
||||
/**
|
||||
* Add interactive links to all anchors inside the Query Pagination block.
|
||||
* This enabled client-side navigation for the product collection 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;
|
||||
$query_context = $instance->context['query'] ?? array();
|
||||
$is_product_collection_block = $query_context['isProductCollectionBlock'] ?? false;
|
||||
$query_id = $instance->context['queryId'] ?? null;
|
||||
$parsed_query_id = $this->parsed_block['attrs']['queryId'] ?? null;
|
||||
|
||||
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();
|
||||
// Only proceed if the block is a product collection block and query IDs match.
|
||||
if ( $is_product_collection_block && $query_id === $parsed_query_id ) {
|
||||
$block_content = $this->process_pagination_links( $block_content );
|
||||
}
|
||||
|
||||
return $block_content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process pagination links within the block content.
|
||||
*
|
||||
* @param string $block_content The block content.
|
||||
* @return string The updated block content.
|
||||
*/
|
||||
private function process_pagination_links( $block_content ) {
|
||||
if ( ! $block_content ) {
|
||||
return $block_content;
|
||||
}
|
||||
|
||||
$p = new \WP_HTML_Tag_Processor( $block_content );
|
||||
$p->next_tag( array( 'class_name' => 'wp-block-query-pagination' ) );
|
||||
|
||||
// This will help us to find the start of the block content using the `seek` method.
|
||||
$p->set_bookmark( 'start' );
|
||||
|
||||
$this->update_pagination_anchors( $p, 'page-numbers', 'product-collection-pagination-numbers' );
|
||||
$this->update_pagination_anchors( $p, 'wp-block-query-pagination-next', 'product-collection-pagination--next' );
|
||||
$this->update_pagination_anchors( $p, 'wp-block-query-pagination-previous', 'product-collection-pagination--previous' );
|
||||
|
||||
return $p->get_updated_html();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up data attributes required for interactivity and client-side navigation.
|
||||
*
|
||||
* @param \WP_HTML_Tag_Processor $processor The HTML tag processor.
|
||||
* @param string $class_name The class name of the anchor tags.
|
||||
* @param string $key_prefix The prefix for the data-wc-key attribute.
|
||||
*/
|
||||
private function update_pagination_anchors( $processor, $class_name, $key_prefix ) {
|
||||
// Start from the beginning of the block content.
|
||||
$processor->seek( 'start' );
|
||||
|
||||
while ( $processor->next_tag(
|
||||
array(
|
||||
'tag_name' => 'a',
|
||||
'class_name' => $class_name,
|
||||
)
|
||||
) ) {
|
||||
$processor->set_attribute( 'data-wc-interactive', wp_json_encode( array( 'namespace' => 'woocommerce/product-collection' ) ) );
|
||||
$processor->set_attribute( 'data-wc-on--click', 'actions.navigate' );
|
||||
$processor->set_attribute( 'data-wc-key', $key_prefix . '--' . esc_attr( wp_rand() ) );
|
||||
|
||||
if ( in_array( $class_name, array( 'wp-block-query-pagination-next', 'wp-block-query-pagination-previous' ), true ) ) {
|
||||
$processor->set_attribute( 'data-wc-watch', 'callbacks.prefetch' );
|
||||
$processor->set_attribute( 'data-wc-on--mouseenter', 'actions.prefetchOnHover' );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extra data passed through from server to client for block.
|
||||
*
|
||||
@@ -765,7 +819,7 @@ class ProductCollection extends AbstractBlock {
|
||||
* Get an array of taxonomy names associated with the "product" post type because
|
||||
* we also want to include custom taxonomies associated with the "product" post type.
|
||||
*/
|
||||
$product_taxonomies = get_taxonomies( array( 'object_type' => array( 'product' ) ), 'names' );
|
||||
$product_taxonomies = array_diff( get_object_taxonomies( 'product', 'names' ), array( 'product_visibility', 'product_shipping_class' ) );
|
||||
$result = array_filter(
|
||||
$tax_query,
|
||||
function ( $item ) use ( $product_taxonomies ) {
|
||||
|
||||
@@ -3,6 +3,7 @@ namespace Automattic\WooCommerce\Blocks\BlockTypes;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\QueryFilters;
|
||||
use Automattic\WooCommerce\Blocks\Package;
|
||||
use WP_HTML_Tag_Processor;
|
||||
|
||||
/**
|
||||
* Product Filter Block.
|
||||
@@ -15,13 +16,6 @@ final class ProductFilter extends AbstractBlock {
|
||||
*/
|
||||
protected $block_name = 'product-filter';
|
||||
|
||||
/**
|
||||
* Cache the current response from the API.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $current_response = null;
|
||||
|
||||
/**
|
||||
* Get the frontend style handle for this block type.
|
||||
*
|
||||
@@ -42,17 +36,6 @@ final class ProductFilter extends AbstractBlock {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize this block type.
|
||||
*
|
||||
* - Hook into WP lifecycle.
|
||||
* - Register the block with WordPress.
|
||||
*/
|
||||
protected function initialize() {
|
||||
parent::initialize();
|
||||
add_action( 'render_block_context', array( $this, 'modify_inner_blocks_context' ), 10, 3 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Extra data passed through from server to client for block.
|
||||
*
|
||||
@@ -70,55 +53,6 @@ final class ProductFilter extends AbstractBlock {
|
||||
$this->asset_data_registry->add( 'isWidgetEditor', 'widgets.php' === $pagenow || 'customize.php' === $pagenow, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the collection data is empty.
|
||||
*
|
||||
* @param mixed $attributes - Block attributes.
|
||||
* @return bool - Whether the collection data is empty.
|
||||
*/
|
||||
private function collection_data_is_empty( $attributes ) {
|
||||
$filter_type = $attributes['filterType'];
|
||||
|
||||
if ( 'active-filters' !== $filter_type && empty( $this->current_response ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ( 'attribute-filter' === $filter_type ) {
|
||||
return empty( $this->current_response['attribute_counts'] );
|
||||
}
|
||||
|
||||
if ( 'rating-filter' === $filter_type ) {
|
||||
return empty( $this->current_response['rating_counts'] );
|
||||
}
|
||||
|
||||
if ( 'price-filter' === $filter_type ) {
|
||||
return empty( $this->current_response['price_range'] ) || ( $this->current_response['price_range']['min_price'] === $this->current_response['price_range']['max_price'] );
|
||||
}
|
||||
|
||||
if ( 'stock-filter' === $filter_type ) {
|
||||
return empty( $this->current_response['stock_status_counts'] );
|
||||
}
|
||||
|
||||
if ( 'active-filters' === $filter_type ) {
|
||||
// Duplicate query param logic from ProductFilterActive block, to determine if we should
|
||||
// display the ProductFilter block or not.
|
||||
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
|
||||
$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : '';
|
||||
$parsed_url = wp_parse_url( esc_url_raw( $request_uri ) );
|
||||
|
||||
$url_query_params = [];
|
||||
|
||||
if ( isset( $parsed_url['query'] ) ) {
|
||||
parse_str( $parsed_url['query'], $url_query_params );
|
||||
}
|
||||
|
||||
// phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
|
||||
return empty( array_unique( apply_filters( 'collection_filter_query_param_keys', array(), array_keys( $url_query_params ) ) ) );
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the block.
|
||||
*
|
||||
@@ -132,49 +66,30 @@ final class ProductFilter extends AbstractBlock {
|
||||
return $content;
|
||||
}
|
||||
|
||||
if ( $this->collection_data_is_empty( $attributes ) ) {
|
||||
return $this->render_empty_block( $block );
|
||||
}
|
||||
|
||||
return $this->render_filter_block( $content, $block );
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the current response, must be done before rendering.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function reset_current_response() {
|
||||
/**
|
||||
* When WP starts rendering the Product Filters block,
|
||||
* we can safely unset the current response.
|
||||
*/
|
||||
$this->current_response = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the block when it's empty.
|
||||
*
|
||||
* @param mixed $block - Block instance.
|
||||
* @return string - Rendered block type output.
|
||||
*/
|
||||
private function render_empty_block( $block ) {
|
||||
$this->reset_current_response();
|
||||
|
||||
$attributes = array(
|
||||
$attributes_data = array(
|
||||
'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ) ),
|
||||
'class' => 'wc-block-product-filters',
|
||||
);
|
||||
|
||||
if ( ! isset( $block->context['queryId'] ) ) {
|
||||
$attributes['data-wc-navigation-id'] = $this->generate_navigation_id( $block );
|
||||
$attributes_data['data-wc-navigation-id'] = $this->generate_navigation_id( $block );
|
||||
}
|
||||
|
||||
$tags = new WP_HTML_Tag_Processor( $content );
|
||||
|
||||
while ( $tags->next_tag( 'div' ) ) {
|
||||
if ( 'yes' === $tags->get_attribute( 'data-has-filter' ) ) {
|
||||
return sprintf(
|
||||
'<nav %1$s>%2$s</nav>',
|
||||
get_block_wrapper_attributes( $attributes_data ),
|
||||
$content
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'<nav %1$s></nav>',
|
||||
get_block_wrapper_attributes(
|
||||
$attributes
|
||||
)
|
||||
get_block_wrapper_attributes( $attributes_data ),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -190,257 +105,4 @@ final class ProductFilter extends AbstractBlock {
|
||||
md5( wp_json_encode( $block->parsed_block['innerBlocks'] ) )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the block when it's not empty.
|
||||
*
|
||||
* @param string $content - Block content.
|
||||
* @param WP_Block $block - Block instance.
|
||||
* @return string - Rendered block type output.
|
||||
*/
|
||||
private function render_filter_block( $content, $block ) {
|
||||
$this->reset_current_response();
|
||||
|
||||
$attributes_data = array(
|
||||
'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ) ),
|
||||
'class' => 'wc-block-product-filters',
|
||||
);
|
||||
|
||||
if ( ! isset( $block->context['queryId'] ) ) {
|
||||
$attributes_data['data-wc-navigation-id'] = $this->generate_navigation_id( $block );
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'<nav %1$s>%2$s</nav>',
|
||||
get_block_wrapper_attributes( $attributes_data ),
|
||||
$content
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify the context of inner blocks.
|
||||
*
|
||||
* @param array $context The block context.
|
||||
* @param array $parsed_block The parsed block.
|
||||
* @param WP_Block $parent_block The parent block.
|
||||
* @return array
|
||||
*/
|
||||
public function modify_inner_blocks_context( $context, $parsed_block, $parent_block ) {
|
||||
if ( is_admin() || ! is_a( $parent_block, 'WP_Block' ) ) {
|
||||
return $context;
|
||||
}
|
||||
|
||||
/**
|
||||
* When the first direct child of Product Filters is rendering, we
|
||||
* hydrate and cache the collection data response.
|
||||
*/
|
||||
if (
|
||||
"woocommerce/{$this->block_name}" === $parent_block->name &&
|
||||
! isset( $this->current_response )
|
||||
) {
|
||||
$this->current_response = $this->get_aggregated_collection_data( $parent_block );
|
||||
}
|
||||
|
||||
if ( empty( $this->current_response ) ) {
|
||||
return $context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter blocks use the collectionData context, so we only update that
|
||||
* specific context with fetched data.
|
||||
*/
|
||||
if ( isset( $context['collectionData'] ) ) {
|
||||
$context['collectionData'] = $this->current_response;
|
||||
}
|
||||
|
||||
return $context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the aggregated collection data from the API.
|
||||
* Loop through inner blocks and build a query string to pass to the API.
|
||||
*
|
||||
* @param WP_Block $block The block instance.
|
||||
* @return array
|
||||
*/
|
||||
private function get_aggregated_collection_data( $block ) {
|
||||
$collection_data_params = $this->get_inner_collection_data_params( $block->inner_blocks );
|
||||
|
||||
if ( empty( array_filter( $collection_data_params ) ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$data = array(
|
||||
'min_price' => null,
|
||||
'max_price' => null,
|
||||
'attribute_counts' => null,
|
||||
'stock_status_counts' => null,
|
||||
'rating_counts' => null,
|
||||
);
|
||||
|
||||
$filters = Package::container()->get( QueryFilters::class );
|
||||
|
||||
if ( ! empty( $block->context['query'] ) && ! $block->context['query']['inherit'] ) {
|
||||
$query_vars = build_query_vars_from_query_block( $block, 1 );
|
||||
} else {
|
||||
global $wp_query;
|
||||
$query_vars = array_filter( $wp_query->query_vars );
|
||||
}
|
||||
|
||||
if ( ! empty( $collection_data_params['calculate_price_range'] ) ) {
|
||||
$filter_query_vars = $query_vars;
|
||||
|
||||
unset( $filter_query_vars['min_price'], $filter_query_vars['max_price'] );
|
||||
|
||||
if ( ! empty( $filter_query_vars['meta_query'] ) ) {
|
||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
$filter_query_vars['meta_query'] = $this->remove_query_array( $filter_query_vars['meta_query'], 'key', '_price' );
|
||||
}
|
||||
|
||||
$price_results = $filters->get_filtered_price( $filter_query_vars );
|
||||
$data['price_range'] = array(
|
||||
'min_price' => intval( floor( $price_results->min_price ?? 0 ) ),
|
||||
'max_price' => intval( ceil( $price_results->max_price ?? 0 ) ),
|
||||
);
|
||||
}
|
||||
|
||||
if ( ! empty( $collection_data_params['calculate_stock_status_counts'] ) ) {
|
||||
$filter_query_vars = $query_vars;
|
||||
|
||||
unset( $filter_query_vars['filter_stock_status'] );
|
||||
|
||||
if ( ! empty( $filter_query_vars['meta_query'] ) ) {
|
||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
$filter_query_vars['meta_query'] = $this->remove_query_array( $filter_query_vars['meta_query'], 'key', '_stock_status' );
|
||||
}
|
||||
|
||||
$counts = $filters->get_stock_status_counts( $filter_query_vars );
|
||||
|
||||
$data['stock_status_counts'] = array();
|
||||
|
||||
foreach ( $counts as $key => $value ) {
|
||||
$data['stock_status_counts'][] = array(
|
||||
'status' => $key,
|
||||
'count' => $value,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! empty( $collection_data_params['calculate_rating_counts'] ) ) {
|
||||
// Regenerate the products query vars without rating filter.
|
||||
$filter_query_vars = $query_vars;
|
||||
|
||||
if ( ! empty( $filter_query_vars['tax_query'] ) ) {
|
||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
|
||||
$filter_query_vars['tax_query'] = $this->remove_query_array( $filter_query_vars['tax_query'], 'rating_filter', true );
|
||||
}
|
||||
|
||||
$counts = $filters->get_rating_counts( $filter_query_vars );
|
||||
$data['rating_counts'] = array();
|
||||
|
||||
foreach ( $counts as $key => $value ) {
|
||||
$data['rating_counts'][] = array(
|
||||
'rating' => $key,
|
||||
'count' => $value,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! empty( $collection_data_params['calculate_attribute_counts'] ) ) {
|
||||
foreach ( $collection_data_params['calculate_attribute_counts'] as $attributes_to_count ) {
|
||||
if ( ! isset( $attributes_to_count['taxonomy'] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$filter_query_vars = $query_vars;
|
||||
|
||||
if ( 'and' !== strtolower( $attributes_to_count['queryType'] ) ) {
|
||||
unset( $filter_query_vars[ 'filter_' . str_replace( 'pa_', '', $attributes_to_count['taxonomy'] ) ] );
|
||||
}
|
||||
|
||||
unset(
|
||||
$filter_query_vars['taxonomy'],
|
||||
$filter_query_vars['term']
|
||||
);
|
||||
|
||||
if ( ! empty( $filter_query_vars['tax_query'] ) ) {
|
||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
|
||||
$filter_query_vars['tax_query'] = $this->remove_query_array( $filter_query_vars['tax_query'], 'taxonomy', $attributes_to_count['taxonomy'] );
|
||||
}
|
||||
|
||||
$counts = $filters->get_attribute_counts( $filter_query_vars, $attributes_to_count['taxonomy'] );
|
||||
|
||||
foreach ( $counts as $key => $value ) {
|
||||
$data['attribute_counts'][] = array(
|
||||
'term' => $key,
|
||||
'count' => $value,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove query array from tax or meta query by searching for arrays that
|
||||
* contain exact key => value pair.
|
||||
*
|
||||
* @param array $queries tax_query or meta_query.
|
||||
* @param string $key Array key to search for.
|
||||
* @param mixed $value Value to compare with search result.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function remove_query_array( $queries, $key, $value ) {
|
||||
if ( empty( $queries ) ) {
|
||||
return $queries;
|
||||
}
|
||||
|
||||
foreach ( $queries as $query_key => $query ) {
|
||||
if ( isset( $query[ $key ] ) && $query[ $key ] === $value ) {
|
||||
unset( $queries[ $query_key ] );
|
||||
}
|
||||
|
||||
if ( isset( $query['relation'] ) ) {
|
||||
$queries[ $query_key ] = $this->remove_query_array( $query, $key, $value );
|
||||
}
|
||||
}
|
||||
|
||||
return $queries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all inner blocks recursively.
|
||||
*
|
||||
* @param WP_Block_List $inner_blocks The block to get inner blocks from.
|
||||
* @param array $results The results array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function get_inner_collection_data_params( $inner_blocks, &$results = array() ) {
|
||||
if ( is_a( $inner_blocks, 'WP_Block_List' ) ) {
|
||||
foreach ( $inner_blocks as $inner_block ) {
|
||||
if ( ! empty( $inner_block->attributes['queryParam'] ) ) {
|
||||
$query_param = $inner_block->attributes['queryParam'];
|
||||
/**
|
||||
* There can be multiple attribute filters so we transform
|
||||
* the query param of each filter into an array to merge
|
||||
* them together.
|
||||
*/
|
||||
if ( ! empty( $query_param['calculate_attribute_counts'] ) ) {
|
||||
$query_param['calculate_attribute_counts'] = array( $query_param['calculate_attribute_counts'] );
|
||||
}
|
||||
$results = array_merge_recursive( $results, $query_param );
|
||||
}
|
||||
$this->get_inner_collection_data_params(
|
||||
$inner_block->inner_blocks,
|
||||
$results
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -55,28 +55,34 @@ final class ProductFilterActive extends AbstractBlock {
|
||||
|
||||
$wrapper_attributes = get_block_wrapper_attributes(
|
||||
array(
|
||||
'class' => 'wc-block-active-filters',
|
||||
'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ) ),
|
||||
'data-wc-context' => wp_json_encode( $context ),
|
||||
'data-has-filter' => empty( $active_filters ) ? 'no' : 'yes',
|
||||
)
|
||||
);
|
||||
|
||||
$list_classes = 'filter-list';
|
||||
|
||||
if ( 'chips' === $attributes['displayStyle'] ) {
|
||||
$list_classes .= ' list-chips';
|
||||
}
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
|
||||
<div <?php echo $wrapper_attributes; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
|
||||
<?php if ( ! empty( $active_filters ) ) : ?>
|
||||
<ul class="wc-block-active-filters__list %3$s">
|
||||
<ul class="<?php echo esc_attr( $list_classes ); ?>">
|
||||
<?php foreach ( $active_filters as $filter ) : ?>
|
||||
<li>
|
||||
<span class="wc-block-active-filters__list-item-type"><?php echo esc_html( $filter['type'] ); ?>: </span>
|
||||
<span class="list-item-type"><?php echo esc_html( $filter['type'] ); ?>: </span>
|
||||
<ul>
|
||||
<?php $this->render_items( $filter['items'], $attributes['displayStyle'] ); ?>
|
||||
</ul>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<button class="wc-block-active-filters__clear-all" data-wc-on--click="actions.clearAll">
|
||||
<button class="clear-all" data-wc-on--click="actions.clearAll">
|
||||
<span aria-hidden="true"><?php echo esc_html__( 'Clear All', 'woocommerce' ); ?></span>
|
||||
<span class="screen-reader-text"><?php echo esc_html__( 'Clear All Filters', 'woocommerce' ); ?></span>
|
||||
</button>
|
||||
@@ -124,10 +130,10 @@ final class ProductFilterActive extends AbstractBlock {
|
||||
|
||||
$remove_label = sprintf( 'Remove %s filter', wp_strip_all_tags( $title ) );
|
||||
?>
|
||||
<li class="wc-block-active-filters__list-item">
|
||||
<span class="wc-block-active-filters__list-item-name">
|
||||
<li class="list-item">
|
||||
<span class="list-item-name">
|
||||
<?php // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
<button class="wc-block-active-filters__list-item-remove" <?php echo $this->get_html_attributes( $attributes ); ?>>
|
||||
<button class="list-item-remove" <?php echo $this->get_html_attributes( $attributes ); ?>>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" class="wc-block-components-chip__remove-icon" aria-hidden="true" focusable="false"><path d="M12 13.06l3.712 3.713 1.061-1.06L13.061 12l3.712-3.712-1.06-1.06L12 10.938 8.288 7.227l-1.061 1.06L10.939 12l-3.712 3.712 1.06 1.061L12 13.061z"></path></svg>
|
||||
<span class="screen-reader-text"><?php echo esc_html( $remove_label ); ?></span>
|
||||
</button>
|
||||
@@ -158,7 +164,7 @@ final class ProductFilterActive extends AbstractBlock {
|
||||
|
||||
$remove_label = sprintf( 'Remove %s filter', wp_strip_all_tags( $title ) );
|
||||
?>
|
||||
<li class="wc-block-active-filters__list-item">
|
||||
<li class="list-item">
|
||||
<span class="is-removable wc-block-components-chip wc-block-components-chip--radius-large">
|
||||
<span aria-hidden="false" class="wc-block-components-chip__text"><?php echo wp_kses_post( $title ); ?></span>
|
||||
<?php // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
<?php
|
||||
namespace Automattic\WooCommerce\Blocks\BlockTypes;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\QueryFilters;
|
||||
use Automattic\WooCommerce\Blocks\Package;
|
||||
use Automattic\WooCommerce\Blocks\InteractivityComponents\Dropdown;
|
||||
use Automattic\WooCommerce\Blocks\InteractivityComponents\CheckboxList;
|
||||
use Automattic\WooCommerce\Blocks\Utils\ProductCollectionUtils;
|
||||
|
||||
/**
|
||||
* Product Filter: Attribute Block.
|
||||
@@ -136,14 +139,7 @@ final class ProductFilterAttribute extends AbstractBlock {
|
||||
}
|
||||
|
||||
$product_attribute = wc_get_attribute( $attributes['attributeId'] );
|
||||
$attribute_counts = array_reduce(
|
||||
$block->context['collectionData']['attribute_counts'] ?? [],
|
||||
function( $acc, $count ) {
|
||||
$acc[ $count['term'] ] = $count['count'];
|
||||
return $acc;
|
||||
},
|
||||
[]
|
||||
);
|
||||
$attribute_counts = $this->get_attribute_counts( $block, $product_attribute->slug, $attributes['queryType'] );
|
||||
|
||||
if ( empty( $attribute_counts ) ) {
|
||||
return sprintf(
|
||||
@@ -151,6 +147,7 @@ final class ProductFilterAttribute extends AbstractBlock {
|
||||
get_block_wrapper_attributes(
|
||||
array(
|
||||
'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ) ),
|
||||
'data-has-filter' => 'no',
|
||||
)
|
||||
),
|
||||
);
|
||||
@@ -203,6 +200,7 @@ final class ProductFilterAttribute extends AbstractBlock {
|
||||
array(
|
||||
'data-wc-context' => wp_json_encode( $context ),
|
||||
'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ) ),
|
||||
'data-has-filter' => 'yes',
|
||||
)
|
||||
),
|
||||
$content,
|
||||
@@ -284,4 +282,51 @@ final class ProductFilterAttribute extends AbstractBlock {
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the attribute count for current block.
|
||||
*
|
||||
* @param WP_Block $block Block instance.
|
||||
* @param string $slug Attribute slug.
|
||||
* @param string $query_type Query type, accept 'and' or 'or'.
|
||||
*/
|
||||
private function get_attribute_counts( $block, $slug, $query_type ) {
|
||||
$filters = Package::container()->get( QueryFilters::class );
|
||||
$query_vars = ProductCollectionUtils::get_query_vars( $block, 1 );
|
||||
|
||||
if ( 'and' !== strtolower( $query_type ) ) {
|
||||
unset( $query_vars[ 'filter_' . str_replace( 'pa_', '', $slug ) ] );
|
||||
}
|
||||
|
||||
unset(
|
||||
$query_vars['taxonomy'],
|
||||
$query_vars['term']
|
||||
);
|
||||
|
||||
if ( ! empty( $query_vars['tax_query'] ) ) {
|
||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
|
||||
$query_vars['tax_query'] = ProductCollectionUtils::remove_query_array( $query_vars['tax_query'], 'taxonomy', $slug );
|
||||
}
|
||||
|
||||
$counts = $filters->get_attribute_counts( $query_vars, $slug );
|
||||
$attribute_counts = array();
|
||||
|
||||
foreach ( $counts as $key => $value ) {
|
||||
$attribute_counts[] = array(
|
||||
'term' => $key,
|
||||
'count' => $value,
|
||||
);
|
||||
}
|
||||
|
||||
$attribute_counts = array_reduce(
|
||||
$attribute_counts,
|
||||
function( $acc, $count ) {
|
||||
$acc[ $count['term'] ] = $count['count'];
|
||||
return $acc;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return $attribute_counts;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
<?php
|
||||
namespace Automattic\WooCommerce\Blocks\BlockTypes;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\Utils\ProductCollectionUtils;
|
||||
use Automattic\WooCommerce\Blocks\QueryFilters;
|
||||
use Automattic\WooCommerce\Blocks\Package;
|
||||
|
||||
/**
|
||||
* Product Filter: Price Block.
|
||||
*/
|
||||
@@ -116,7 +120,7 @@ final class ProductFilterPrice extends AbstractBlock {
|
||||
return '';
|
||||
}
|
||||
|
||||
$price_range = $block->context['collectionData']['price_range'] ?? [];
|
||||
$price_range = $this->get_filtered_price( $block );
|
||||
$min_range = $price_range['min_price'] ?? 0;
|
||||
$max_range = $price_range['max_price'] ?? 0;
|
||||
$min_price = intval( get_query_var( self::MIN_PRICE_QUERY_VAR, $min_range ) );
|
||||
@@ -136,18 +140,17 @@ final class ProductFilterPrice extends AbstractBlock {
|
||||
'inlineInput' => $inline_input
|
||||
) = $attributes;
|
||||
|
||||
$wrapper_attributes = get_block_wrapper_attributes(
|
||||
array(
|
||||
'class' => $show_input_fields && $inline_input ? 'inline-input' : '',
|
||||
'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ) ),
|
||||
'data-wc-context' => wp_json_encode( $data ),
|
||||
)
|
||||
$wrapper_attributes = array(
|
||||
'class' => $show_input_fields && $inline_input ? 'inline-input' : '',
|
||||
'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ) ),
|
||||
'data-wc-context' => wp_json_encode( $data ),
|
||||
'data-has-filter' => 'no',
|
||||
);
|
||||
|
||||
if ( $min_range === $max_range || ! $max_range ) {
|
||||
return sprintf(
|
||||
'<div %s></div>',
|
||||
$wrapper_attributes
|
||||
get_block_wrapper_attributes( $wrapper_attributes )
|
||||
);
|
||||
}
|
||||
|
||||
@@ -188,9 +191,11 @@ final class ProductFilterPrice extends AbstractBlock {
|
||||
$formatted_max_price
|
||||
);
|
||||
|
||||
$wrapper_attributes['data-has-filter'] = 'yes';
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
<div <?php echo $wrapper_attributes; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
|
||||
<div <?php echo get_block_wrapper_attributes( $wrapper_attributes ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
|
||||
<?php echo $content; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
<div class="filter-controls">
|
||||
<div
|
||||
@@ -234,4 +239,28 @@ final class ProductFilterPrice extends AbstractBlock {
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the price filter data for current block.
|
||||
*
|
||||
* @param WP_Block $block Block instance.
|
||||
*/
|
||||
private function get_filtered_price( $block ) {
|
||||
$filters = Package::container()->get( QueryFilters::class );
|
||||
$query_vars = ProductCollectionUtils::get_query_vars( $block, 1 );
|
||||
|
||||
unset( $query_vars['min_price'], $query_vars['max_price'] );
|
||||
|
||||
if ( ! empty( $query_vars['meta_query'] ) ) {
|
||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
$query_vars['meta_query'] = ProductCollectionUtils::remove_query_array( $query_vars['meta_query'], 'key', '_price' );
|
||||
}
|
||||
|
||||
$price_results = $filters->get_filtered_price( $query_vars );
|
||||
|
||||
return array(
|
||||
'min_price' => intval( floor( $price_results->min_price ?? 0 ) ),
|
||||
'max_price' => intval( ceil( $price_results->max_price ?? 0 ) ),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,10 @@ namespace Automattic\WooCommerce\Blocks\BlockTypes;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\InteractivityComponents\CheckboxList;
|
||||
use Automattic\WooCommerce\Blocks\InteractivityComponents\Dropdown;
|
||||
use Automattic\WooCommerce\Blocks\Utils\ProductCollectionUtils;
|
||||
use Automattic\WooCommerce\Blocks\QueryFilters;
|
||||
use Automattic\WooCommerce\Blocks\Package;
|
||||
|
||||
|
||||
/**
|
||||
* Product Filter: Rating Block
|
||||
@@ -41,7 +45,7 @@ final class ProductFilterRating extends AbstractBlock {
|
||||
* @return array Active filters param keys.
|
||||
*/
|
||||
public function get_filter_query_param_keys( $filter_param_keys, $url_param_keys ) {
|
||||
$price_param_keys = array_filter(
|
||||
$rating_param_keys = array_filter(
|
||||
$url_param_keys,
|
||||
function( $param ) {
|
||||
return self::RATING_FILTER_QUERY_VAR === $param;
|
||||
@@ -50,7 +54,7 @@ final class ProductFilterRating extends AbstractBlock {
|
||||
|
||||
return array_merge(
|
||||
$filter_param_keys,
|
||||
$price_param_keys
|
||||
$rating_param_keys
|
||||
);
|
||||
}
|
||||
|
||||
@@ -110,17 +114,10 @@ final class ProductFilterRating extends AbstractBlock {
|
||||
return '';
|
||||
}
|
||||
|
||||
$rating_counts = $block->context['collectionData']['rating_counts'] ?? array();
|
||||
$rating_counts = $this->get_rating_counts( $block );
|
||||
$display_style = $attributes['displayStyle'] ?? 'list';
|
||||
$show_counts = $attributes['showCounts'] ?? false;
|
||||
|
||||
$wrapper_attributes = get_block_wrapper_attributes(
|
||||
array(
|
||||
'data-wc-interactive' => $this->get_full_block_name(),
|
||||
'class' => 'wc-block-rating-filter',
|
||||
)
|
||||
);
|
||||
|
||||
$filtered_rating_counts = array_filter(
|
||||
$rating_counts,
|
||||
function( $rating ) {
|
||||
@@ -128,6 +125,13 @@ final class ProductFilterRating extends AbstractBlock {
|
||||
}
|
||||
);
|
||||
|
||||
$wrapper_attributes = get_block_wrapper_attributes(
|
||||
array(
|
||||
'data-wc-interactive' => $this->get_full_block_name(),
|
||||
'data-has-filter' => empty( $filtered_rating_counts ) ? 'no' : 'yes',
|
||||
)
|
||||
);
|
||||
|
||||
if ( empty( $filtered_rating_counts ) ) {
|
||||
return sprintf(
|
||||
'<div %s></div>',
|
||||
@@ -150,11 +154,8 @@ final class ProductFilterRating extends AbstractBlock {
|
||||
return sprintf(
|
||||
'<div %1$s>
|
||||
%2$s
|
||||
<div class="wc-block-rating-filter__controls">%3$s</div>
|
||||
<div class="wc-block-rating-filter__actions"></div>
|
||||
</div>',
|
||||
$wrapper_attributes,
|
||||
$content,
|
||||
$input
|
||||
);
|
||||
}
|
||||
@@ -207,11 +208,18 @@ final class ProductFilterRating extends AbstractBlock {
|
||||
$count = $rating['count'];
|
||||
$count_label = $show_counts ? "($count)" : '';
|
||||
|
||||
$aria_label = sprintf(
|
||||
/* translators: %1$d is referring to rating value. Example: Rated 4 out of 5. */
|
||||
__( 'Rated %s out of 5', 'woocommerce' ),
|
||||
$rating_str,
|
||||
);
|
||||
|
||||
return array(
|
||||
'id' => 'rating-' . $rating_str,
|
||||
'checked' => in_array( $rating_str, $ratings_array, true ),
|
||||
'label' => $this->render_rating_label( (int) $rating_str, $count_label ),
|
||||
'value' => $rating_str,
|
||||
'id' => 'rating-' . $rating_str,
|
||||
'checked' => in_array( $rating_str, $ratings_array, true ),
|
||||
'label' => $this->render_rating_label( (int) $rating_str, $count_label ),
|
||||
'aria_label' => $aria_label,
|
||||
'value' => $rating_str,
|
||||
);
|
||||
},
|
||||
$rating_counts
|
||||
@@ -269,4 +277,31 @@ final class ProductFilterRating extends AbstractBlock {
|
||||
'placeholder' => $placeholder_text,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the rating filter data for current block.
|
||||
*
|
||||
* @param WP_Block $block Block instance.
|
||||
*/
|
||||
private function get_rating_counts( $block ) {
|
||||
$filters = Package::container()->get( QueryFilters::class );
|
||||
$query_vars = ProductCollectionUtils::get_query_vars( $block, 1 );
|
||||
|
||||
if ( ! empty( $query_vars['tax_query'] ) ) {
|
||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
|
||||
$query_vars['tax_query'] = ProductCollectionUtils::remove_query_array( $query_vars['tax_query'], 'rating_filter', true );
|
||||
}
|
||||
|
||||
$counts = $filters->get_rating_counts( $query_vars );
|
||||
$data = array();
|
||||
|
||||
foreach ( $counts as $key => $value ) {
|
||||
$data[] = array(
|
||||
'rating' => $key,
|
||||
'count' => $value,
|
||||
);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@ namespace Automattic\WooCommerce\Blocks\BlockTypes;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\InteractivityComponents\Dropdown;
|
||||
use Automattic\WooCommerce\Blocks\InteractivityComponents\CheckboxList;
|
||||
use Automattic\WooCommerce\Blocks\Utils\ProductCollectionUtils;
|
||||
use Automattic\WooCommerce\Blocks\QueryFilters;
|
||||
use Automattic\WooCommerce\Blocks\Package;
|
||||
|
||||
/**
|
||||
* Product Filter: Stock Status Block.
|
||||
@@ -125,17 +128,16 @@ final class ProductFilterStockStatus extends AbstractBlock {
|
||||
return '';
|
||||
}
|
||||
|
||||
$stock_status_counts = $block->context['collectionData']['stock_status_counts'] ?? [];
|
||||
$wrapper_attributes = get_block_wrapper_attributes();
|
||||
$stock_status_counts = $this->get_stock_status_counts( $block );
|
||||
$wrapper_attributes = get_block_wrapper_attributes(
|
||||
array(
|
||||
'data-has-filter' => empty( $stock_status_counts ) ? 'no' : 'yes',
|
||||
)
|
||||
);
|
||||
|
||||
return sprintf(
|
||||
'<div %1$s>
|
||||
%2$s
|
||||
<div class="wc-block-stock-filter__controls">%3$s</div>
|
||||
<div class="wc-block-stock-filter__actions"></div>
|
||||
</div>',
|
||||
'<div %1$s>%2$s</div>',
|
||||
$wrapper_attributes,
|
||||
$content,
|
||||
$this->get_stock_filter_html( $stock_status_counts, $attributes ),
|
||||
);
|
||||
}
|
||||
@@ -148,6 +150,10 @@ final class ProductFilterStockStatus extends AbstractBlock {
|
||||
* @return string Rendered block type output.
|
||||
*/
|
||||
private function get_stock_filter_html( $stock_counts, $attributes ) {
|
||||
if ( empty( $stock_counts ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$display_style = $attributes['displayStyle'] ?? 'list';
|
||||
$show_counts = $attributes['showCounts'] ?? false;
|
||||
$select_type = $attributes['selectType'] ?? 'single';
|
||||
@@ -160,17 +166,6 @@ final class ProductFilterStockStatus extends AbstractBlock {
|
||||
$query = isset( $_GET[ self::STOCK_STATUS_QUERY_VAR ] ) ? sanitize_text_field( wp_unslash( $_GET[ self::STOCK_STATUS_QUERY_VAR ] ) ) : '';
|
||||
$selected_stock_statuses = explode( ',', $query );
|
||||
|
||||
$filtered_stock_counts = array_filter(
|
||||
$stock_counts,
|
||||
function( $stock_count ) {
|
||||
return $stock_count['count'] > 0;
|
||||
}
|
||||
);
|
||||
|
||||
if ( empty( $filtered_stock_counts ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$list_items = array_values(
|
||||
array_map(
|
||||
function( $item ) use ( $stock_statuses, $show_counts, $selected_stock_statuses ) {
|
||||
@@ -181,7 +176,7 @@ final class ProductFilterStockStatus extends AbstractBlock {
|
||||
'checked' => in_array( $item['status'], $selected_stock_statuses, true ),
|
||||
);
|
||||
},
|
||||
$filtered_stock_counts
|
||||
$stock_counts
|
||||
)
|
||||
);
|
||||
|
||||
@@ -231,4 +226,38 @@ final class ProductFilterStockStatus extends AbstractBlock {
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the stock status filter data for current block.
|
||||
*
|
||||
* @param WP_Block $block Block instance.
|
||||
*/
|
||||
private function get_stock_status_counts( $block ) {
|
||||
$filters = Package::container()->get( QueryFilters::class );
|
||||
$query_vars = ProductCollectionUtils::get_query_vars( $block, 1 );
|
||||
|
||||
unset( $query_vars['filter_stock_status'] );
|
||||
|
||||
if ( ! empty( $query_vars['meta_query'] ) ) {
|
||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
$query_vars['meta_query'] = ProductCollectionUtils::remove_query_array( $query_vars['meta_query'], 'key', '_stock_status' );
|
||||
}
|
||||
|
||||
$counts = $filters->get_stock_status_counts( $query_vars );
|
||||
$data = array();
|
||||
|
||||
foreach ( $counts as $key => $value ) {
|
||||
$data[] = array(
|
||||
'status' => $key,
|
||||
'count' => $value,
|
||||
);
|
||||
}
|
||||
|
||||
return array_filter(
|
||||
$data,
|
||||
function( $stock_count ) {
|
||||
return $stock_count['count'] > 0;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,25 +77,25 @@ class ProductGallery extends AbstractBlock {
|
||||
|
||||
$gallery_dialog = strtr(
|
||||
'
|
||||
<div class="wc-block-product-gallery-dialog__overlay" hidden data-wc-bind--hidden="!context.isDialogOpen" data-wc-watch="callbacks.keyboardAccess">
|
||||
<dialog data-wc-bind--open="context.isDialogOpen">
|
||||
<div class="wc-block-product-gallery-dialog__header">
|
||||
<div class="wc-block-product-galler-dialog__header-right">
|
||||
<button class="wc-block-product-gallery-dialog__close" data-wc-on--click="actions.closeDialog">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="24" height="24" rx="2"/>
|
||||
<path d="M13 11.8L19.1 5.5L18.1 4.5L12 10.7L5.9 4.5L4.9 5.5L11 11.8L4.5 18.5L5.5 19.5L12 12.9L18.5 19.5L19.5 18.5L13 11.8Z" fill="black"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wc-block-product-gallery-dialog__body">
|
||||
{{html}}
|
||||
</div>
|
||||
</dialog>
|
||||
</div>',
|
||||
<dialog data-wc-bind--open="context.isDialogOpen" role="dialog" aria-modal="true" aria-label="{{dialog_aria_label}}" hidden data-wc-bind--hidden="!context.isDialogOpen" data-wc-watch="callbacks.keyboardAccess" data-wc-watch--dialog-focus-trap="callbacks.dialogFocusTrap" data-wc-class--wc-block-product-gallery--dialog-open="context.isDialogOpen">
|
||||
<div class="wc-block-product-gallery-dialog__header">
|
||||
<div class="wc-block-product-galler-dialog__header-right">
|
||||
<button class="wc-block-product-gallery-dialog__close" data-wc-on--click="actions.closeDialog" aria-label="{{close_dialog_aria_label}}">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="24" height="24" rx="2"/>
|
||||
<path d="M13 11.8L19.1 5.5L18.1 4.5L12 10.7L5.9 4.5L4.9 5.5L11 11.8L4.5 18.5L5.5 19.5L12 12.9L18.5 19.5L19.5 18.5L13 11.8Z" fill="black"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wc-block-product-gallery-dialog__body">
|
||||
{{html}}
|
||||
</div>
|
||||
</dialog>',
|
||||
array(
|
||||
'{{html}}' => $html_processor->get_updated_html(),
|
||||
'{{html}}' => $html_processor->get_updated_html(),
|
||||
'{{dialog_aria_label}}' => __( 'Product gallery', 'woocommerce' ),
|
||||
'{{close_dialog_aria_label}}' => __( 'Close Product Gallery dialog', 'woocommerce' ),
|
||||
)
|
||||
);
|
||||
return $gallery_dialog;
|
||||
@@ -146,6 +146,7 @@ class ProductGallery extends AbstractBlock {
|
||||
'dialogVisibleImagesIds' => ProductGalleryUtils::get_product_gallery_image_ids( $product, null, false ),
|
||||
'mouseIsOverPreviousOrNextButton' => false,
|
||||
'productId' => $product_id,
|
||||
'elementThatTriggeredDialogOpening' => null,
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
@@ -87,7 +87,7 @@ class ProductGalleryLargeImage extends AbstractBlock {
|
||||
|
||||
return strtr(
|
||||
'<div class="wc-block-product-gallery-large-image wp-block-woocommerce-product-gallery-large-image" {directives}>
|
||||
<ul class="wc-block-product-gallery-large-image__container">
|
||||
<ul class="wc-block-product-gallery-large-image__container" tabindex="-1">
|
||||
{main_images}
|
||||
</ul>
|
||||
{content}
|
||||
@@ -117,9 +117,11 @@ class ProductGalleryLargeImage extends AbstractBlock {
|
||||
*/
|
||||
private function get_main_images_html( $context, $product_id ) {
|
||||
$attributes = array(
|
||||
'class' => 'wc-block-woocommerce-product-gallery-large-image__image',
|
||||
'data-wc-bind--hidden' => '!state.isSelected',
|
||||
'data-wc-watch' => 'callbacks.scrollInto',
|
||||
'class' => 'wc-block-woocommerce-product-gallery-large-image__image',
|
||||
'data-wc-bind--hidden' => '!state.isSelected',
|
||||
'data-wc-watch' => 'callbacks.scrollInto',
|
||||
'data-wc-bind--tabindex' => 'state.thumbnailTabIndex',
|
||||
'data-wc-on--keydown' => 'actions.onSelectedLargeImageKeyDown',
|
||||
'data-wc-class--wc-block-woocommerce-product-gallery-large-image__image--active-image-slide' => 'state.isSelected',
|
||||
);
|
||||
|
||||
|
||||
@@ -89,6 +89,10 @@ class ProductGalleryLargeImageNextPrevious extends AbstractBlock {
|
||||
'data-wc-on--click',
|
||||
'actions.selectPreviousImage'
|
||||
);
|
||||
$p->set_attribute(
|
||||
'aria-label',
|
||||
__( 'Previous image', 'woocommerce' )
|
||||
);
|
||||
$prev_button = $p->get_updated_html();
|
||||
}
|
||||
|
||||
@@ -100,6 +104,10 @@ class ProductGalleryLargeImageNextPrevious extends AbstractBlock {
|
||||
'data-wc-on--click',
|
||||
'actions.selectNextImage'
|
||||
);
|
||||
$p->set_attribute(
|
||||
'aria-label',
|
||||
__( 'Next image', 'woocommerce' )
|
||||
);
|
||||
$next_button = $p->get_updated_html();
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ class ProductGalleryThumbnails extends AbstractBlock {
|
||||
* @return string
|
||||
*/
|
||||
protected function generate_view_all_html( $remaining_thumbnails_count ) {
|
||||
$view_all_html = '<div class="wc-block-product-gallery-thumbnails__thumbnail__overlay wc-block-product-gallery-dialog-on-click" data-wc-on--click="actions.openDialog">
|
||||
$view_all_html = '<div class="wc-block-product-gallery-thumbnails__thumbnail__overlay wc-block-product-gallery-dialog-on-click" data-wc-on--click="actions.openDialog" data-wc-on--keydown="actions.onViewAllImagesKeyDown" tabindex="0">
|
||||
<span class="wc-block-product-gallery-thumbnails__thumbnail__remaining-thumbnails-count wc-block-product-gallery-dialog-on-click">+%1$s</span>
|
||||
<span class="wc-block-product-gallery-thumbnails__thumbnail__view-all wc-block-product-gallery-dialog-on-click">%2$s</span>
|
||||
</div>';
|
||||
@@ -156,6 +156,9 @@ class ProductGalleryThumbnails extends AbstractBlock {
|
||||
$processor = new \WP_HTML_Tag_Processor( $product_gallery_image_html );
|
||||
|
||||
if ( $processor->next_tag( 'img' ) ) {
|
||||
|
||||
$processor->set_attribute( 'data-wc-on--keydown', 'actions.onThumbnailKeyDown' );
|
||||
$processor->set_attribute( 'tabindex', '0' );
|
||||
$processor->set_attribute(
|
||||
'data-wc-on--click',
|
||||
'actions.selectImage'
|
||||
|
||||
@@ -176,6 +176,13 @@ class ProductQuery extends AbstractBlock {
|
||||
$this->parsed_block = $parsed_block;
|
||||
|
||||
if ( self::is_woocommerce_variation( $parsed_block ) ) {
|
||||
// Indicate to interactivity powered components that this block is on the page
|
||||
// and needs refresh to update data.
|
||||
$this->asset_data_registry->add(
|
||||
'needsRefreshForInteractivityAPI',
|
||||
true,
|
||||
true
|
||||
);
|
||||
// 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 );
|
||||
@@ -941,7 +948,7 @@ class ProductQuery extends AbstractBlock {
|
||||
* Get an array of taxonomy names associated with the "product" post type because
|
||||
* we also want to include custom taxonomies associated with the "product" post type.
|
||||
*/
|
||||
$product_taxonomies = get_taxonomies( array( 'object_type' => array( 'product' ) ), 'names' );
|
||||
$product_taxonomies = array_diff( get_object_taxonomies( 'product', 'names' ), array( 'product_visibility', 'product_shipping_class' ) );
|
||||
$result = array_filter(
|
||||
$tax_query,
|
||||
function( $item ) use ( $product_taxonomies ) {
|
||||
|
||||
@@ -97,8 +97,13 @@ class ProductSaleBadge extends AbstractBlock {
|
||||
return $content;
|
||||
}
|
||||
|
||||
$post_id = $block->context['postId'];
|
||||
$product = wc_get_product( $post_id );
|
||||
$post_id = $block->context['postId'];
|
||||
$product = wc_get_product( $post_id );
|
||||
|
||||
if ( ! $product ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$is_on_sale = $product->is_on_sale();
|
||||
|
||||
if ( ! $is_on_sale ) {
|
||||
|
||||
@@ -84,8 +84,13 @@ class ProductStockIndicator extends AbstractBlock {
|
||||
return $content;
|
||||
}
|
||||
|
||||
$post_id = $block->context['postId'];
|
||||
$product = wc_get_product( $post_id );
|
||||
$post_id = $block->context['postId'];
|
||||
$product = wc_get_product( $post_id );
|
||||
|
||||
if ( ! $product ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$is_in_stock = $product->is_in_stock();
|
||||
$is_on_backorder = $product->is_on_backorder();
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ use Automattic\WooCommerce\Blocks\Domain\Services\GoogleAnalytics;
|
||||
use Automattic\WooCommerce\Blocks\Domain\Services\Hydration;
|
||||
use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields;
|
||||
use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFieldsAdmin;
|
||||
use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFieldsFrontend;
|
||||
use Automattic\WooCommerce\Blocks\InboxNotifications;
|
||||
use Automattic\WooCommerce\Blocks\Installer;
|
||||
use Automattic\WooCommerce\Blocks\Migration;
|
||||
@@ -132,7 +133,7 @@ class Bootstrap {
|
||||
$this->container->get( CreateAccount::class )->init();
|
||||
$this->container->get( ShippingController::class )->init();
|
||||
$this->container->get( TasksController::class )->init();
|
||||
$this->container->get( CheckoutFields::class );
|
||||
$this->container->get( CheckoutFields::class )->init();
|
||||
|
||||
// Load assets in admin and on the frontend.
|
||||
if ( ! $is_rest ) {
|
||||
@@ -141,8 +142,7 @@ class Bootstrap {
|
||||
$this->container->get( AssetsController::class );
|
||||
$this->container->get( Installer::class )->init();
|
||||
$this->container->get( GoogleAnalytics::class )->init();
|
||||
$this->container->get( CheckoutFields::class )->init();
|
||||
$this->container->get( CheckoutFieldsAdmin::class )->init();
|
||||
$this->container->get( is_admin() ? CheckoutFieldsAdmin::class : CheckoutFieldsFrontend::class )->init();
|
||||
}
|
||||
|
||||
// Load assets unless this is a request specifically for the store API.
|
||||
@@ -362,6 +362,13 @@ class Bootstrap {
|
||||
return new CheckoutFieldsAdmin( $checkout_fields_controller );
|
||||
}
|
||||
);
|
||||
$this->container->register(
|
||||
CheckoutFieldsFrontend::class,
|
||||
function( Container $container ) {
|
||||
$checkout_fields_controller = $container->get( CheckoutFields::class );
|
||||
return new CheckoutFieldsFrontend( $checkout_fields_controller );
|
||||
}
|
||||
);
|
||||
$this->container->register(
|
||||
PaymentsApi::class,
|
||||
function ( Container $container ) {
|
||||
|
||||
@@ -4,6 +4,8 @@ namespace Automattic\WooCommerce\Blocks\Domain\Services;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry;
|
||||
use WC_Customer;
|
||||
use WC_Order;
|
||||
use WP_Error;
|
||||
|
||||
/**
|
||||
* Service class managing checkout fields and its related extensibility points.
|
||||
@@ -219,11 +221,12 @@ class CheckoutFields {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize hooks. This is not run Store API requests.
|
||||
* Initialize hooks.
|
||||
*/
|
||||
public function init() {
|
||||
add_action( 'woocommerce_blocks_checkout_enqueue_data', array( $this, 'add_fields_data' ) );
|
||||
add_action( 'woocommerce_blocks_cart_enqueue_data', array( $this, 'add_fields_data' ) );
|
||||
add_filter( 'woocommerce_customer_allowed_session_meta_keys', array( $this, 'add_session_meta_keys' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -234,63 +237,100 @@ class CheckoutFields {
|
||||
$this->asset_data_registry->add( 'addressFieldsLocations', $this->fields_locations, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add session meta keys.
|
||||
*
|
||||
* This is an allow-list of meta data keys which we want to store in session.
|
||||
*
|
||||
* @param array $keys Session meta keys.
|
||||
* @return array
|
||||
*/
|
||||
public function add_session_meta_keys( $keys ) {
|
||||
return array_merge( $keys, array( self::BILLING_FIELDS_KEY, self::SHIPPING_FIELDS_KEY, self::ADDITIONAL_FIELDS_KEY ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* If a field does not declare a sanitization callback, this is the default sanitization callback.
|
||||
*
|
||||
* @param mixed $value Value to sanitize.
|
||||
* @param array $field Field data.
|
||||
* @return mixed
|
||||
*/
|
||||
public function default_sanitize_callback( $value, $field ) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* If a field does not declare a validation callback, this is the default validation callback.
|
||||
*
|
||||
* @param mixed $value Value to sanitize.
|
||||
* @param array $field Field data.
|
||||
* @return WP_Error|void If there is a validation error, return an WP_Error object.
|
||||
*/
|
||||
public function default_validate_callback( $value, $field ) {
|
||||
if ( ! empty( $field['required'] ) && empty( $value ) ) {
|
||||
return new WP_Error(
|
||||
'woocommerce_blocks_checkout_field_required',
|
||||
sprintf(
|
||||
// translators: %s is field key.
|
||||
__( 'The field %s is required.', 'woocommerce' ),
|
||||
$field['id']
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an additional field for Checkout.
|
||||
*
|
||||
* @param array $options The field options.
|
||||
*
|
||||
* @return \WP_Error|void True if the field was registered, a WP_Error otherwise.
|
||||
* @return WP_Error|void True if the field was registered, a WP_Error otherwise.
|
||||
*/
|
||||
public function register_checkout_field( $options ) {
|
||||
|
||||
// Check the options and show warnings if they're not supplied. Return early if an error that would prevent registration is encountered.
|
||||
$result = $this->validate_options( $options );
|
||||
if ( false === $result ) {
|
||||
if ( false === $this->validate_options( $options ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The above validate_options function ensures these options are valid. Type might not be supplied but then it defaults to text.
|
||||
$id = $options['id'];
|
||||
$location = $options['location'];
|
||||
$type = $options['type'] ?? 'text';
|
||||
|
||||
$field_data = array(
|
||||
'label' => $options['label'],
|
||||
'hidden' => false,
|
||||
'type' => $type,
|
||||
'optionalLabel' => empty( $options['optionalLabel'] ) ? sprintf(
|
||||
/* translators: %s Field label. */
|
||||
__( '%s (optional)', 'woocommerce' ),
|
||||
$options['label']
|
||||
) : $options['optionalLabel'],
|
||||
'required' => empty( $options['required'] ) ? false : $options['required'],
|
||||
$field_data = wp_parse_args(
|
||||
$options,
|
||||
array(
|
||||
'id' => '',
|
||||
'label' => '',
|
||||
'optionalLabel' => sprintf(
|
||||
/* translators: %s Field label. */
|
||||
__( '%s (optional)', 'woocommerce' ),
|
||||
$options['label']
|
||||
),
|
||||
'location' => '',
|
||||
'type' => 'text',
|
||||
'hidden' => false,
|
||||
'required' => false,
|
||||
'attributes' => array(),
|
||||
'show_in_order_confirmation' => true,
|
||||
'sanitize_callback' => array( $this, 'default_sanitize_callback' ),
|
||||
'validate_callback' => array( $this, 'default_validate_callback' ),
|
||||
)
|
||||
);
|
||||
|
||||
$field_data['attributes'] = $this->register_field_attributes( $id, $options['attributes'] ?? [] );
|
||||
$field_data['attributes'] = $this->register_field_attributes( $field_data['id'], $field_data['attributes'] );
|
||||
|
||||
if ( 'checkbox' === $type ) {
|
||||
$result = $this->process_checkbox_field( $options, $field_data );
|
||||
|
||||
// $result will be false if an error that will prevent the field being registered is encountered.
|
||||
if ( false === $result ) {
|
||||
return;
|
||||
}
|
||||
$field_data = $result;
|
||||
if ( 'checkbox' === $field_data['type'] ) {
|
||||
$field_data = $this->process_checkbox_field( $field_data, $options );
|
||||
} elseif ( 'select' === $field_data['type'] ) {
|
||||
$field_data = $this->process_select_field( $field_data, $options );
|
||||
}
|
||||
|
||||
if ( 'select' === $type ) {
|
||||
$result = $this->process_select_field( $options, $field_data );
|
||||
|
||||
// $result will be false if an error that will prevent the field being registered is encountered.
|
||||
if ( false === $result ) {
|
||||
return;
|
||||
}
|
||||
$field_data = $result;
|
||||
// $field_data will be false if an error that will prevent the field being registered is encountered.
|
||||
if ( false === $field_data ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Insert new field into the correct location array.
|
||||
$this->additional_fields[ $id ] = $field_data;
|
||||
$this->fields_locations[ $location ][] = $id;
|
||||
$this->additional_fields[ $field_data['id'] ] = $field_data;
|
||||
$this->fields_locations[ $field_data['location'] ][] = $field_data['id'];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -301,32 +341,32 @@ class CheckoutFields {
|
||||
*/
|
||||
private function validate_options( $options ) {
|
||||
if ( empty( $options['id'] ) ) {
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', 'A checkout field cannot be registered without an id.', '8.6.0' );
|
||||
_doing_it_wrong( '__experimental_woocommerce_blocks_register_checkout_field', 'A checkout field cannot be registered without an id.', '8.6.0' );
|
||||
return false;
|
||||
}
|
||||
|
||||
// Having fewer than 2 after exploding around a / means there is no namespace.
|
||||
if ( count( explode( '/', $options['id'] ) ) < 2 ) {
|
||||
$message = sprintf( 'Unable to register field with id: "%s". %s', $options['id'], 'A checkout field id must consist of namespace/name.' );
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
_doing_it_wrong( '__experimental_woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( empty( $options['label'] ) ) {
|
||||
$message = sprintf( 'Unable to register field with id: "%s". %s', $options['id'], 'The field label is required.' );
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
_doing_it_wrong( '__experimental_woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( empty( $options['location'] ) ) {
|
||||
$message = sprintf( 'Unable to register field with id: "%s". %s', $options['id'], 'The field location is required.' );
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
_doing_it_wrong( '__experimental_woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! in_array( $options['location'], array_keys( $this->fields_locations ), true ) ) {
|
||||
$message = sprintf( 'Unable to register field with id: "%s". %s', $options['id'], 'The field location is invalid.' );
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
_doing_it_wrong( '__experimental_woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -337,14 +377,14 @@ class CheckoutFields {
|
||||
// Check to see if field is already in the array.
|
||||
if ( ! empty( $this->additional_fields[ $id ] ) || in_array( $id, $this->fields_locations[ $location ], true ) ) {
|
||||
$message = sprintf( 'Unable to register field with id: "%s". %s', $id, 'The field is already registered.' );
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
_doing_it_wrong( '__experimental_woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
return false;
|
||||
}
|
||||
|
||||
// Hidden fields are not supported right now. They will be registered with hidden => false.
|
||||
if ( ! empty( $options['hidden'] ) && true === $options['hidden'] ) {
|
||||
$message = sprintf( 'Registering a field with hidden set to true is not supported. The field "%s" will be registered as visible.', $id );
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
_doing_it_wrong( '__experimental_woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
// Don't return here unlike the other fields because this is not an issue that will prevent registration.
|
||||
}
|
||||
|
||||
@@ -356,38 +396,42 @@ class CheckoutFields {
|
||||
$options['type'],
|
||||
implode( ', ', $this->supported_field_types )
|
||||
);
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
_doing_it_wrong( '__experimental_woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! empty( $options['sanitize_callback'] ) && ! is_callable( $options['sanitize_callback'] ) ) {
|
||||
$message = sprintf( 'Unable to register field with id: "%s". %s', $id, 'The sanitize_callback must be a valid callback.' );
|
||||
_doing_it_wrong( '__experimental_woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! empty( $options['validate_callback'] ) && ! is_callable( $options['validate_callback'] ) ) {
|
||||
$message = sprintf( 'Unable to register field with id: "%s". %s', $id, 'The validate_callback must be a valid callback.' );
|
||||
_doing_it_wrong( '__experimental_woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the options for a select field and returns the new field_options array.
|
||||
*
|
||||
* @param array $options The options supplied during field registration.
|
||||
* @param array $field_data The field data array to be updated.
|
||||
* @param array $options The options supplied during field registration.
|
||||
*
|
||||
* @return array|false The updated $field_data array or false if an error was encountered.
|
||||
*/
|
||||
private function process_select_field( $options, $field_data ) {
|
||||
private function process_select_field( $field_data, $options ) {
|
||||
$id = $options['id'];
|
||||
|
||||
if ( empty( $options['options'] ) || ! is_array( $options['options'] ) ) {
|
||||
$message = sprintf( 'Unable to register field with id: "%s". %s', $id, 'Fields of type "select" must have an array of "options".' );
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
_doing_it_wrong( '__experimental_woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
return false;
|
||||
}
|
||||
|
||||
// Select fields are always required. Log a warning if it's set explicitly as false.
|
||||
$field_data['required'] = true;
|
||||
if ( isset( $options['required'] ) && false === $options['required'] ) {
|
||||
$message = sprintf( 'Registering select fields as optional is not supported. "%s" will be registered as required.', $id );
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
}
|
||||
|
||||
$cleaned_options = array();
|
||||
$added_values = array();
|
||||
|
||||
@@ -395,7 +439,7 @@ class CheckoutFields {
|
||||
foreach ( $options['options'] as $option ) {
|
||||
if ( ! isset( $option['value'] ) || ! isset( $option['label'] ) ) {
|
||||
$message = sprintf( 'Unable to register field with id: "%s". %s', $id, 'Fields of type "select" must have an array of "options" and each option must contain a "value" and "label" member.' );
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
_doing_it_wrong( '__experimental_woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -404,7 +448,7 @@ class CheckoutFields {
|
||||
|
||||
if ( in_array( $sanitized_value, $added_values, true ) ) {
|
||||
$message = sprintf( 'Duplicate key found when registering field with id: "%s". The value in each option of "select" fields must be unique. Duplicate value "%s" found. The duplicate key will be removed.', $id, $sanitized_value );
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
_doing_it_wrong( '__experimental_woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -417,18 +461,32 @@ class CheckoutFields {
|
||||
}
|
||||
|
||||
$field_data['options'] = $cleaned_options;
|
||||
|
||||
// If the field is not required, inject an empty option at the start.
|
||||
if ( isset( $field_data['required'] ) && false === $field_data['required'] && ! in_array( '', $added_values, true ) ) {
|
||||
$field_data['options'] = array_merge(
|
||||
array(
|
||||
array(
|
||||
'value' => '',
|
||||
'label' => '',
|
||||
),
|
||||
),
|
||||
$field_data['options']
|
||||
);
|
||||
}
|
||||
|
||||
return $field_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the options for a checkbox field and returns the new field_options array.
|
||||
*
|
||||
* @param array $options The options supplied during field registration.
|
||||
* @param array $field_data The field data array to be updated.
|
||||
* @param array $options The options supplied during field registration.
|
||||
*
|
||||
* @return array|false The updated $field_data array or false if an error was encountered.
|
||||
*/
|
||||
private function process_checkbox_field( $options, $field_data ) {
|
||||
private function process_checkbox_field( $field_data, $options ) {
|
||||
$id = $options['id'];
|
||||
|
||||
// Checkbox fields are always optional. Log a warning if it's set explicitly as true.
|
||||
@@ -436,7 +494,7 @@ class CheckoutFields {
|
||||
|
||||
if ( isset( $options['required'] ) && true === $options['required'] ) {
|
||||
$message = sprintf( 'Registering checkbox fields as required is not supported. "%s" will be registered as optional.', $id );
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
_doing_it_wrong( '__experimental_woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
}
|
||||
|
||||
return $field_data;
|
||||
@@ -451,18 +509,15 @@ class CheckoutFields {
|
||||
* @return array The processed attributes.
|
||||
*/
|
||||
private function register_field_attributes( $id, $attributes ) {
|
||||
|
||||
// We check if attributes are valid. This is done to prevent too much nesting and also to allow field registration
|
||||
// even if the attributes property is invalid. We can just skip it and register the field without attributes.
|
||||
$has_attributes = false;
|
||||
|
||||
if ( empty( $attributes ) ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if ( ! is_array( $attributes ) || 0 === count( $attributes ) ) {
|
||||
$message = sprintf( 'An invalid attributes value was supplied when registering field with id: "%s". %s', $id, 'Attributes must be a non-empty array.' );
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
_doing_it_wrong( '__experimental_woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -488,7 +543,7 @@ class CheckoutFields {
|
||||
if ( count( $attributes ) !== count( $valid_attributes ) ) {
|
||||
$invalid_attributes = array_keys( array_diff_key( $attributes, $valid_attributes ) );
|
||||
$message = sprintf( 'Invalid attribute found when registering field with id: "%s". Attributes: %s are not allowed.', $id, implode( ', ', $invalid_attributes ) );
|
||||
_doing_it_wrong( 'woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
_doing_it_wrong( '__experimental_woocommerce_blocks_register_checkout_field', esc_html( $message ), '8.6.0' );
|
||||
}
|
||||
|
||||
// Escape attributes to remove any malicious code and return them.
|
||||
@@ -534,73 +589,97 @@ class CheckoutFields {
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate an additional field against any custom validation rules. The result should be a WP_Error or true.
|
||||
* Sanitize an additional field against any custom sanitization rules.
|
||||
*
|
||||
* @param string $key The key of the field.
|
||||
* @param mixed $field_value The value of the field.
|
||||
* @param \WP_REST_Request $request The current API Request.
|
||||
* @param string|null $address_type The type of address (billing, shipping, or null if the field is a contact/additional field).
|
||||
* @since 8.7.0
|
||||
|
||||
* @param string $field_key The key of the field.
|
||||
* @param mixed $field_value The value of the field.
|
||||
* @return mixed
|
||||
*/
|
||||
public function sanitize_field( $field_key, $field_value ) {
|
||||
try {
|
||||
$field = $this->additional_fields[ $field_key ] ?? null;
|
||||
|
||||
if ( $field ) {
|
||||
$field_value = call_user_func( $field['sanitize_callback'], $field_value, $field );
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow custom sanitization of an additional field.
|
||||
*
|
||||
* @param mixed $field_value The value of the field being sanitized.
|
||||
* @param string $field_key Key of the field being sanitized.
|
||||
*
|
||||
* @since 8.7.0
|
||||
*/
|
||||
return apply_filters( '__experimental_woocommerce_blocks_sanitize_additional_field', $field_value, $field_key );
|
||||
|
||||
} catch ( \Throwable $e ) {
|
||||
// One of the filters errored so skip it. This allows the checkout process to continue.
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
|
||||
trigger_error(
|
||||
sprintf(
|
||||
'Field sanitization for %s encountered an error. %s',
|
||||
esc_html( $field_key ),
|
||||
esc_html( $e->getMessage() )
|
||||
),
|
||||
E_USER_WARNING
|
||||
);
|
||||
}
|
||||
|
||||
return $field_value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate an additional field against any custom validation rules.
|
||||
*
|
||||
* @since 8.6.0
|
||||
*
|
||||
* @param string $field_key The key of the field.
|
||||
* @param mixed $field_value The value of the field.
|
||||
* @return WP_Error
|
||||
*/
|
||||
public function validate_field( $key, $field_value, $request, $address_type = null ) {
|
||||
public function validate_field( $field_key, $field_value ) {
|
||||
$errors = new WP_Error();
|
||||
|
||||
$error = new \WP_Error();
|
||||
try {
|
||||
/**
|
||||
* Filter the result of validating an additional field.
|
||||
*
|
||||
* @param \WP_Error $error A WP_Error that extensions may add errors to.
|
||||
* @param mixed $field_value The value of the field.
|
||||
* @param \WP_REST_Request $request The current API Request.
|
||||
* @param string|null $address_type The type of address (billing, shipping, or null if the field is a contact/additional field).
|
||||
*
|
||||
* @since 8.6.0
|
||||
*/
|
||||
$filtered_result = apply_filters( 'woocommerce_blocks_validate_additional_field_' . $key, $error, $field_value, $request, $address_type );
|
||||
$field = $this->additional_fields[ $field_key ] ?? null;
|
||||
|
||||
if ( $error !== $filtered_result ) {
|
||||
if ( $field ) {
|
||||
$validation = call_user_func( $field['validate_callback'], $field_value, $field );
|
||||
|
||||
// Different WP_Error was returned. This would remove errors from other filters. Skip filtering and allow the order to place without validating this field.
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
|
||||
trigger_error(
|
||||
sprintf(
|
||||
'The filter %s encountered an error. One of the filters returned a new WP_Error. Filters should use the same WP_Error passed to the filter and use the WP_Error->add function to add errors. The field will not have any custom validation applied to it.',
|
||||
'woocommerce_blocks_validate_additional_field_' . esc_html( $key ),
|
||||
),
|
||||
E_USER_WARNING
|
||||
);
|
||||
if ( is_wp_error( $validation ) ) {
|
||||
$errors->merge_from( $validation );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pass an error object to allow validation of an additional field.
|
||||
*
|
||||
* @param WP_Error $errors A WP_Error object that extensions may add errors to.
|
||||
* @param string $field_key Key of the field being sanitized.
|
||||
* @param mixed $field_value The value of the field being validated.
|
||||
*
|
||||
* @since 8.7.0
|
||||
*/
|
||||
do_action( '__experimental_woocommerce_blocks_validate_additional_field', $errors, $field_key, $field_value );
|
||||
|
||||
} catch ( \Throwable $e ) {
|
||||
|
||||
// One of the filters errored so skip them and validate the field. This allows the checkout process to continue.
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
|
||||
trigger_error(
|
||||
sprintf(
|
||||
'The filter %s encountered an error. The field will not have any custom validation applied to it. %s',
|
||||
'woocommerce_blocks_validate_additional_field_' . esc_html( $key ),
|
||||
'Field validation for %s encountered an error. %s',
|
||||
esc_html( $field_key ),
|
||||
esc_html( $e->getMessage() )
|
||||
),
|
||||
E_USER_WARNING
|
||||
);
|
||||
|
||||
return new \WP_Error();
|
||||
}
|
||||
|
||||
if ( is_wp_error( $filtered_result ) ) {
|
||||
return $filtered_result;
|
||||
}
|
||||
|
||||
// If the filters didn't return a valid value, ignore them and return an empty WP_Error. This allows the checkout process to continue.
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
|
||||
trigger_error(
|
||||
sprintf(
|
||||
'The filter %s did not return a valid value. The field will not have any custom validation applied to it.',
|
||||
'woocommerce_blocks_validate_additional_field_' . esc_html( $key )
|
||||
),
|
||||
E_USER_WARNING
|
||||
);
|
||||
return new \WP_Error();
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -645,24 +724,6 @@ class CheckoutFields {
|
||||
return $this->fields_locations['additional'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of fields definitions only meant for order.
|
||||
*
|
||||
* @return array An array of fields definitions.
|
||||
*/
|
||||
public function get_order_only_fields() {
|
||||
// For now, all contact fields are order only fields, along with additional fields.
|
||||
$order_fields_keys = array_merge( $this->get_contact_fields_keys(), $this->get_additional_fields_keys() );
|
||||
|
||||
return array_filter(
|
||||
$this->get_additional_fields(),
|
||||
function( $key ) use ( $order_fields_keys ) {
|
||||
return in_array( $key, $order_fields_keys, true );
|
||||
},
|
||||
ARRAY_FILTER_USE_KEY
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of fields for a given group.
|
||||
*
|
||||
@@ -685,17 +746,60 @@ class CheckoutFields {
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a field value for a given group.
|
||||
* Validates a set of fields for a given location against custom validation rules.
|
||||
*
|
||||
* @param array $fields Array of key value pairs of field values to validate.
|
||||
* @param string $location The location being validated (address|contact|additional).
|
||||
* @param string $group The group to get the field value for (shipping|billing|'') in which '' refers to the additional group.
|
||||
* @return WP_Error
|
||||
*/
|
||||
public function validate_fields_for_location( $fields, $location, $group = '' ) {
|
||||
$errors = new WP_Error();
|
||||
|
||||
try {
|
||||
/**
|
||||
* Pass an error object to allow validation of an additional field.
|
||||
*
|
||||
* @param WP_Error $errors A WP_Error object that extensions may add errors to.
|
||||
* @param mixed $fields List of fields (key value pairs) in this location.
|
||||
* @param string $group The group of this location (shipping|billing|'').
|
||||
*
|
||||
* @since 8.7.0
|
||||
*/
|
||||
do_action( '__experimental_woocommerce_blocks_validate_location_' . $location . '_fields', $errors, $fields, $group );
|
||||
|
||||
} catch ( \Throwable $e ) {
|
||||
|
||||
// One of the filters errored so skip them. This allows the checkout process to continue.
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
|
||||
trigger_error(
|
||||
sprintf(
|
||||
'The action %s encountered an error. The field location %s may not have any custom validation applied to it. %s',
|
||||
esc_html( 'woocommerce_blocks_validate_' . $location . '_fields' ),
|
||||
esc_html( $location ),
|
||||
esc_html( $e->getMessage() )
|
||||
),
|
||||
E_USER_WARNING
|
||||
);
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a field to check it belongs to the given location and is valid according to its registration.
|
||||
*
|
||||
* This does not apply any custom validation rules on the value.
|
||||
*
|
||||
* @param string $key The field key.
|
||||
* @param mixed $value The field value.
|
||||
* @param string $location The location to validate the field for (address|contact|additional).
|
||||
*
|
||||
* @return true|\WP_Error True if the field is valid, a WP_Error otherwise.
|
||||
* @return true|WP_Error True if the field is valid, a WP_Error otherwise.
|
||||
*/
|
||||
public function validate_field_for_location( $key, $value, $location ) {
|
||||
if ( ! $this->is_field( $key ) ) {
|
||||
return new \WP_Error(
|
||||
return new WP_Error(
|
||||
'woocommerce_blocks_checkout_field_invalid',
|
||||
\sprintf(
|
||||
// translators: % is field key.
|
||||
@@ -706,7 +810,7 @@ class CheckoutFields {
|
||||
}
|
||||
|
||||
if ( ! in_array( $key, $this->fields_locations[ $location ], true ) ) {
|
||||
return new \WP_Error(
|
||||
return new WP_Error(
|
||||
'woocommerce_blocks_checkout_field_invalid_location',
|
||||
\sprintf(
|
||||
// translators: %1$s is field key, %2$s location.
|
||||
@@ -719,7 +823,7 @@ class CheckoutFields {
|
||||
|
||||
$field = $this->additional_fields[ $key ];
|
||||
if ( ! empty( $field['required'] ) && empty( $value ) ) {
|
||||
return new \WP_Error(
|
||||
return new WP_Error(
|
||||
'woocommerce_blocks_checkout_field_required',
|
||||
\sprintf(
|
||||
// translators: %s is field key.
|
||||
@@ -744,33 +848,29 @@ class CheckoutFields {
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists a field value for a given order. This would also optionally set the field value on the customer.
|
||||
* Persists a field value for a given order. This would also optionally set the field value on the customer object if the order is linked to a registered customer.
|
||||
*
|
||||
* @param string $key The field key.
|
||||
* @param mixed $value The field value.
|
||||
* @param \WC_Order $order The order to persist the field for.
|
||||
* @param bool $set_customer Whether to set the field value on the customer or not.
|
||||
* @param string $key The field key.
|
||||
* @param mixed $value The field value.
|
||||
* @param WC_Order $order The order to persist the field for.
|
||||
* @param bool $set_customer Whether to set the field value on the customer or not.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function persist_field_for_order( $key, $value, $order, $set_customer = true ) {
|
||||
$this->set_array_meta( $key, $value, $order );
|
||||
if ( $set_customer ) {
|
||||
if ( isset( wc()->customer ) ) {
|
||||
$this->set_array_meta( $key, $value, wc()->customer );
|
||||
} elseif ( $order->get_customer_id() ) {
|
||||
$customer = new \WC_Customer( $order->get_customer_id() );
|
||||
$this->set_array_meta( $key, $value, $customer );
|
||||
}
|
||||
if ( $set_customer && $order->get_customer_id() ) {
|
||||
$customer = new WC_Customer( $order->get_customer_id() );
|
||||
$this->persist_field_for_customer( $key, $value, $customer );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists a field value for a given customer.
|
||||
*
|
||||
* @param string $key The field key.
|
||||
* @param mixed $value The field value.
|
||||
* @param \WC_Customer $customer The customer to persist the field for.
|
||||
* @param string $key The field key.
|
||||
* @param mixed $value The field value.
|
||||
* @param WC_Customer $customer The customer to persist the field for.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
@@ -781,9 +881,9 @@ class CheckoutFields {
|
||||
/**
|
||||
* Sets a field value in an array meta, supporting routing things to billing, shipping, or additional fields, based on a prefix for the key.
|
||||
*
|
||||
* @param string $key The field key.
|
||||
* @param mixed $value The field value.
|
||||
* @param \WC_Customer|\WC_Order $object The object to set the field value for.
|
||||
* @param string $key The field key.
|
||||
* @param mixed $value The field value.
|
||||
* @param WC_Customer|WC_Order $object The object to set the field value for.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
@@ -800,39 +900,24 @@ class CheckoutFields {
|
||||
$meta_key = self::ADDITIONAL_FIELDS_KEY;
|
||||
}
|
||||
|
||||
if ( $object instanceof \WC_Customer ) {
|
||||
if ( ! $object->get_id() ) {
|
||||
$meta_data = wc()->session->get( $meta_key, array() );
|
||||
} else {
|
||||
$meta_data = get_user_meta( $object->get_id(), $meta_key, true );
|
||||
}
|
||||
} elseif ( $object instanceof \WC_Order ) {
|
||||
$meta_data = $object->get_meta( $meta_key, true );
|
||||
}
|
||||
$meta_data = $object->get_meta( $meta_key, true );
|
||||
|
||||
if ( ! is_array( $meta_data ) ) {
|
||||
$meta_data = array();
|
||||
}
|
||||
|
||||
$meta_data[ $key ] = $value;
|
||||
if ( $object instanceof \WC_Customer ) {
|
||||
if ( ! $object->get_id() ) {
|
||||
wc()->session->set( $meta_key, $meta_data );
|
||||
} else {
|
||||
update_user_meta( $object->get_id(), $meta_key, $meta_data );
|
||||
}
|
||||
} elseif ( $object instanceof \WC_Order ) {
|
||||
$object->update_meta_data( $meta_key, $meta_data );
|
||||
}
|
||||
|
||||
// Replacing all meta using `add_meta_data`. For some reason `update_meta_data` causes duplicate keys.
|
||||
$object->add_meta_data( $meta_key, $meta_data, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a field value for a given object.
|
||||
*
|
||||
* @param string $key The field key.
|
||||
* @param \WC_Customer $customer The customer to get the field value for.
|
||||
* @param string $group The group to get the field value for (shipping|billing|'') in which '' refers to the additional group.
|
||||
* @param string $key The field key.
|
||||
* @param WC_Customer $customer The customer to get the field value for.
|
||||
* @param string $group The group to get the field value for (shipping|billing|'') in which '' refers to the additional group.
|
||||
*
|
||||
* @return mixed The field value.
|
||||
*/
|
||||
@@ -843,9 +928,9 @@ class CheckoutFields {
|
||||
/**
|
||||
* Returns a field value for a given order.
|
||||
*
|
||||
* @param string $field The field key.
|
||||
* @param \WC_Order $order The order to get the field value for.
|
||||
* @param string $group The group to get the field value for (shipping|billing|'') in which '' refers to the additional group.
|
||||
* @param string $field The field key.
|
||||
* @param WC_Order $order The order to get the field value for.
|
||||
* @param string $group The group to get the field value for (shipping|billing|'') in which '' refers to the additional group.
|
||||
*
|
||||
* @return mixed The field value.
|
||||
*/
|
||||
@@ -856,9 +941,9 @@ class CheckoutFields {
|
||||
/**
|
||||
* Returns a field value for a given object.
|
||||
*
|
||||
* @param string $key The field key.
|
||||
* @param \WC_Customer|\WC_Order $object The customer to get the field value for.
|
||||
* @param string $group The group to get the field value for (shipping|billing|'') in which '' refers to the additional group.
|
||||
* @param string $key The field key.
|
||||
* @param WC_Customer|WC_Order $object The customer to get the field value for.
|
||||
* @param string $group The group to get the field value for (shipping|billing|'') in which '' refers to the additional group.
|
||||
*
|
||||
* @return mixed The field value.
|
||||
*/
|
||||
@@ -874,15 +959,7 @@ class CheckoutFields {
|
||||
$meta_key = self::ADDITIONAL_FIELDS_KEY;
|
||||
}
|
||||
|
||||
if ( $object instanceof \WC_Customer ) {
|
||||
if ( ! $object->get_id() ) {
|
||||
$meta_data = wc()->session->get( $meta_key, array() );
|
||||
} else {
|
||||
$meta_data = get_user_meta( $object->get_id(), $meta_key, true );
|
||||
}
|
||||
} elseif ( $object instanceof \WC_Order ) {
|
||||
$meta_data = $object->get_meta( $meta_key, true );
|
||||
}
|
||||
$meta_data = $object->get_meta( $meta_key, true );
|
||||
|
||||
if ( ! is_array( $meta_data ) ) {
|
||||
return '';
|
||||
@@ -898,38 +975,30 @@ class CheckoutFields {
|
||||
/**
|
||||
* Returns an array of all fields values for a given customer.
|
||||
*
|
||||
* @param \WC_Customer $customer The customer to get the fields for.
|
||||
* @param bool $all Whether to return all fields or only the ones that are still registered. Default false.
|
||||
* @param WC_Customer $customer The customer to get the fields for.
|
||||
* @param bool $all Whether to return all fields or only the ones that are still registered. Default false.
|
||||
*
|
||||
* @return array An array of fields.
|
||||
*/
|
||||
public function get_all_fields_from_customer( $customer, $all = false ) {
|
||||
$customer_id = $customer->get_id();
|
||||
$meta_data = array(
|
||||
$meta_data = array(
|
||||
'billing' => array(),
|
||||
'shipping' => array(),
|
||||
'additional' => array(),
|
||||
);
|
||||
if ( ! $customer_id ) {
|
||||
if ( isset( wc()->session ) ) {
|
||||
$meta_data['billing'] = wc()->session->get( self::BILLING_FIELDS_KEY, array() );
|
||||
$meta_data['shipping'] = wc()->session->get( self::SHIPPING_FIELDS_KEY, array() );
|
||||
$meta_data['additional'] = wc()->session->get( self::ADDITIONAL_FIELDS_KEY, array() );
|
||||
}
|
||||
} else {
|
||||
$meta_data['billing'] = get_user_meta( $customer_id, self::BILLING_FIELDS_KEY, true );
|
||||
$meta_data['shipping'] = get_user_meta( $customer_id, self::SHIPPING_FIELDS_KEY, true );
|
||||
$meta_data['additional'] = get_user_meta( $customer_id, self::ADDITIONAL_FIELDS_KEY, true );
|
||||
if ( $customer instanceof WC_Customer ) {
|
||||
$meta_data['billing'] = $customer->get_meta( self::BILLING_FIELDS_KEY, true );
|
||||
$meta_data['shipping'] = $customer->get_meta( self::SHIPPING_FIELDS_KEY, true );
|
||||
$meta_data['additional'] = $customer->get_meta( self::ADDITIONAL_FIELDS_KEY, true );
|
||||
}
|
||||
|
||||
return $this->format_meta_data( $meta_data, $all );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of all fields values for a given order.
|
||||
*
|
||||
* @param \WC_Order $order The order to get the fields for.
|
||||
* @param bool $all Whether to return all fields or only the ones that are still registered. Default false.
|
||||
* @param WC_Order $order The order to get the fields for.
|
||||
* @param bool $all Whether to return all fields or only the ones that are still registered. Default false.
|
||||
*
|
||||
* @return array An array of fields.
|
||||
*/
|
||||
@@ -939,7 +1008,7 @@ class CheckoutFields {
|
||||
'shipping' => array(),
|
||||
'additional' => array(),
|
||||
);
|
||||
if ( $order instanceof \WC_Order ) {
|
||||
if ( $order instanceof WC_Order ) {
|
||||
$meta_data['billing'] = $order->get_meta( self::BILLING_FIELDS_KEY, true );
|
||||
$meta_data['shipping'] = $order->get_meta( self::SHIPPING_FIELDS_KEY, true );
|
||||
$meta_data['additional'] = $order->get_meta( self::ADDITIONAL_FIELDS_KEY, true );
|
||||
@@ -997,27 +1066,71 @@ class CheckoutFields {
|
||||
* For now, this only supports fields in address location.
|
||||
*
|
||||
* @param array $fields The fields to filter.
|
||||
*
|
||||
* @return array The filtered fields.
|
||||
*/
|
||||
public function filter_fields_for_customer( $fields ) {
|
||||
$customer_fields_keys = $this->get_address_fields_keys();
|
||||
$customer_fields_keys = array_merge(
|
||||
$this->get_address_fields_keys(),
|
||||
$this->get_contact_fields_keys(),
|
||||
);
|
||||
return array_filter(
|
||||
$fields,
|
||||
function( $key ) use ( $customer_fields_keys ) {
|
||||
if ( 0 === strpos( $key, '/billing/' ) ) {
|
||||
$key = str_replace( '/billing/', '', $key );
|
||||
} elseif ( 0 === strpos( $key, '/shipping/' ) ) {
|
||||
$key = str_replace( '/shipping/', '', $key );
|
||||
}
|
||||
return in_array( $key, $customer_fields_keys, true );
|
||||
},
|
||||
ARRAY_FILTER_USE_KEY
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* From a set of fields, returns only the ones for a given location.
|
||||
*
|
||||
* @param array $fields The fields to filter.
|
||||
* @param string $location The location to validate the field for (address|contact|additional).
|
||||
* @return array The filtered fields.
|
||||
*/
|
||||
public function filter_fields_for_location( $fields, $location ) {
|
||||
return array_filter(
|
||||
$fields,
|
||||
function( $key ) use ( $location ) {
|
||||
if ( 0 === strpos( $key, '/billing/' ) ) {
|
||||
$key = str_replace( '/billing/', '', $key );
|
||||
} elseif ( 0 === strpos( $key, '/shipping/' ) ) {
|
||||
$key = str_replace( '/shipping/', '', $key );
|
||||
}
|
||||
return $this->is_field( $key ) && $this->get_field_location( $key ) === $location;
|
||||
},
|
||||
ARRAY_FILTER_USE_KEY
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter fields for order confirmation.
|
||||
*
|
||||
* @param array $fields The fields to filter.
|
||||
* @return array The filtered fields.
|
||||
*/
|
||||
public function filter_fields_for_order_confirmation( $fields ) {
|
||||
return array_filter(
|
||||
$fields,
|
||||
function( $field ) {
|
||||
return ! empty( $field['show_in_order_confirmation'] );
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get additional fields for an order.
|
||||
*
|
||||
* @param \WC_Order $order Order object.
|
||||
* @param string $location The location to get fields for (address|contact|additional).
|
||||
* @param string $group The group to get the field value for (shipping|billing|'') in which '' refers to the additional group.
|
||||
* @param string $context The context to get the field value for (edit|view).
|
||||
* @param WC_Order $order Order object.
|
||||
* @param string $location The location to get fields for (address|contact|additional).
|
||||
* @param string $group The group to get the field value for (shipping|billing|'') in which '' refers to the additional group.
|
||||
* @param string $context The context to get the field value for (edit|view).
|
||||
* @return array An array of fields definitions as well as their values formatted for display.
|
||||
*/
|
||||
public function get_order_additional_fields_with_values( $order, $location, $group = '', $context = 'edit' ) {
|
||||
|
||||
@@ -0,0 +1,300 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Blocks\Domain\Services;
|
||||
|
||||
use WC_Customer;
|
||||
use WC_Order;
|
||||
|
||||
/**
|
||||
* Service class managing checkout fields and its related extensibility points on the frontend.
|
||||
*/
|
||||
class CheckoutFieldsFrontend {
|
||||
|
||||
/**
|
||||
* Checkout field controller.
|
||||
*
|
||||
* @var CheckoutFields
|
||||
*/
|
||||
private $checkout_fields_controller;
|
||||
|
||||
/**
|
||||
* Sets up core fields.
|
||||
*
|
||||
* @param CheckoutFields $checkout_fields_controller Instance of the checkout field controller.
|
||||
*/
|
||||
public function __construct( CheckoutFields $checkout_fields_controller ) {
|
||||
$this->checkout_fields_controller = $checkout_fields_controller;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize hooks. This is not run Store API requests.
|
||||
*/
|
||||
public function init() {
|
||||
// Show custom checkout fields on the order details page.
|
||||
add_action( 'woocommerce_order_details_after_customer_address', array( $this, 'render_order_address_fields' ), 10, 2 );
|
||||
add_action( 'woocommerce_order_details_after_customer_details', array( $this, 'render_order_additional_fields' ), 10 );
|
||||
|
||||
// Show custom checkout fields on the My Account page.
|
||||
add_action( 'woocommerce_my_account_after_my_address', array( $this, 'render_address_fields' ), 10, 1 );
|
||||
|
||||
// Edit account form under my account (for contact details).
|
||||
add_filter( 'woocommerce_save_account_details_required_fields', array( $this, 'edit_account_form_required_fields' ), 10, 1 );
|
||||
add_filter( 'woocommerce_edit_account_form_fields', array( $this, 'edit_account_form_fields' ), 10, 1 );
|
||||
add_action( 'woocommerce_save_account_details', array( $this, 'save_account_form_fields' ), 10, 1 );
|
||||
|
||||
// Edit address form under my account.
|
||||
add_filter( 'woocommerce_address_to_edit', array( $this, 'edit_address_fields' ), 10, 2 );
|
||||
add_action( 'woocommerce_after_save_address_validation', array( $this, 'save_address_fields' ), 10, 4 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render custom fields.
|
||||
*
|
||||
* @param array $fields List of additional fields with values.
|
||||
* @return string
|
||||
*/
|
||||
protected function render_additional_fields( $fields ) {
|
||||
return ! empty( $fields ) ? '<dl class="wc-block-components-additional-fields-list">' . implode( '', array_map( array( $this, 'render_additional_field' ), $fields ) ) . '</dl>' : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Render custom field.
|
||||
*
|
||||
* @param array $field An additional field and value.
|
||||
* @return string
|
||||
*/
|
||||
protected function render_additional_field( $field ) {
|
||||
return sprintf(
|
||||
'<dt>%1$s</dt><dd>%2$s</dd>',
|
||||
esc_html( $field['label'] ),
|
||||
esc_html( $field['value'] )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders address fields on the order details page.
|
||||
*
|
||||
* @param string $address_type Type of address (billing or shipping).
|
||||
* @param WC_Order $order Order object.
|
||||
*/
|
||||
public function render_order_address_fields( $address_type, $order ) {
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
echo $this->render_additional_fields( $this->checkout_fields_controller->get_order_additional_fields_with_values( $order, 'address', $address_type, 'view' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders additional fields on the order details page.
|
||||
*
|
||||
* @param WC_Order $order Order object.
|
||||
*/
|
||||
public function render_order_additional_fields( $order ) {
|
||||
$fields = array_merge(
|
||||
$this->checkout_fields_controller->get_order_additional_fields_with_values( $order, 'contact', '', 'view' ),
|
||||
$this->checkout_fields_controller->get_order_additional_fields_with_values( $order, 'additional', '', 'view' ),
|
||||
);
|
||||
|
||||
if ( ! $fields ) {
|
||||
return;
|
||||
}
|
||||
|
||||
echo '<section class="wc-block-order-confirmation-additional-fields-wrapper">';
|
||||
echo '<h2>' . esc_html__( 'Additional information', 'woocommerce' ) . '</h2>';
|
||||
echo $this->render_additional_fields( $fields ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
echo '</section>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders address fields on the account page.
|
||||
*
|
||||
* @param string $address_type Type of address (billing or shipping).
|
||||
*/
|
||||
public function render_address_fields( $address_type ) {
|
||||
if ( ! in_array( $address_type, array( 'billing', 'shipping' ), true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$customer = new WC_Customer( get_current_user_id() );
|
||||
$fields = $this->checkout_fields_controller->get_fields_for_location( 'address' );
|
||||
|
||||
if ( ! $fields || ! $customer ) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ( $fields as $key => $field ) {
|
||||
$value = $this->checkout_fields_controller->format_additional_field_value(
|
||||
$this->checkout_fields_controller->get_field_from_customer( $key, $customer, $address_type ),
|
||||
$field
|
||||
);
|
||||
|
||||
if ( ! $value ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
printf( '<br><strong>%s</strong>: %s', wp_kses_post( $field['label'] ), wp_kses_post( $value ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register required additional contact fields.
|
||||
*
|
||||
* @param array $fields Required fields.
|
||||
* @return array
|
||||
*/
|
||||
public function edit_account_form_required_fields( $fields ) {
|
||||
$additional_fields = $this->checkout_fields_controller->get_fields_for_location( 'contact' );
|
||||
|
||||
foreach ( $additional_fields as $key => $field ) {
|
||||
if ( ! empty( $field['required'] ) ) {
|
||||
$fields[ $key ] = $field['label'];
|
||||
}
|
||||
}
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds additional contact fields to the My Account edit account form.
|
||||
*/
|
||||
public function edit_account_form_fields() {
|
||||
$customer = new WC_Customer( get_current_user_id() );
|
||||
$fields = $this->checkout_fields_controller->get_fields_for_location( 'contact' );
|
||||
|
||||
foreach ( $fields as $key => $field ) {
|
||||
$form_field = $field;
|
||||
$form_field['value'] = $this->checkout_fields_controller->get_field_from_customer( $key, $customer, 'contact' );
|
||||
|
||||
if ( 'select' === $field['type'] ) {
|
||||
$form_field['options'] = array_column( $field['options'], 'label', 'value' );
|
||||
}
|
||||
|
||||
if ( 'checkbox' === $field['type'] ) {
|
||||
$form_field['checked_value'] = '1';
|
||||
$form_field['unchecked_value'] = '0';
|
||||
}
|
||||
|
||||
woocommerce_form_field( $key, $form_field, wc_get_post_data_by_key( $key, $form_field['value'] ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates and saves additional address fields to the customer object on the My Account page.
|
||||
*
|
||||
* Customer is not provided by this hook so we handle save here.
|
||||
*
|
||||
* @param integer $user_id User ID.
|
||||
*/
|
||||
public function save_account_form_fields( $user_id ) {
|
||||
// phpcs:disable WordPress.Security.NonceVerification.Missing
|
||||
$customer = new WC_Customer( $user_id );
|
||||
$additional_fields = $this->checkout_fields_controller->get_fields_for_location( 'contact' );
|
||||
$field_values = array();
|
||||
|
||||
foreach ( $additional_fields as $key => $field ) {
|
||||
if ( ! isset( $_POST[ $key ] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$field_value = $this->checkout_fields_controller->sanitize_field( $key, wc_clean( wp_unslash( $_POST[ $key ] ) ) );
|
||||
$validation = $this->checkout_fields_controller->validate_field( $key, $field_value );
|
||||
|
||||
if ( is_wp_error( $validation ) && $validation->has_errors() ) {
|
||||
wc_add_notice( $validation->get_error_message(), 'error' );
|
||||
continue;
|
||||
}
|
||||
|
||||
$field_values[ $key ] = $field_value;
|
||||
}
|
||||
|
||||
// Persist individual additional fields to customer.
|
||||
foreach ( $field_values as $key => $value ) {
|
||||
$this->checkout_fields_controller->persist_field_for_customer( $key, $value, $customer );
|
||||
}
|
||||
|
||||
// Validate all fields for this location.
|
||||
$location_validation = $this->checkout_fields_controller->validate_fields_for_location( $field_values, 'contact' );
|
||||
|
||||
if ( is_wp_error( $location_validation ) && $location_validation->has_errors() ) {
|
||||
wc_add_notice( $location_validation->get_error_message(), 'error' );
|
||||
}
|
||||
|
||||
// phpcs:enable WordPress.Security.NonceVerification.Missing
|
||||
$customer->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds additional address fields to the My Account edit address form.
|
||||
*
|
||||
* @param array $address Address fields.
|
||||
* @param string $address_type Type of address (billing or shipping).
|
||||
* @return array Updated address fields.
|
||||
*/
|
||||
public function edit_address_fields( $address, $address_type ) {
|
||||
$customer = new WC_Customer( get_current_user_id() );
|
||||
$fields = $this->checkout_fields_controller->get_fields_for_location( 'address' );
|
||||
|
||||
foreach ( $fields as $key => $field ) {
|
||||
$field_key = "/{$address_type}/{$key}";
|
||||
$address[ $field_key ] = $field;
|
||||
$address[ $field_key ]['value'] = $this->checkout_fields_controller->get_field_from_customer( $key, $customer, $address_type );
|
||||
|
||||
if ( 'select' === $field['type'] ) {
|
||||
$address[ $field_key ]['options'] = array_column( $field['options'], 'label', 'value' );
|
||||
}
|
||||
|
||||
if ( 'checkbox' === $field['type'] ) {
|
||||
$address[ $field_key ]['checked_value'] = '1';
|
||||
$address[ $field_key ]['unchecked_value'] = '0';
|
||||
}
|
||||
}
|
||||
|
||||
return $address;
|
||||
}
|
||||
|
||||
/**
|
||||
* For the My Account page, save address fields. This uses the Store API endpoint for saving addresses so
|
||||
* extensibility hooks are consistent across the codebase.
|
||||
*
|
||||
* The caller saves the customer object if there are no errors. Nonces are checked before this method executes.
|
||||
*
|
||||
* @param integer $user_id User ID.
|
||||
* @param string $address_type Type of address (billing or shipping).
|
||||
* @param array $address Address fields.
|
||||
* @param WC_Customer $customer Customer object.
|
||||
*/
|
||||
public function save_address_fields( $user_id, $address_type, $address, $customer ) {
|
||||
// phpcs:disable WordPress.Security.NonceVerification.Missing
|
||||
$additional_fields = $this->checkout_fields_controller->get_fields_for_location( 'address' );
|
||||
$field_values = array();
|
||||
|
||||
foreach ( $additional_fields as $key => $field ) {
|
||||
$post_key = "/{$address_type}/{$key}";
|
||||
|
||||
if ( ! isset( $_POST[ $post_key ] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$field_value = $this->checkout_fields_controller->sanitize_field( $key, wc_clean( wp_unslash( $_POST[ $post_key ] ) ) );
|
||||
$validation = $this->checkout_fields_controller->validate_field( $key, $field_value );
|
||||
|
||||
if ( is_wp_error( $validation ) && $validation->has_errors() ) {
|
||||
wc_add_notice( $validation->get_error_message(), 'error' );
|
||||
continue;
|
||||
}
|
||||
|
||||
$field_values[ $key ] = $field_value;
|
||||
}
|
||||
|
||||
// Persist individual additional fields to customer.
|
||||
foreach ( $field_values as $key => $value ) {
|
||||
$this->checkout_fields_controller->persist_field_for_customer( "/{$address_type}/{$key}", $value, $customer );
|
||||
}
|
||||
|
||||
// Validate all fields for this location.
|
||||
$location_validation = $this->checkout_fields_controller->validate_fields_for_location( array_merge( $address, $field_values ), 'address', $address_type );
|
||||
|
||||
if ( is_wp_error( $location_validation ) && $location_validation->has_errors() ) {
|
||||
wc_add_notice( $location_validation->get_error_message(), 'error' );
|
||||
}
|
||||
// phpcs:enable WordPress.Security.NonceVerification.Missing
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
<?php
|
||||
|
||||
use Automattic\WooCommerce\Blocks\Package;
|
||||
use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields;
|
||||
|
||||
if ( ! function_exists( 'woocommerce_blocks_register_checkout_field' ) && Package::feature()->is_experimental_build() ) {
|
||||
|
||||
if ( ! function_exists( '__experimental_woocommerce_blocks_register_checkout_field' ) ) {
|
||||
/**
|
||||
* Register a checkout field.
|
||||
*
|
||||
* @param array $options Field arguments.
|
||||
* @throws Exception If field registration fails.
|
||||
* @param array $options Field arguments. See CheckoutFields::register_checkout_field() for details.
|
||||
* @throws \Exception If field registration fails.
|
||||
*/
|
||||
function woocommerce_blocks_register_checkout_field( $options ) {
|
||||
function __experimental_woocommerce_blocks_register_checkout_field( $options ) { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionDoubleUnderscore,PHPCompatibility.FunctionNameRestrictions.ReservedFunctionNames.FunctionDoubleUnderscore
|
||||
|
||||
// Check if `woocommerce_blocks_loaded` ran. If not then the CheckoutFields class will not be available yet.
|
||||
// In that case, re-hook `woocommerce_blocks_loaded` and try running this again.
|
||||
@@ -19,15 +19,15 @@ if ( ! function_exists( 'woocommerce_blocks_register_checkout_field' ) && Packag
|
||||
add_action(
|
||||
'woocommerce_blocks_loaded',
|
||||
function() use ( $options ) {
|
||||
woocommerce_blocks_register_checkout_field( $options );
|
||||
__experimental_woocommerce_blocks_register_checkout_field( $options );
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
$checkout_fields = Package::container()->get( \Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields::class );
|
||||
$checkout_fields = Package::container()->get( CheckoutFields::class );
|
||||
$result = $checkout_fields->register_checkout_field( $options );
|
||||
if ( is_wp_error( $result ) ) {
|
||||
throw new Exception( $result->get_error_message() );
|
||||
throw new \Exception( $result->get_error_message() );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<?php
|
||||
use Automattic\Jetpack\Constants;
|
||||
|
||||
/**
|
||||
* Move interactive scripts to the footer. This is a temporary measure to make
|
||||
* it work with `wc_store` and it should be replaced with deferred scripts or
|
||||
@@ -36,7 +38,8 @@ function woocommerce_interactivity_register_runtime() {
|
||||
if ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG && file_exists( $file_path ) ) {
|
||||
$version = filemtime( $file_path );
|
||||
} else {
|
||||
$version = \Automattic\WooCommerce\Blocks\Package::get_version();
|
||||
// Use wc- prefix here to prevent collisions when WC Core version catches up to a version previously used by the WC Blocks feature plugin.
|
||||
$version = 'wc-' . Constants::get_constant( 'WC_VERSION' );
|
||||
}
|
||||
|
||||
wp_register_script(
|
||||
|
||||
@@ -16,6 +16,7 @@ class CheckboxList {
|
||||
* - id: string of the id to use for the checkbox (optional).
|
||||
* - checked: boolean to indicate if the checkbox is checked.
|
||||
* - label: string of the label to display (plaintext or HTML).
|
||||
* - aria_label: string of the aria label to use for the checkbox. (optional, plaintext only).
|
||||
* - value: string of the value to use.
|
||||
* on_change: string of the action to perform when the dropdown changes.
|
||||
* @return string|false
|
||||
@@ -35,17 +36,22 @@ class CheckboxList {
|
||||
<div data-wc-interactive='<?php echo esc_attr( $namespace ); ?>'>
|
||||
<div data-wc-context='<?php echo esc_attr( wp_json_encode( $checkbox_list_context ) ); ?>' >
|
||||
<div class="wc-block-stock-filter style-list">
|
||||
<ul class="wc-block-checkbox-list wc-block-components-checkbox-list wc-block-stock-filter-list">
|
||||
<ul class="wc-block-components-checkbox-list">
|
||||
<?php foreach ( $items as $item ) { ?>
|
||||
<?php $item['id'] = $item['id'] ?? uniqid( 'checkbox-' ); ?>
|
||||
<?php
|
||||
$item['id'] = $item['id'] ?? uniqid( 'checkbox-' );
|
||||
// translators: %s: checkbox label.
|
||||
$i18n_label = sprintf( __( 'Checkbox: %s', 'woocommerce' ), $item['aria_label'] ?? '' );
|
||||
?>
|
||||
<li>
|
||||
<div class="wc-block-components-checkbox wc-block-checkbox-list__checkbox">
|
||||
<div class="wc-block-components-checkbox">
|
||||
<label for="<?php echo esc_attr( $item['id'] ); ?>">
|
||||
<input
|
||||
id="<?php echo esc_attr( $item['id'] ); ?>"
|
||||
class="wc-block-components-checkbox__input"
|
||||
type="checkbox"
|
||||
aria-invalid="false"
|
||||
aria-label="<?php echo esc_attr( $i18n_label ); ?>"
|
||||
data-wc-on--change--select-item="actions.selectCheckboxItem"
|
||||
data-wc-on--change--parent-action="<?php echo esc_attr( $on_change ); ?>"
|
||||
value="<?php echo esc_attr( $item['value'] ); ?>"
|
||||
|
||||
@@ -70,7 +70,7 @@ class Migration {
|
||||
* Rename `checkout` template to `page-checkout`.
|
||||
*/
|
||||
public static function wc_blocks_update_1120_rename_checkout_template() {
|
||||
$template = BlockTemplateUtils::get_block_template( BlockTemplateUtils::PLUGIN_SLUG . '//checkout', 'wp_template' );
|
||||
$template = get_block_template( BlockTemplateUtils::PLUGIN_SLUG . '//checkout', 'wp_template' );
|
||||
|
||||
if ( $template && ! empty( $template->wp_id ) ) {
|
||||
if ( ! defined( 'WP_POST_REVISIONS' ) ) {
|
||||
@@ -90,7 +90,7 @@ class Migration {
|
||||
* Rename `cart` template to `page-cart`.
|
||||
*/
|
||||
public static function wc_blocks_update_1120_rename_cart_template() {
|
||||
$template = BlockTemplateUtils::get_block_template( BlockTemplateUtils::PLUGIN_SLUG . '//cart', 'wp_template' );
|
||||
$template = get_block_template( BlockTemplateUtils::PLUGIN_SLUG . '//cart', 'wp_template' );
|
||||
|
||||
if ( $template && ! empty( $template->wp_id ) ) {
|
||||
if ( ! defined( 'WP_POST_REVISIONS' ) ) {
|
||||
|
||||
@@ -15,7 +15,7 @@ use Automattic\WooCommerce\Blocks\Domain\Services\FeatureGating;
|
||||
* In the context of this plugin, it handles init and is called from the main
|
||||
* plugin file (woocommerce-gutenberg-products-block.php).
|
||||
*
|
||||
* In the context of WooCommere core, it handles init and is called from
|
||||
* In the context of WooCommerce core, it handles init and is called from
|
||||
* WooCommerce's package loader. The main plugin file is _not_ loaded.
|
||||
*
|
||||
* @since 2.5.0
|
||||
|
||||
@@ -70,8 +70,8 @@ final class QueryFilters {
|
||||
$query_vars['posts_per_page'] = -1;
|
||||
$query_vars['fields'] = 'ids';
|
||||
$query = new \WP_Query();
|
||||
$result = $query->query( $query_vars );
|
||||
$product_query_sql = $query->request;
|
||||
$query->query( $query_vars );
|
||||
$product_query_sql = $query->request;
|
||||
|
||||
remove_filter( 'posts_clauses', array( $this, 'add_query_clauses' ), 10 );
|
||||
remove_filter( 'posts_pre_query', '__return_empty_array' );
|
||||
@@ -136,8 +136,8 @@ final class QueryFilters {
|
||||
$query_vars['posts_per_page'] = -1;
|
||||
$query_vars['fields'] = 'ids';
|
||||
$query = new \WP_Query();
|
||||
$result = $query->query( $query_vars );
|
||||
$product_query_sql = $query->request;
|
||||
$query->query( $query_vars );
|
||||
$product_query_sql = $query->request;
|
||||
|
||||
remove_filter( 'posts_clauses', array( $this, 'add_query_clauses' ), 10 );
|
||||
remove_filter( 'posts_pre_query', '__return_empty_array' );
|
||||
|
||||
@@ -77,7 +77,6 @@ class ShippingController {
|
||||
add_filter( 'woocommerce_shipping_settings', array( $this, 'remove_shipping_settings' ) );
|
||||
add_filter( 'wc_shipping_enabled', array( $this, 'force_shipping_enabled' ), 100, 1 );
|
||||
add_filter( 'woocommerce_order_shipping_to_display', array( $this, 'show_local_pickup_details' ), 10, 2 );
|
||||
add_filter( 'woocommerce_shipping_chosen_method', array( $this, 'prevent_shipping_method_selection_changes' ), 20, 3 );
|
||||
|
||||
// This is required to short circuit `show_shipping` from class-wc-cart.php - without it, that function
|
||||
// returns based on the option's value in the DB and we can't override it any other way.
|
||||
@@ -86,29 +85,6 @@ class ShippingController {
|
||||
add_action( 'rest_pre_serve_request', array( $this, 'track_local_pickup' ), 10, 4 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent changes in the selected shipping method when new rates are added or removed.
|
||||
*
|
||||
* If the chosen method exists within package rates, it is returned to maintain the selection.
|
||||
* Otherwise, the default rate is returned.
|
||||
*
|
||||
* @param string $default Default shipping method.
|
||||
* @param array $package_rates Associative array of available package rates.
|
||||
* @param string $chosen_method Previously chosen shipping method.
|
||||
*
|
||||
* @return string Chosen shipping method or default.
|
||||
*/
|
||||
public function prevent_shipping_method_selection_changes( $default, $package_rates, $chosen_method ) {
|
||||
|
||||
// If the chosen method exists in the package rates, return it.
|
||||
if ( $chosen_method && isset( $package_rates[ $chosen_method ] ) ) {
|
||||
return $chosen_method;
|
||||
}
|
||||
|
||||
// Otherwise, return the default method.
|
||||
return $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the option to force shipping calculations NOT to wait until an address is entered, but only if the
|
||||
* Checkout page contains the Checkout Block.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
<?php
|
||||
namespace Automattic\WooCommerce\Blocks\Templates;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\Utils\BlockTemplateMigrationUtils;
|
||||
|
||||
/**
|
||||
* CheckoutTemplate class.
|
||||
*
|
||||
@@ -34,11 +32,6 @@ class CheckoutTemplate extends AbstractPageTemplate {
|
||||
* @return boolean
|
||||
*/
|
||||
protected 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;
|
||||
|
||||
@@ -1,129 +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.
|
||||
*/
|
||||
public static function migrate_page( $template_slug ) {
|
||||
// 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//page-' . $template_slug, 'wp_template' );
|
||||
// If we were unable to get the block template, bail. Try again later.
|
||||
if ( ! $block_template ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip migration if the theme has a template file for this page.
|
||||
$theme_template = BlockTemplateUtils::get_block_template( get_stylesheet() . '//page-' . $template_slug, 'wp_template' );
|
||||
if ( $theme_template ) {
|
||||
return self::set_has_migrated_page( $template_slug, 'theme-file-exists' );
|
||||
}
|
||||
|
||||
// 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' );
|
||||
}
|
||||
|
||||
$template_content = self::get_default_template( $template_slug );
|
||||
|
||||
if ( self::create_custom_template( $block_template, $template_content ) ) {
|
||||
return self::set_has_migrated_page( $template_slug );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare default page template.
|
||||
*
|
||||
* @param string $template_slug Template slug.
|
||||
* @return string
|
||||
*/
|
||||
protected static function get_default_template( $template_slug ) {
|
||||
|
||||
$default_template_content = '
|
||||
<!-- wp:group {"layout":{"inherit":true,"type":"constrained"}} -->
|
||||
<div class="wp-block-group"><!-- wp:woocommerce/page-content-wrapper {"page":"' . $template_slug . '"} -->
|
||||
<!-- wp:post-title {"align":"wide", "level":1} /-->
|
||||
<!-- wp:post-content {"align":"wide"} /-->
|
||||
<!-- /wp:woocommerce/page-content-wrapper --></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;
|
||||
}
|
||||
}
|
||||
@@ -492,28 +492,6 @@ class BlockTemplateUtils {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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`.
|
||||
*
|
||||
* @return WP_Block_Template|null Template.
|
||||
*/
|
||||
public static function get_block_template( $id, $template_type ) {
|
||||
if ( function_exists( 'get_block_template' ) ) {
|
||||
return get_block_template( $id, $template_type );
|
||||
}
|
||||
|
||||
if ( function_exists( 'gutenberg_get_block_template' ) ) {
|
||||
return gutenberg_get_block_template( $id, $template_type );
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if we can fall back to the `archive-product` template for a given slug.
|
||||
*
|
||||
@@ -621,35 +599,6 @@ class BlockTemplateUtils {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter block templates by feature flag.
|
||||
*
|
||||
* @param WP_Block_Template[] $block_templates An array of block template objects.
|
||||
*
|
||||
* @return WP_Block_Template[] An array of block template objects.
|
||||
*/
|
||||
public static function filter_block_templates_by_feature_flag( $block_templates ) {
|
||||
$feature_gating = new FeatureGating();
|
||||
$flag = $feature_gating->get_flag();
|
||||
|
||||
/**
|
||||
* An array of block templates with slug as key and flag as value.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
$block_templates_with_feature_gate = array();
|
||||
|
||||
return array_filter(
|
||||
$block_templates,
|
||||
function( $block_template ) use ( $flag, $block_templates_with_feature_gate ) {
|
||||
if ( isset( $block_templates_with_feature_gate[ $block_template->slug ] ) ) {
|
||||
return $block_templates_with_feature_gate[ $block_template->slug ] <= $flag;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes templates that were added to a theme's block-templates directory, but already had a customised version saved in the database.
|
||||
*
|
||||
@@ -690,20 +639,48 @@ class BlockTemplateUtils {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes customized templates that shouldn't be available. That means customized templates based on the
|
||||
* WooCommerce default template when there is a customized template based on the theme template.
|
||||
*
|
||||
* @param \WP_Block_Template[]|\stdClass[] $templates List of templates to run the filter on.
|
||||
* @param string $theme_slug Slug of the theme currently active.
|
||||
*
|
||||
* @return array Filtered list of templates with only relevant templates available.
|
||||
*/
|
||||
public static function remove_duplicate_customized_templates( $templates, $theme_slug ) {
|
||||
$filtered_templates = array_filter(
|
||||
$templates,
|
||||
function( $template ) use ( $templates, $theme_slug ) {
|
||||
if ( $template->theme === $theme_slug ) {
|
||||
// This is a customized template based on the theme template, so it should be returned.
|
||||
return true;
|
||||
}
|
||||
// This is a template customized from the WooCommerce default template.
|
||||
// Only return it if there isn't a customized version of the theme template.
|
||||
$is_there_a_customized_theme_template = array_filter(
|
||||
$templates,
|
||||
function( $theme_template ) use ( $template, $theme_slug ) {
|
||||
return $theme_template->slug === $template->slug && $theme_template->theme === $theme_slug;
|
||||
}
|
||||
);
|
||||
if ( $is_there_a_customized_theme_template ) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
);
|
||||
|
||||
return $filtered_templates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the blockified templates should be used or not.
|
||||
* First, we need to make sure WordPress version is higher than 6.1 (lowest that supports Products block).
|
||||
* Then, if the option is not stored on the db, we need to check if the current theme is a block one or not.
|
||||
* If the option is not stored on the db, we need to check if the current theme is a block one or not.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public static function should_use_blockified_product_grid_templates() {
|
||||
$minimum_wp_version = '6.1';
|
||||
|
||||
if ( version_compare( $GLOBALS['wp_version'], $minimum_wp_version, '<' ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$use_blockified_templates = get_option( Options::WC_BLOCK_USE_BLOCKIFIED_PRODUCT_GRID_BLOCK_AS_TEMPLATE );
|
||||
|
||||
if ( false === $use_blockified_templates ) {
|
||||
@@ -785,7 +762,7 @@ class BlockTemplateUtils {
|
||||
$theme_has_template = self::theme_has_template_part( $slug );
|
||||
$template_slug_to_load = $theme_has_template ? get_stylesheet() : self::PLUGIN_SLUG;
|
||||
}
|
||||
$template_part = self::get_block_template( $template_slug_to_load . '//' . $slug, 'wp_template_part' );
|
||||
$template_part = get_block_template( $template_slug_to_load . '//' . $slug, 'wp_template_part' );
|
||||
|
||||
if ( $template_part && ! empty( $template_part->content ) ) {
|
||||
return $template_part->content;
|
||||
|
||||
@@ -31,4 +31,65 @@ class ProductCollectionUtils {
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function that constructs a WP_Query args array from
|
||||
* a Product Collection or global query.
|
||||
*
|
||||
* @param WP_Block $block Block instance.
|
||||
* @param int $page Current query's page.
|
||||
*
|
||||
* @return array Returns the constructed WP_Query arguments.
|
||||
*/
|
||||
public static function get_query_vars( $block, $page ) {
|
||||
if ( ! empty( $block->context['query'] ) && ! $block->context['query']['inherit'] ) {
|
||||
return build_query_vars_from_query_block( $block, $page );
|
||||
}
|
||||
|
||||
global $wp_query;
|
||||
return array_filter( $wp_query->query_vars );
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove query array from tax or meta query by searching for arrays that
|
||||
* contain exact key => value pair.
|
||||
*
|
||||
* @param array $queries tax_query or meta_query.
|
||||
* @param string $key Array key to search for.
|
||||
* @param mixed $value Value to compare with search result.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function remove_query_array( $queries, $key, $value ) {
|
||||
if ( ! is_array( $queries ) || empty( $queries ) ) {
|
||||
return $queries;
|
||||
}
|
||||
|
||||
foreach ( $queries as $query_key => $query ) {
|
||||
if ( isset( $query[ $key ] ) && $query[ $key ] === $value ) {
|
||||
unset( $queries[ $query_key ] );
|
||||
}
|
||||
|
||||
if ( isset( $query['relation'] ) || ! isset( $query[ $key ] ) ) {
|
||||
$queries[ $query_key ] = self::remove_query_array( $query, $key, $value );
|
||||
}
|
||||
}
|
||||
|
||||
return self::remove_empty_array_recursive( $queries );
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove falsy item from array, recursively.
|
||||
*
|
||||
* @param array $array The input array to filter.
|
||||
*/
|
||||
private static function remove_empty_array_recursive( $array ) {
|
||||
$array = array_filter( $array );
|
||||
foreach ( $array as $key => $item ) {
|
||||
if ( is_array( $item ) ) {
|
||||
$array[ $key ] = self::remove_empty_array_recursive( $item );
|
||||
}
|
||||
}
|
||||
return $array;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ class CLIRunner {
|
||||
WP_CLI::add_command( 'wc hpos cleanup', array( $this, 'cleanup_post_data' ) );
|
||||
WP_CLI::add_command( 'wc hpos status', array( $this, 'status' ) );
|
||||
WP_CLI::add_command( 'wc hpos diff', array( $this, 'diff' ) );
|
||||
WP_CLI::add_command( 'wc hpos backfill', array( $this, 'backfill' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -262,7 +263,7 @@ class CLIRunner {
|
||||
* ## EXAMPLES
|
||||
*
|
||||
* # Copy all order data into the post meta table, 500 posts at a time.
|
||||
* wp wc cot backfill --batch-size=500
|
||||
* wp wc cot migrate --batch-size=500
|
||||
*
|
||||
* @param array $args Positional arguments passed to the command.
|
||||
* @param array $assoc_args Associative arguments (options) passed to the command.
|
||||
@@ -993,7 +994,7 @@ ORDER BY $meta_table.order_id ASC, $meta_table.meta_key ASC;
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
* <id>
|
||||
* <order_id>
|
||||
* :The ID of the order.
|
||||
*
|
||||
* [--format=<format>]
|
||||
@@ -1063,4 +1064,82 @@ ORDER BY $meta_table.order_id ASC, $meta_table.meta_key ASC;
|
||||
array( 'property', 'hpos', 'post' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Backfills an order from either the HPOS or the posts datastore.
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
* <order_id>
|
||||
* : The ID of the order.
|
||||
*
|
||||
* --from=<datastore>
|
||||
* : Source datastore. Either 'hpos' or 'posts'.
|
||||
* ---
|
||||
* options:
|
||||
* - hpos
|
||||
* - posts
|
||||
* ---
|
||||
*
|
||||
* --to=<datastore>
|
||||
* : Destination datastore. Either 'hpos' or 'posts'.
|
||||
* ---
|
||||
* options:
|
||||
* - hpos
|
||||
* - posts
|
||||
* ---
|
||||
*
|
||||
* @since 8.6.0
|
||||
*
|
||||
* @param array $args Positional arguments passed to the command.
|
||||
* @param array $assoc_args Associative arguments (options) passed to the command.
|
||||
*/
|
||||
public function backfill( array $args = array(), array $assoc_args = array() ) {
|
||||
$legacy_handler = wc_get_container()->get( LegacyDataHandler::class );
|
||||
|
||||
$from = $assoc_args['from'] ?? '';
|
||||
$to = $assoc_args['to'] ?? '';
|
||||
$order_id = absint( $args[0] );
|
||||
|
||||
if ( ! $order_id ) {
|
||||
WP_CLI::error( __( 'Please provide a valid order ID.', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
foreach ( array( 'from', 'to' ) as $datastore ) {
|
||||
if ( ! in_array( ${"$datastore"}, array( 'posts', 'hpos' ), true ) ) {
|
||||
// translators: %s is a shell argument representing a datastore name.
|
||||
WP_CLI::error( sprintf( __( '\'%s\' is not a valid datastore.', 'woocommerce' ), ${"$datastore"} ) );
|
||||
}
|
||||
}
|
||||
|
||||
if ( $from === $to ) {
|
||||
WP_CLI::error( __( 'Please use different source (--from) and destination (--to) datastores.', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
try {
|
||||
$legacy_handler->backfill_order_to_datastore( $order_id, $from, $to );
|
||||
} catch ( \Exception $e ) {
|
||||
WP_CLI::error(
|
||||
sprintf(
|
||||
// translators: %1$d is an order ID, %2$s and %3$s are datastore names, %4$s is an error message.
|
||||
__( 'An error occurred while backfilling order %1$d from %2$s to %3$s: %4$s', 'woocommerce' ),
|
||||
$order_id,
|
||||
$from,
|
||||
$to,
|
||||
$e->getMessage()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
WP_CLI::success(
|
||||
sprintf(
|
||||
// translators: %1$d is an order ID, %2$s and %3$s are datastore names ("hpos" or "posts" for example).
|
||||
__( 'Order %1$d backfilled from %2$s to %3$s.', 'woocommerce' ),
|
||||
$order_id,
|
||||
$from,
|
||||
$to
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -664,6 +664,26 @@ class FileController {
|
||||
return array_slice( $matched_lines, $args['offset'], $args['per_page'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the size, in bytes, of the log directory.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function get_log_directory_size(): int {
|
||||
$bytes = 0;
|
||||
$path = realpath( $this->log_directory );
|
||||
|
||||
if ( wp_is_writable( $path ) ) {
|
||||
$iterator = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator( $path, \FilesystemIterator::SKIP_DOTS ) );
|
||||
|
||||
foreach ( $iterator as $file ) {
|
||||
$bytes += $file->getSize();
|
||||
}
|
||||
}
|
||||
|
||||
return $bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate the cache group related to log file data.
|
||||
*
|
||||
|
||||
@@ -123,6 +123,10 @@ class LogHandlerFileV2 extends WC_Log_Handler {
|
||||
$backtrace = static::get_backtrace();
|
||||
|
||||
foreach ( $backtrace as $frame ) {
|
||||
if ( ! isset( $frame['file'] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ( $source_roots as $type => $path ) {
|
||||
if ( 0 === strpos( $frame['file'], $path ) ) {
|
||||
$relative_path = trim( substr( $frame['file'], strlen( $path ) ), DIRECTORY_SEPARATOR );
|
||||
@@ -233,7 +237,29 @@ class LogHandlerFileV2 extends WC_Log_Handler {
|
||||
)
|
||||
);
|
||||
|
||||
if ( is_wp_error( $files ) || count( $files ) < 1 ) {
|
||||
if ( is_wp_error( $files ) ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$files = array_filter(
|
||||
$files,
|
||||
function( $file ) use ( $timestamp ) {
|
||||
/**
|
||||
* Allows preventing an expired log file from being deleted.
|
||||
*
|
||||
* @param bool $delete True to delete the file.
|
||||
* @param File $file The log file object.
|
||||
* @param int $timestamp The expiration threshold.
|
||||
*
|
||||
* @since 8.7.0
|
||||
*/
|
||||
$delete = apply_filters( 'woocommerce_logger_delete_expired_file', true, $file, $timestamp );
|
||||
|
||||
return boolval( $delete );
|
||||
}
|
||||
);
|
||||
|
||||
if ( count( $files ) < 1 ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -250,31 +276,16 @@ class LogHandlerFileV2 extends WC_Log_Handler {
|
||||
time(),
|
||||
'info',
|
||||
sprintf(
|
||||
'%s %s',
|
||||
sprintf(
|
||||
esc_html(
|
||||
// translators: %s is a number of log files.
|
||||
_n(
|
||||
'%s expired log file was deleted.',
|
||||
'%s expired log files were deleted.',
|
||||
$deleted,
|
||||
'woocommerce'
|
||||
)
|
||||
),
|
||||
number_format_i18n( $deleted )
|
||||
esc_html(
|
||||
// translators: %s is a number of log files.
|
||||
_n(
|
||||
'%s expired log file was deleted.',
|
||||
'%s expired log files were deleted.',
|
||||
$deleted,
|
||||
'woocommerce'
|
||||
)
|
||||
),
|
||||
sprintf(
|
||||
esc_html(
|
||||
// translators: %s is a number of days.
|
||||
_n(
|
||||
'The retention period for log files is %s day.',
|
||||
'The retention period for log files is %s days.',
|
||||
$retention_days,
|
||||
'woocommerce'
|
||||
)
|
||||
),
|
||||
number_format_i18n( $retention_days )
|
||||
)
|
||||
number_format_i18n( $deleted )
|
||||
),
|
||||
array(
|
||||
'source' => 'wc_logger',
|
||||
|
||||
@@ -4,10 +4,12 @@ declare( strict_types=1 );
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Logging;
|
||||
|
||||
use Automattic\Jetpack\Constants;
|
||||
use Automattic\WooCommerce\Internal\Admin\Logging\FileV2\File;
|
||||
use Automattic\WooCommerce\Internal\Admin\Logging\LogHandlerFileV2;
|
||||
use Automattic\WooCommerce\Internal\Admin\Logging\FileV2\FileController;
|
||||
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
|
||||
use WC_Admin_Settings;
|
||||
use WC_Log_Handler, WC_Log_Handler_DB, WC_Log_Handler_File, WC_Log_Levels;
|
||||
use WC_Log_Handler_DB, WC_Log_Handler_File, WC_Log_Levels;
|
||||
|
||||
/**
|
||||
* Settings class.
|
||||
@@ -22,11 +24,10 @@ class Settings {
|
||||
* @const array
|
||||
*/
|
||||
private const DEFAULTS = array(
|
||||
'logging_enabled' => true,
|
||||
'default_handler' => LogHandlerFileV2::class,
|
||||
'retention_period_days' => 30,
|
||||
'level_threshold' => 'none',
|
||||
'file_entry_collapse_lines' => true,
|
||||
'logging_enabled' => true,
|
||||
'default_handler' => LogHandlerFileV2::class,
|
||||
'retention_period_days' => 30,
|
||||
'level_threshold' => 'none',
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -77,13 +78,13 @@ class Settings {
|
||||
$settings['default_handler'] = $this->get_default_handler_setting_definition();
|
||||
$settings['retention_period_days'] = $this->get_retention_period_days_setting_definition();
|
||||
$settings['level_threshold'] = $this->get_level_threshold_setting_definition();
|
||||
}
|
||||
|
||||
$default_handler = $this->get_default_handler();
|
||||
if ( in_array( $default_handler, array( LogHandlerFileV2::class, WC_Log_Handler_File::class ), true ) ) {
|
||||
$settings += $this->get_filesystem_settings_definitions();
|
||||
} elseif ( WC_Log_Handler_DB::class === $default_handler ) {
|
||||
$settings += $this->get_database_settings_definitions();
|
||||
$default_handler = $this->get_default_handler();
|
||||
if ( in_array( $default_handler, array( LogHandlerFileV2::class, WC_Log_Handler_File::class ), true ) ) {
|
||||
$settings += $this->get_filesystem_settings_definitions();
|
||||
} elseif ( WC_Log_Handler_DB::class === $default_handler ) {
|
||||
$settings += $this->get_database_settings_definitions();
|
||||
}
|
||||
}
|
||||
|
||||
return $settings;
|
||||
@@ -153,18 +154,28 @@ class Settings {
|
||||
'step' => 1,
|
||||
);
|
||||
|
||||
$desc = array();
|
||||
|
||||
$hardcoded = has_filter( 'woocommerce_logger_days_to_retain_logs' );
|
||||
$desc = '';
|
||||
if ( $hardcoded ) {
|
||||
$custom_attributes['disabled'] = 'true';
|
||||
|
||||
$desc = sprintf(
|
||||
$desc[] = sprintf(
|
||||
// translators: %s is the name of a filter hook.
|
||||
__( 'This setting cannot be changed here because it is being set by a filter on the %s hook.', 'woocommerce' ),
|
||||
'<code>woocommerce_logger_days_to_retain_logs</code>'
|
||||
);
|
||||
}
|
||||
|
||||
$file_delete_has_filter = LogHandlerFileV2::class === $this->get_default_handler() && has_filter( 'woocommerce_logger_delete_expired_file' );
|
||||
if ( $file_delete_has_filter ) {
|
||||
$desc[] = sprintf(
|
||||
// translators: %s is the name of a filter hook.
|
||||
__( 'The %s hook has a filter set, so some log files may have different retention settings.', 'woocommerce' ),
|
||||
'<code>woocommerce_logger_delete_expired_file</code>'
|
||||
);
|
||||
}
|
||||
|
||||
return array(
|
||||
'title' => __( 'Retention period', 'woocommerce' ),
|
||||
'desc_tip' => __( 'This sets how many days log entries will be kept before being auto-deleted.', 'woocommerce' ),
|
||||
@@ -180,7 +191,7 @@ class Settings {
|
||||
' %s',
|
||||
__( 'days', 'woocommerce' ),
|
||||
),
|
||||
'desc' => $desc,
|
||||
'desc' => implode( '<br><br>', $desc ),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -231,7 +242,7 @@ class Settings {
|
||||
*/
|
||||
private function get_filesystem_settings_definitions(): array {
|
||||
$location_info = array();
|
||||
$directory = trailingslashit( realpath( Constants::get_constant( 'WC_LOG_DIR' ) ) );
|
||||
$directory = trailingslashit( Constants::get_constant( 'WC_LOG_DIR' ) );
|
||||
|
||||
$location_info[] = sprintf(
|
||||
// translators: %s is a location in the filesystem.
|
||||
@@ -253,6 +264,12 @@ class Settings {
|
||||
'<code>wp-config.php</code>'
|
||||
);
|
||||
|
||||
$location_info[] = sprintf(
|
||||
// translators: %s is an amount of computer disk space, e.g. 5 KB.
|
||||
__( 'Directory size: %s', 'woocommerce' ),
|
||||
size_format( wc_get_container()->get( FileController::class )->get_log_directory_size() )
|
||||
);
|
||||
|
||||
return array(
|
||||
'file_start' => array(
|
||||
'title' => __( 'File system settings', 'woocommerce' ),
|
||||
@@ -260,8 +277,9 @@ class Settings {
|
||||
'type' => 'title',
|
||||
),
|
||||
'log_directory' => array(
|
||||
'type' => 'info',
|
||||
'text' => implode( "\n\n", $location_info ),
|
||||
'title' => __( 'Location', 'woocommerce' ),
|
||||
'type' => 'info',
|
||||
'text' => implode( "\n\n", $location_info ),
|
||||
),
|
||||
'entry_format' => array(),
|
||||
'file_end' => array(
|
||||
@@ -293,8 +311,9 @@ class Settings {
|
||||
'type' => 'title',
|
||||
),
|
||||
'database_table' => array(
|
||||
'type' => 'info',
|
||||
'text' => $location_info,
|
||||
'title' => __( 'Location', 'woocommerce' ),
|
||||
'type' => 'info',
|
||||
'text' => $location_info,
|
||||
),
|
||||
'file_end' => array(
|
||||
'id' => self::PREFIX . 'settings',
|
||||
|
||||
@@ -11,8 +11,10 @@ namespace Automattic\WooCommerce\Internal\Admin\Onboarding;
|
||||
class Onboarding {
|
||||
/**
|
||||
* Initialize onboarding functionality.
|
||||
*
|
||||
* @internal This method is for internal purposes only.
|
||||
*/
|
||||
public static function init() {
|
||||
final public static function init() {
|
||||
OnboardingHelper::instance()->init();
|
||||
OnboardingIndustries::init();
|
||||
OnboardingJetpack::instance()->init();
|
||||
@@ -21,5 +23,6 @@ class Onboarding {
|
||||
OnboardingSetupWizard::instance()->init();
|
||||
OnboardingSync::instance()->init();
|
||||
OnboardingThemes::init();
|
||||
OnboardingFonts::init();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
<?php
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\Admin\Onboarding;
|
||||
|
||||
use Automattic\WooCommerce\Internal\Font\FontFace;
|
||||
use Automattic\WooCommerce\Internal\Font\FontFamily;
|
||||
|
||||
|
||||
/**
|
||||
* Class to install fonts for the Assembler.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class OnboardingFonts {
|
||||
|
||||
/**
|
||||
* Initialize the class.
|
||||
*
|
||||
* @internal This method is for internal purposes only.
|
||||
*/
|
||||
final public static function init() {
|
||||
add_action( 'woocommerce_install_assembler_fonts', array( __CLASS__, 'install_fonts' ) );
|
||||
add_filter( 'update_option_woocommerce_allow_tracking', array( self::class, 'start_install_fonts_async_job' ), 10, 2 );
|
||||
}
|
||||
|
||||
const SOURCE_LOGGER = 'font_loader';
|
||||
|
||||
/**
|
||||
* Font families to install.
|
||||
* PHP version of https://github.com/woocommerce/woocommerce/blob/45923dc5f38150c717210ae9db10045cd9582331/plugins/woocommerce-admin/client/customize-store/assembler-hub/sidebar/global-styles/font-pairing-variations/constants.ts/#L13-L74
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
const FONT_FAMILIES_TO_INSTALL = array(
|
||||
'inter' => array(
|
||||
'fontFamily' => 'Inter',
|
||||
'fontWeights' => array( '400', '500', '600' ),
|
||||
'fontStyles' => array( 'normal' ),
|
||||
),
|
||||
'bodoni-moda' => array(
|
||||
'fontFamily' => 'Bodoni Moda',
|
||||
'fontWeights' => array( '400' ),
|
||||
'fontStyles' => array( 'normal' ),
|
||||
),
|
||||
'overpass' => array(
|
||||
'fontFamily' => 'Overpass',
|
||||
'fontWeights' => array( '300', '400' ),
|
||||
'fontStyles' => array( 'normal' ),
|
||||
),
|
||||
'albert-sans' => array(
|
||||
'fontFamily' => 'Albert Sans',
|
||||
'fontWeights' => array( '700' ),
|
||||
'fontStyles' => array( 'normal' ),
|
||||
),
|
||||
'lora' => array(
|
||||
'fontFamily' => 'Lora',
|
||||
'fontWeights' => array( '400' ),
|
||||
'fontStyles' => array( 'normal' ),
|
||||
),
|
||||
'montserrat' => array(
|
||||
'fontFamily' => 'Montserrat',
|
||||
'fontWeights' => array( '500', '700' ),
|
||||
'fontStyles' => array( 'normal' ),
|
||||
),
|
||||
'arvo' => array(
|
||||
'fontFamily' => 'Arvo',
|
||||
'fontWeights' => array( '400' ),
|
||||
'fontStyles' => array( 'normal' ),
|
||||
),
|
||||
'rubik' => array(
|
||||
'fontFamily' => 'Rubik',
|
||||
'fontWeights' => array( '400', '800' ),
|
||||
'fontStyles' => array( 'normal' ),
|
||||
),
|
||||
'newsreader' => array(
|
||||
'fontFamily' => 'Newsreader',
|
||||
'fontWeights' => array( '400' ),
|
||||
'fontStyles' => array( 'normal' ),
|
||||
),
|
||||
'cormorant' => array(
|
||||
'fontFamily' => 'Cormorant',
|
||||
'fontWeights' => array( '400', '500' ),
|
||||
'fontStyles' => array( 'normal' ),
|
||||
),
|
||||
'work-sans' => array(
|
||||
'fontFamily' => 'Work Sans',
|
||||
'fontWeights' => array( '400' ),
|
||||
'fontStyles' => array( 'normal' ),
|
||||
),
|
||||
'raleway' => array(
|
||||
'fontFamily' => 'Raleway',
|
||||
'fontWeights' => array( '700' ),
|
||||
'fontStyles' => array( 'normal' ),
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* Start install fonts async job.
|
||||
*
|
||||
* @param string $old_value Old option value.
|
||||
* @param string $value Option value.
|
||||
* @return string
|
||||
*/
|
||||
public static function start_install_fonts_async_job( $old_value, $value ) {
|
||||
if ( 'yes' !== $value || ! class_exists( 'WP_Font_Library' ) ) {
|
||||
return;
|
||||
}
|
||||
WC()->call_function(
|
||||
'as_schedule_single_action',
|
||||
WC()->call_function( 'time' ),
|
||||
'woocommerce_install_assembler_fonts',
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create Font Families and Font Faces.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function install_fonts() {
|
||||
$collections = \WP_Font_Library::get_instance()->get_font_collections();
|
||||
$google_fonts = $collections['google-fonts']->get_data();
|
||||
$font_collection = $google_fonts['font_families'];
|
||||
$slug_font_families_to_install = array_keys( self::FONT_FAMILIES_TO_INSTALL );
|
||||
$installed_font_families = self::install_font_families( $slug_font_families_to_install, $font_collection );
|
||||
|
||||
if ( ! empty( $installed_font_families ) ) {
|
||||
$font_faces_from_collection = self::get_font_faces_data_from_font_collection( $slug_font_families_to_install, $font_collection );
|
||||
self::install_font_faces( $slug_font_families_to_install, $installed_font_families, $font_faces_from_collection );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Install font families.
|
||||
*
|
||||
* @param array $slug_font_families_to_install Font families to install.
|
||||
* @param array $font_collection Font collection.
|
||||
* @return array
|
||||
*/
|
||||
private static function install_font_families( $slug_font_families_to_install, $font_collection ) {
|
||||
return array_reduce(
|
||||
$slug_font_families_to_install,
|
||||
function( $carry, $slug ) use ( $font_collection ) {
|
||||
$font_family_from_collection = self::get_font_family_by_slug_from_font_collection( $slug, $font_collection );
|
||||
$font_family_name = $font_family_from_collection['fontFamily'];
|
||||
$font_family_installed = FontFamily::get_font_family_by_name( $font_family_name );
|
||||
if ( $font_family_installed ) {
|
||||
return array_merge( $carry, array( $slug => $font_family_installed ) );
|
||||
}
|
||||
|
||||
$font_family_settings = array(
|
||||
'fontFamily' => $font_family_from_collection['fontFamily'],
|
||||
'preview' => $font_family_from_collection['preview'],
|
||||
'slug' => $font_family_from_collection['slug'],
|
||||
'name' => $font_family_from_collection['name'],
|
||||
);
|
||||
|
||||
$font_family_id = FontFamily::insert_font_family( $font_family_settings );
|
||||
if ( is_wp_error( $font_family_id ) ) {
|
||||
if ( 'duplicate_font_family' !== $font_family_id->get_error_code() ) {
|
||||
wc_get_logger()->error(
|
||||
sprintf(
|
||||
'Font Family installation error: %s',
|
||||
$font_family_id->get_error_message(),
|
||||
),
|
||||
array( 'source' => self::SOURCE_LOGGER )
|
||||
);
|
||||
}
|
||||
|
||||
return $carry;
|
||||
}
|
||||
return array_merge( $carry, array( $slug => get_post( $font_family_id ) ) );
|
||||
},
|
||||
array(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Install font faces.
|
||||
*
|
||||
* @param array $slug_font_families_to_install Font families to install.
|
||||
* @param array $installed_font_families Installed font families.
|
||||
* @param array $font_faces_from_collection Font faces from collection.
|
||||
*/
|
||||
private static function install_font_faces( $slug_font_families_to_install, $installed_font_families, $font_faces_from_collection ) {
|
||||
foreach ( $slug_font_families_to_install as $slug ) {
|
||||
$font_family = $installed_font_families[ $slug ];
|
||||
$font_faces = $font_faces_from_collection[ $slug ];
|
||||
$font_faces_to_install = self::FONT_FAMILIES_TO_INSTALL[ $slug ]['fontWeights'];
|
||||
|
||||
foreach ( $font_faces as $font_face ) {
|
||||
if ( ! in_array( $font_face['fontWeight'], $font_faces_to_install, true ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$slug = \WP_Font_Utils::get_font_face_slug( $font_face );
|
||||
$font_face_installed = FontFace::get_installed_font_faces_by_slug( $slug );
|
||||
if ( $font_face_installed ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$wp_error = FontFace::insert_font_face( $font_face, $font_family->ID );
|
||||
|
||||
if ( is_wp_error( $wp_error ) ) {
|
||||
wc_get_logger()->error(
|
||||
sprintf(
|
||||
/* translators: %s: error message */
|
||||
__( 'Font Face installation error: %s', 'woocommerce' ),
|
||||
$wp_error->get_error_message()
|
||||
),
|
||||
array( 'source' => self::SOURCE_LOGGER )
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get font faces data from font collection.
|
||||
*
|
||||
* @param array $slug_font_families_to_install Font families to install.
|
||||
* @param array $font_collection Font collection.
|
||||
* @return array
|
||||
*/
|
||||
private static function get_font_faces_data_from_font_collection( $slug_font_families_to_install, $font_collection ) {
|
||||
return array_reduce(
|
||||
$slug_font_families_to_install,
|
||||
function( $carry, $slug ) use ( $font_collection ) {
|
||||
$font_family = self::get_font_family_by_slug_from_font_collection( $slug, $font_collection );
|
||||
if ( ! $font_family ) {
|
||||
return $carry;
|
||||
}
|
||||
return array_merge( $carry, array( $slug => $font_family['fontFace'] ) );
|
||||
},
|
||||
array()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get font family by slug from font collection.
|
||||
*
|
||||
* @param string $slug Font slug.
|
||||
* @param array $font_families_collection Font families collection.
|
||||
* @return array|null
|
||||
*/
|
||||
private static function get_font_family_by_slug_from_font_collection( $slug, $font_families_collection ) {
|
||||
$font_family = null;
|
||||
|
||||
foreach ( $font_families_collection as $font_family ) {
|
||||
if ( $font_family['font_family_settings']['slug'] === $slug ) {
|
||||
$font_family = $font_family['font_family_settings'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $font_family;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -20,7 +20,7 @@ class EvaluateExtension {
|
||||
* @param object $extension The extension to evaluate.
|
||||
* @return object The evaluated extension.
|
||||
*/
|
||||
public static function evaluate( $extension ) {
|
||||
private static function evaluate( $extension ) {
|
||||
global $wp_version;
|
||||
$rule_evaluator = new RuleEvaluator();
|
||||
|
||||
@@ -49,4 +49,44 @@ class EvaluateExtension {
|
||||
|
||||
return $extension;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates the specs and returns the bundles with visible extensions.
|
||||
*
|
||||
* @param array $specs extensions spec array.
|
||||
* @param array $allowed_bundles Optional array of allowed bundles to be returned.
|
||||
* @return array The bundles and errors.
|
||||
*/
|
||||
public static function evaluate_bundles( $specs, $allowed_bundles = array() ) {
|
||||
$bundles = array();
|
||||
|
||||
foreach ( $specs as $spec ) {
|
||||
$spec = (object) $spec;
|
||||
$bundle = (array) $spec;
|
||||
$bundle['plugins'] = array();
|
||||
|
||||
if ( ! empty( $allowed_bundles ) && ! in_array( $spec->key, $allowed_bundles, true ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$errors = array();
|
||||
foreach ( $spec->plugins as $plugin ) {
|
||||
try {
|
||||
$extension = self::evaluate( (object) $plugin );
|
||||
if ( ! property_exists( $extension, 'is_visible' ) || $extension->is_visible ) {
|
||||
$bundle['plugins'][] = $extension;
|
||||
}
|
||||
} catch ( \Throwable $e ) {
|
||||
$errors[] = $e;
|
||||
}
|
||||
}
|
||||
|
||||
$bundles[] = $bundle;
|
||||
}
|
||||
|
||||
return array(
|
||||
'bundles' => $bundles,
|
||||
'errors' => $errors,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,12 +8,13 @@ namespace Automattic\WooCommerce\Internal\Admin\RemoteFreeExtensions;
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use Automattic\WooCommerce\Internal\Admin\RemoteFreeExtensions\DefaultFreeExtensions;
|
||||
use Automattic\WooCommerce\Admin\RemoteSpecs\RemoteSpecsEngine;
|
||||
|
||||
/**
|
||||
* Remote Payment Methods engine.
|
||||
* This goes through the specs and gets eligible payment methods.
|
||||
*/
|
||||
class Init {
|
||||
class Init extends RemoteSpecsEngine {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
@@ -29,34 +30,39 @@ class Init {
|
||||
* @return array
|
||||
*/
|
||||
public static function get_extensions( $allowed_bundles = array() ) {
|
||||
$bundles = array();
|
||||
$specs = self::get_specs();
|
||||
$locale = get_user_locale();
|
||||
|
||||
foreach ( $specs as $spec ) {
|
||||
$spec = (object) $spec;
|
||||
$bundle = (array) $spec;
|
||||
$bundle['plugins'] = array();
|
||||
$specs = self::get_specs();
|
||||
$results = EvaluateExtension::evaluate_bundles( $specs, $allowed_bundles );
|
||||
$specs_to_return = $results['bundles'];
|
||||
$specs_to_save = null;
|
||||
|
||||
if ( ! empty( $allowed_bundles ) && ! in_array( $spec->key, $allowed_bundles, true ) ) {
|
||||
continue;
|
||||
$plugins = array_filter(
|
||||
$results['bundles'],
|
||||
function( $bundle ) {
|
||||
return count( $bundle['plugins'] ) > 0;
|
||||
}
|
||||
);
|
||||
|
||||
foreach ( $spec->plugins as $plugin ) {
|
||||
try {
|
||||
$extension = EvaluateExtension::evaluate( (object) $plugin );
|
||||
if ( ! property_exists( $extension, 'is_visible' ) || $extension->is_visible ) {
|
||||
$bundle['plugins'][] = $extension;
|
||||
}
|
||||
// phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
|
||||
} catch ( \Throwable $e ) {
|
||||
// Ignore errors.
|
||||
}
|
||||
}
|
||||
|
||||
$bundles[] = $bundle;
|
||||
if ( empty( $plugins ) ) {
|
||||
// When no plugins are visible, replace it with defaults and save for 3 hours.
|
||||
$specs_to_save = DefaultFreeExtensions::get_all();
|
||||
$specs_to_return = EvaluateExtension::evaluate_bundles( $specs_to_save, $allowed_bundles )['bundles'];
|
||||
} elseif ( count( $results['errors'] ) > 0 ) {
|
||||
// When suggestions is not empty but has errors, save it for 3 hours.
|
||||
$specs_to_save = $specs;
|
||||
}
|
||||
|
||||
return $bundles;
|
||||
// When plugins is not empty but has errors, save it for 3 hours.
|
||||
if ( count( $results['errors'] ) > 0 ) {
|
||||
self::log_errors( $results['errors'] );
|
||||
}
|
||||
|
||||
if ( $specs_to_save ) {
|
||||
RemoteFreeExtensionsDataSourcePoller::get_instance()->set_specs_transient( array( $locale => $specs_to_save ), 3 * HOUR_IN_SECONDS );
|
||||
}
|
||||
|
||||
return $specs_to_return;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -288,6 +288,8 @@ class WCAdminAssets {
|
||||
'wc-components',
|
||||
'wc-customer-effort-score',
|
||||
'wc-experimental',
|
||||
'wc-navigation',
|
||||
'wc-product-editor',
|
||||
WC_ADMIN_APP,
|
||||
);
|
||||
|
||||
|
||||
@@ -7,15 +7,14 @@ namespace Automattic\WooCommerce\Internal\Admin\WCPayPromotion;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use Automattic\WooCommerce\Admin\DataSourcePoller;
|
||||
use Automattic\WooCommerce\Admin\Features\PaymentGatewaySuggestions\EvaluateSuggestion;
|
||||
use Automattic\WooCommerce\Admin\Features\PaymentGatewaySuggestions\PaymentGatewaySuggestionsDataSourcePoller as PaymentGatewaySuggestionsDataSourcePoller;
|
||||
use Automattic\WooCommerce\Internal\Admin\WCAdminAssets;
|
||||
use Automattic\WooCommerce\Admin\RemoteSpecs\RemoteSpecsEngine;
|
||||
|
||||
/**
|
||||
* WC Pay Promotion engine.
|
||||
*/
|
||||
class Init {
|
||||
class Init extends RemoteSpecsEngine {
|
||||
const EXPLAT_VARIATION_PREFIX = 'woocommerce_wc_pay_promotion_payment_methods_table_';
|
||||
|
||||
/**
|
||||
@@ -30,8 +29,8 @@ class Init {
|
||||
}
|
||||
|
||||
add_filter( 'woocommerce_payment_gateways', array( __CLASS__, 'possibly_register_pre_install_wc_pay_promotion_gateway' ) );
|
||||
add_filter( 'option_woocommerce_gateway_order', [ __CLASS__, 'set_gateway_top_of_list' ] );
|
||||
add_filter( 'default_option_woocommerce_gateway_order', [ __CLASS__, 'set_gateway_top_of_list' ] );
|
||||
add_filter( 'option_woocommerce_gateway_order', array( __CLASS__, 'set_gateway_top_of_list' ) );
|
||||
add_filter( 'default_option_woocommerce_gateway_order', array( __CLASS__, 'set_gateway_top_of_list' ) );
|
||||
|
||||
$rtl = is_rtl() ? '.rtl' : '';
|
||||
|
||||
@@ -122,28 +121,19 @@ class Init {
|
||||
* Go through the specs and run them.
|
||||
*/
|
||||
public static function get_promotions() {
|
||||
$suggestions = array();
|
||||
$specs = self::get_specs();
|
||||
$locale = get_user_locale();
|
||||
|
||||
foreach ( $specs as $spec ) {
|
||||
try {
|
||||
$suggestion = EvaluateSuggestion::evaluate( $spec );
|
||||
$suggestions[] = $suggestion;
|
||||
// phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
|
||||
} catch ( \Throwable $e ) {
|
||||
// Ignore errors.
|
||||
}
|
||||
$specs = self::get_specs();
|
||||
$results = EvaluateSuggestion::evaluate_specs( $specs );
|
||||
|
||||
if ( count( $results['errors'] ) > 0 ) {
|
||||
// Unlike payment gateway suggestions, we don't have a non-empty default set of promotions to fall back to.
|
||||
// So just set the specs transient with expired time to 3 hours.
|
||||
WCPayPromotionDataSourcePoller::get_instance()->set_specs_transient( array( $locale => $specs ), 3 * HOUR_IN_SECONDS );
|
||||
self::log_errors( $results['errors'] );
|
||||
}
|
||||
|
||||
return array_values(
|
||||
array_filter(
|
||||
$suggestions,
|
||||
function( $suggestion ) {
|
||||
return ! property_exists( $suggestion, 'is_visible' ) || $suggestion->is_visible;
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
return $results['suggestions'];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,7 +11,6 @@ use Automattic\WooCommerce\Internal\BatchProcessing\BatchProcessingController;
|
||||
use Automattic\WooCommerce\Internal\Features\FeaturesController;
|
||||
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
|
||||
use Automattic\WooCommerce\Utilities\PluginUtil;
|
||||
use ActionScheduler;
|
||||
use WC_Admin_Settings;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
@@ -68,6 +67,13 @@ class CustomOrdersTableController {
|
||||
*/
|
||||
private $data_synchronizer;
|
||||
|
||||
/**
|
||||
* The data cleanup instance to use.
|
||||
*
|
||||
* @var LegacyDataCleanup
|
||||
*/
|
||||
private $data_cleanup;
|
||||
|
||||
/**
|
||||
* The batch processing controller to use.
|
||||
*
|
||||
@@ -116,7 +122,7 @@ class CustomOrdersTableController {
|
||||
private function init_hooks() {
|
||||
self::add_filter( 'woocommerce_order_data_store', array( $this, 'get_orders_data_store' ), 999, 1 );
|
||||
self::add_filter( 'woocommerce_order-refund_data_store', array( $this, 'get_refunds_data_store' ), 999, 1 );
|
||||
self::add_filter( 'woocommerce_debug_tools', array( $this, 'add_initiate_regeneration_entry_to_tools_array' ), 999, 1 );
|
||||
self::add_filter( 'woocommerce_debug_tools', array( $this, 'add_hpos_tools' ), 999 );
|
||||
self::add_filter( 'updated_option', array( $this, 'process_updated_option' ), 999, 3 );
|
||||
self::add_filter( 'pre_update_option', array( $this, 'process_pre_update_option' ), 999, 3 );
|
||||
self::add_action( 'woocommerce_after_register_post_type', array( $this, 'register_post_type_for_order_placeholders' ), 10, 0 );
|
||||
@@ -131,6 +137,7 @@ class CustomOrdersTableController {
|
||||
* @internal
|
||||
* @param OrdersTableDataStore $data_store The data store to use.
|
||||
* @param DataSynchronizer $data_synchronizer The data synchronizer to use.
|
||||
* @param LegacyDataCleanup $data_cleanup The legacy data cleanup instance to use.
|
||||
* @param OrdersTableRefundDataStore $refund_data_store The refund data store to use.
|
||||
* @param BatchProcessingController $batch_processing_controller The batch processing controller to use.
|
||||
* @param FeaturesController $features_controller The features controller instance to use.
|
||||
@@ -141,6 +148,7 @@ class CustomOrdersTableController {
|
||||
final public function init(
|
||||
OrdersTableDataStore $data_store,
|
||||
DataSynchronizer $data_synchronizer,
|
||||
LegacyDataCleanup $data_cleanup,
|
||||
OrdersTableRefundDataStore $refund_data_store,
|
||||
BatchProcessingController $batch_processing_controller,
|
||||
FeaturesController $features_controller,
|
||||
@@ -150,6 +158,7 @@ class CustomOrdersTableController {
|
||||
) {
|
||||
$this->data_store = $data_store;
|
||||
$this->data_synchronizer = $data_synchronizer;
|
||||
$this->data_cleanup = $data_cleanup;
|
||||
$this->batch_processing_controller = $batch_processing_controller;
|
||||
$this->refund_data_store = $refund_data_store;
|
||||
$this->features_controller = $features_controller;
|
||||
@@ -218,11 +227,15 @@ class CustomOrdersTableController {
|
||||
* @param array $tools_array The array of tools to add the tool to.
|
||||
* @return array The updated array of tools-
|
||||
*/
|
||||
private function add_initiate_regeneration_entry_to_tools_array( array $tools_array ): array {
|
||||
private function add_hpos_tools( array $tools_array ): array {
|
||||
if ( ! $this->data_synchronizer->check_orders_table_exists() ) {
|
||||
return $tools_array;
|
||||
}
|
||||
|
||||
// Cleanup tool.
|
||||
$tools_array = array_merge( $tools_array, $this->data_cleanup->get_tools_entries() );
|
||||
|
||||
// Delete HPOS tables tool.
|
||||
if ( $this->custom_orders_table_usage_is_enabled() || $this->data_synchronizer->data_sync_is_enabled() ) {
|
||||
$disabled = true;
|
||||
$message = __( 'This will delete the custom orders tables. The tables can be deleted only if the "High-Performance order storage" is not authoritative and sync is disabled (via Settings > Advanced > Features).', 'woocommerce' );
|
||||
@@ -336,6 +349,7 @@ class CustomOrdersTableController {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->data_cleanup->toggle_flag( false );
|
||||
$this->batch_processing_controller->enqueue_processor( DataSynchronizer::class );
|
||||
}
|
||||
|
||||
|
||||
@@ -281,6 +281,7 @@ class DataSynchronizer implements BatchProcessorInterface {
|
||||
}
|
||||
|
||||
if ( $this->data_sync_is_enabled() ) {
|
||||
wc_get_container()->get( LegacyDataCleanup::class )->toggle_flag( false );
|
||||
$this->batch_processing_controller->enqueue_processor( self::class );
|
||||
} else {
|
||||
$this->batch_processing_controller->remove_processor( self::class );
|
||||
|
||||
@@ -0,0 +1,326 @@
|
||||
<?php
|
||||
/**
|
||||
* LegacyDataCleanup class file.
|
||||
*/
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\DataStores\Orders;
|
||||
|
||||
use Automattic\WooCommerce\Internal\BatchProcessing\BatchProcessingController;
|
||||
use Automattic\WooCommerce\Internal\BatchProcessing\BatchProcessorInterface;
|
||||
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* This class handles the background process in charge of cleaning up legacy data for orders when HPOS is authoritative.
|
||||
*/
|
||||
class LegacyDataCleanup implements BatchProcessorInterface {
|
||||
|
||||
use AccessiblePrivateMethods;
|
||||
|
||||
/**
|
||||
* Option name for this feature.
|
||||
*/
|
||||
public const OPTION_NAME = 'woocommerce_hpos_legacy_data_cleanup_in_progress';
|
||||
|
||||
/**
|
||||
* The default number of orders to process per batch.
|
||||
*/
|
||||
private const BATCH_SIZE = 25;
|
||||
|
||||
/**
|
||||
* The batch processing controller to use.
|
||||
*
|
||||
* @var BatchProcessingController
|
||||
*/
|
||||
private $batch_processing;
|
||||
|
||||
/**
|
||||
* The legacy handler to use for the actual cleanup.
|
||||
*
|
||||
* @var LegacyHandler
|
||||
*/
|
||||
private $legacy_handler;
|
||||
|
||||
/**
|
||||
* The data synchronizer object to use.
|
||||
*
|
||||
* @var DataSynchronizer
|
||||
*/
|
||||
private $data_synchronizer;
|
||||
|
||||
/**
|
||||
* Logger object to be used to log events.
|
||||
*
|
||||
* @var \WC_Logger
|
||||
*/
|
||||
private $error_logger;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
self::add_filter( 'pre_update_option_' . self::OPTION_NAME, array( $this, 'pre_update_option' ), 999, 2 );
|
||||
self::add_action( 'add_option_' . self::OPTION_NAME, array( $this, 'process_added_option' ), 999, 2 );
|
||||
self::add_action( 'update_option_' . self::OPTION_NAME, array( $this, 'process_updated_option' ), 999, 2 );
|
||||
self::add_action( 'delete_option_' . self::OPTION_NAME, array( $this, 'process_deleted_option' ), 999 );
|
||||
self::add_action( 'shutdown', array( $this, 'maybe_reset_state' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Class initialization, invoked by the DI container.
|
||||
*
|
||||
* @param BatchProcessingController $batch_processing The batch processing controller to use.
|
||||
* @param LegacyDataHandler $legacy_handler Legacy order data handler instance.
|
||||
* @param DataSynchronizer $data_synchronizer Data synchronizer instance.
|
||||
* @internal
|
||||
*/
|
||||
final public function init( BatchProcessingController $batch_processing, LegacyDataHandler $legacy_handler, DataSynchronizer $data_synchronizer ) {
|
||||
$this->legacy_handler = $legacy_handler;
|
||||
$this->data_synchronizer = $data_synchronizer;
|
||||
$this->batch_processing = $batch_processing;
|
||||
$this->error_logger = wc_get_logger();
|
||||
}
|
||||
|
||||
/**
|
||||
* A user friendly name for this process.
|
||||
*
|
||||
* @return string Name of the process.
|
||||
*/
|
||||
public function get_name(): string {
|
||||
return 'Order legacy data cleanup';
|
||||
}
|
||||
|
||||
/**
|
||||
* A user friendly description for this process.
|
||||
*
|
||||
* @return string Description.
|
||||
*/
|
||||
public function get_description(): string {
|
||||
return 'Cleans up order data from legacy tables.';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total number of pending records that require update.
|
||||
*
|
||||
* @return int Number of pending records.
|
||||
*/
|
||||
public function get_total_pending_count(): int {
|
||||
return $this->should_run() ? $this->legacy_handler->count_orders_for_cleanup() : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the batch with records that needs to be processed for a given size.
|
||||
*
|
||||
* @param int $size Size of the batch.
|
||||
* @return array Batch of records.
|
||||
*/
|
||||
public function get_next_batch_to_process( int $size ): array {
|
||||
return $this->should_run()
|
||||
? array_map( 'absint', $this->legacy_handler->get_orders_for_cleanup( array(), $size ) )
|
||||
: array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Process data for current batch.
|
||||
*
|
||||
* @param array $batch Batch details.
|
||||
*/
|
||||
public function process_batch( array $batch ): void {
|
||||
// This is a destructive operation, so check if we need to bail out just in case.
|
||||
if ( ! $this->should_run() ) {
|
||||
$this->toggle_flag( false );
|
||||
return;
|
||||
}
|
||||
|
||||
$batch_failed = true;
|
||||
|
||||
foreach ( $batch as $order_id ) {
|
||||
try {
|
||||
$this->legacy_handler->cleanup_post_data( absint( $order_id ) );
|
||||
$batch_failed = false;
|
||||
} catch ( \Exception $e ) {
|
||||
$this->error_logger->error(
|
||||
sprintf(
|
||||
// translators: %1$d is an order ID, %2$s is an error message.
|
||||
__( 'Order %1$d legacy data could not be cleaned up during batch process. Error: %2$s', 'woocommerce' ),
|
||||
$order_id,
|
||||
$e->getMessage()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ( $batch_failed ) {
|
||||
$this->error_logger->error( __( 'Order legacy cleanup failed for an entire batch of orders. Aborting cleanup.', 'woocommerce' ) );
|
||||
}
|
||||
|
||||
if ( ! $this->orders_pending() || $batch_failed ) {
|
||||
$this->toggle_flag( false );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default batch size to use.
|
||||
*
|
||||
* @return int Default batch size.
|
||||
*/
|
||||
public function get_default_batch_size(): int {
|
||||
return self::BATCH_SIZE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the cleanup process can be initiated. Legacy data cleanup requires HPOS to be authoritative and
|
||||
* compatibility mode to be disabled.
|
||||
*
|
||||
* @return boolean TRUE if the cleanup process can be enabled, FALSE otherwise.
|
||||
*/
|
||||
public function can_run() {
|
||||
return $this->data_synchronizer->custom_orders_table_is_authoritative() && ! $this->data_synchronizer->data_sync_is_enabled() && ! $this->batch_processing->is_enqueued( get_class( $this->data_synchronizer ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the cleanup process should run. That is, it must be activated and {@see can_run()} must return TRUE.
|
||||
*
|
||||
* @return boolean TRUE if the cleanup process should be run, FALSE otherwise.
|
||||
*/
|
||||
public function should_run() {
|
||||
return $this->can_run() && $this->is_flag_set();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the user has initiated the cleanup process.
|
||||
*
|
||||
* @return boolean TRUE if the user has initiated the cleanup process, FALSE otherwise.
|
||||
*/
|
||||
public function is_flag_set() {
|
||||
return 'yes' === get_option( self::OPTION_NAME, 'no' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the flag that indicates that the cleanup process should be initiated.
|
||||
*
|
||||
* @param boolean $enabled TRUE if the process should be initiated, FALSE if it should be canceled.
|
||||
*/
|
||||
public function toggle_flag( bool $enabled ) {
|
||||
if ( $enabled ) {
|
||||
update_option( self::OPTION_NAME, wc_bool_to_string( $enabled ) );
|
||||
} else {
|
||||
delete_option( self::OPTION_NAME );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array in format required by 'woocommerce_debug_tools' to register the cleanup tool in WC.
|
||||
*
|
||||
* @return array Tools entries to register with WC.
|
||||
*/
|
||||
public function get_tools_entries() {
|
||||
$orders_for_cleanup_exist = ! empty( $this->legacy_handler->get_orders_for_cleanup( array(), 1 ) );
|
||||
$entry_id = $this->is_flag_set() ? 'hpos_legacy_cleanup_cancel' : 'hpos_legacy_cleanup';
|
||||
$entry = array(
|
||||
'name' => __( 'Clean up order data from legacy tables', 'woocommerce' ),
|
||||
'desc' => __( 'This tool will clear the data from legacy order tables in WooCommerce.', 'woocommerce' ),
|
||||
'requires_refresh' => true,
|
||||
'button' => __( 'Clear data', 'woocommerce' ),
|
||||
'disabled' => ! ( $this->can_run() && ( $orders_for_cleanup_exist || $this->is_flag_set() ) ),
|
||||
);
|
||||
|
||||
if ( ! $this->can_run() ) {
|
||||
$entry['desc'] .= '<br />';
|
||||
$entry['desc'] .= sprintf(
|
||||
'<strong class="red">%1$s</strong> %2$s',
|
||||
__( 'Note:', 'woocommerce' ),
|
||||
__( 'Only available when HPOS is authoritative and compatibility mode is disabled.', 'woocommerce' )
|
||||
);
|
||||
} else {
|
||||
if ( $this->is_flag_set() ) {
|
||||
$entry['status_text'] = sprintf(
|
||||
'%1$s %2$s',
|
||||
'<span class="dashicons dashicons-update spin"></span>',
|
||||
__( 'Clearing data...', 'woocommerce' )
|
||||
);
|
||||
$entry['button'] = __( 'Cancel', 'woocommerce' );
|
||||
$entry['callback'] = function() {
|
||||
$this->toggle_flag( false );
|
||||
return __( 'Order legacy data cleanup has been canceled.', 'woocommerce' );
|
||||
};
|
||||
} elseif ( ! $orders_for_cleanup_exist ) {
|
||||
$entry['button'] = __( 'No orders in need of cleanup', 'woocommerce' );
|
||||
} else {
|
||||
$entry['callback'] = function() {
|
||||
$this->toggle_flag( true );
|
||||
return __( 'Order legacy data cleanup process has been started.', 'woocommerce' );
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return array( $entry_id => $entry );
|
||||
}
|
||||
|
||||
/**
|
||||
* Hooked onto 'add_option' to enqueue the batch processor (if needed).
|
||||
*
|
||||
* @param string $option Name of the option to add.
|
||||
* @param mixed $value Value of the option.
|
||||
*/
|
||||
private function process_added_option( string $option, $value ) {
|
||||
$this->process_updated_option( false, $value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Hooked onto 'delete_option' to remove the batch processor.
|
||||
*/
|
||||
private function process_deleted_option() {
|
||||
$this->process_updated_option( false, false );
|
||||
}
|
||||
|
||||
/**
|
||||
* Hooked onto 'update_option' to enqueue the batch processor as needed.
|
||||
*
|
||||
* @param mixed $old_value Previous option value.
|
||||
* @param mixed $new_value New option value.
|
||||
*/
|
||||
private function process_updated_option( $old_value, $new_value ) {
|
||||
$enable = wc_string_to_bool( $new_value );
|
||||
|
||||
if ( $enable ) {
|
||||
$this->batch_processing->enqueue_processor( self::class );
|
||||
} else {
|
||||
$this->batch_processing->remove_processor( self::class );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hooked onto 'pre_update_option' to prevent enabling of the cleanup process when conditions aren't met.
|
||||
*
|
||||
* @param mixed $new_value New option value.
|
||||
* @param mixed $old_value Previous option value.
|
||||
*/
|
||||
private function pre_update_option( $new_value, $old_value ) {
|
||||
return $this->can_run() ? $new_value : 'no';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether there are any orders in need of cleanup and cleanup can run.
|
||||
*
|
||||
* @return bool TRUE if there are orders in need of cleanup, FALSE otherwise.
|
||||
*/
|
||||
private function orders_pending() {
|
||||
return ! empty( $this->get_next_batch_to_process( 1 ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Hooked onto 'shutdown' to clean up or set things straight in case of failures (timeouts, etc).
|
||||
*/
|
||||
private function maybe_reset_state() {
|
||||
$is_enqueued = $this->batch_processing->is_enqueued( self::class );
|
||||
$is_flag_set = $this->is_flag_set();
|
||||
|
||||
if ( $is_enqueued xor $is_flag_set ) {
|
||||
$this->toggle_flag( false );
|
||||
$this->batch_processing->remove_processor( self::class );
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
namespace Automattic\WooCommerce\Internal\DataStores\Orders;
|
||||
|
||||
use Automattic\WooCommerce\Database\Migrations\CustomOrderTable\PostsToOrdersMigrationController;
|
||||
use Automattic\WooCommerce\Utilities\ArrayUtil;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
@@ -28,17 +29,26 @@ class LegacyDataHandler {
|
||||
*/
|
||||
private DataSynchronizer $data_synchronizer;
|
||||
|
||||
/**
|
||||
* Instance of the PostsToOrdersMigrationController.
|
||||
*
|
||||
* @var PostsToOrdersMigrationController
|
||||
*/
|
||||
private PostsToOrdersMigrationController $posts_to_cot_migrator;
|
||||
|
||||
/**
|
||||
* Class initialization, invoked by the DI container.
|
||||
*
|
||||
* @param OrdersTableDataStore $data_store HPOS datastore instance to use.
|
||||
* @param DataSynchronizer $data_synchronizer DataSynchronizer instance to use.
|
||||
* @param OrdersTableDataStore $data_store HPOS datastore instance to use.
|
||||
* @param DataSynchronizer $data_synchronizer DataSynchronizer instance to use.
|
||||
* @param PostsToOrdersMigrationController $posts_to_cot_migrator Posts to HPOS migration controller instance to use.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final public function init( OrdersTableDataStore $data_store, DataSynchronizer $data_synchronizer ) {
|
||||
$this->data_store = $data_store;
|
||||
$this->data_synchronizer = $data_synchronizer;
|
||||
final public function init( OrdersTableDataStore $data_store, DataSynchronizer $data_synchronizer, PostsToOrdersMigrationController $posts_to_cot_migrator ) {
|
||||
$this->data_store = $data_store;
|
||||
$this->data_synchronizer = $data_synchronizer;
|
||||
$this->posts_to_cot_migrator = $posts_to_cot_migrator;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -151,10 +161,13 @@ class LegacyDataHandler {
|
||||
throw new \Exception( sprintf( __( 'Data in posts table appears to be more recent than in HPOS tables.', 'woocommerce' ) ) );
|
||||
}
|
||||
|
||||
$meta_ids = $wpdb->get_col( $wpdb->prepare( "SELECT meta_id FROM {$wpdb->postmeta} WHERE post_id = %d", $order->get_id() ) );
|
||||
foreach ( $meta_ids as $meta_id ) {
|
||||
delete_metadata_by_mid( 'post', $meta_id );
|
||||
}
|
||||
// Delete all metadata.
|
||||
$wpdb->query(
|
||||
$wpdb->prepare(
|
||||
"DELETE FROM {$wpdb->postmeta} WHERE post_id = %d",
|
||||
$order->get_id()
|
||||
)
|
||||
);
|
||||
|
||||
// wp_update_post() changes the post modified date, so we do this manually.
|
||||
// Also, we suspect using wp_update_post() could lead to integrations mistakenly updating the entity.
|
||||
@@ -208,7 +221,7 @@ class LegacyDataHandler {
|
||||
$diff = array();
|
||||
|
||||
$hpos_order = $this->get_order_from_datastore( $order_id, 'hpos' );
|
||||
$cpt_order = $this->get_order_from_datastore( $order_id, 'cpt' );
|
||||
$cpt_order = $this->get_order_from_datastore( $order_id, 'posts' );
|
||||
|
||||
if ( $hpos_order->get_type() !== $cpt_order->get_type() ) {
|
||||
$diff['type'] = array( $hpos_order->get_type(), $cpt_order->get_type() );
|
||||
@@ -252,8 +265,9 @@ class LegacyDataHandler {
|
||||
* @since 8.6.0
|
||||
*
|
||||
* @param int $order_id Order ID.
|
||||
* @param string $data_store_id Datastore to use. Should be either 'hpos' or 'cpt'. Defaults to 'hpos'.
|
||||
* @param string $data_store_id Datastore to use. Should be either 'hpos' or 'posts'. Defaults to 'hpos'.
|
||||
* @return \WC_Order Order instance.
|
||||
* @throws \Exception When an error occurs.
|
||||
*/
|
||||
public function get_order_from_datastore( int $order_id, string $data_store_id = 'hpos' ) {
|
||||
$data_store = ( 'hpos' === $data_store_id ) ? $this->data_store : $this->data_store->get_cpt_data_store_instance();
|
||||
@@ -265,7 +279,14 @@ class LegacyDataHandler {
|
||||
$data_store->prime_caches_for_orders( array( $order_id ), array() );
|
||||
}
|
||||
|
||||
$classname = wc_get_order_type( $data_store->get_order_type( $order_id ) )['class_name'];
|
||||
$order_type = wc_get_order_type( $data_store->get_order_type( $order_id ) );
|
||||
|
||||
if ( ! $order_type ) {
|
||||
// translators: %d is an order ID.
|
||||
throw new \Exception( sprintf( __( '%d is not an order or has an invalid order type.', 'woocommerce' ), $order_id ) );
|
||||
}
|
||||
|
||||
$classname = $order_type['class_name'];
|
||||
$order = new $classname();
|
||||
$order->set_id( $order_id );
|
||||
|
||||
@@ -292,6 +313,38 @@ class LegacyDataHandler {
|
||||
return $order;
|
||||
}
|
||||
|
||||
/**
|
||||
* Backfills an order from/to the CPT or HPOS datastore.
|
||||
*
|
||||
* @since 8.7.0
|
||||
*
|
||||
* @param int $order_id Order ID.
|
||||
* @param string $source_data_store Datastore to use as source. Should be either 'hpos' or 'posts'.
|
||||
* @param string $destination_data_store Datastore to use as destination. Should be either 'hpos' or 'posts'.
|
||||
* @return void
|
||||
* @throws \Exception When an error occurs.
|
||||
*/
|
||||
public function backfill_order_to_datastore( int $order_id, string $source_data_store, string $destination_data_store ) {
|
||||
$valid_data_stores = array( 'posts', 'hpos' );
|
||||
|
||||
if ( ! in_array( $source_data_store, $valid_data_stores, true ) || ! in_array( $destination_data_store, $valid_data_stores, true ) || $destination_data_store === $source_data_store ) {
|
||||
throw new \Exception( sprintf( 'Invalid datastore arguments: %1$s -> %2$s.', $source_data_store, $destination_data_store ) );
|
||||
}
|
||||
|
||||
$order = $this->get_order_from_datastore( $order_id, $source_data_store );
|
||||
|
||||
switch ( $destination_data_store ) {
|
||||
case 'posts':
|
||||
$order->get_data_store()->backfill_post_record( $order );
|
||||
break;
|
||||
case 'hpos':
|
||||
$this->posts_to_cot_migrator->migrate_orders( array( $order_id ) );
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all metadata in an order object as an array.
|
||||
*
|
||||
|
||||
@@ -2550,6 +2550,9 @@ FROM $order_meta_table
|
||||
}
|
||||
|
||||
$this->persist_order_to_db( $order );
|
||||
|
||||
$this->update_order_meta( $order );
|
||||
|
||||
$order->save_meta_data();
|
||||
|
||||
if ( $backfill ) {
|
||||
@@ -2882,6 +2885,8 @@ CREATE TABLE $meta_table (
|
||||
* @return bool
|
||||
*/
|
||||
public function delete_meta( &$object, $meta ) {
|
||||
global $wpdb;
|
||||
|
||||
if ( $this->should_backfill_post_record() && isset( $meta->id ) ) {
|
||||
// Let's get the actual meta key before its deleted for backfilling. We cannot delete just by ID because meta IDs are different in HPOS and posts tables.
|
||||
$db_meta = $this->data_store_meta->get_metadata_by_id( $meta->id );
|
||||
@@ -2896,7 +2901,23 @@ CREATE TABLE $meta_table (
|
||||
|
||||
if ( ! $changes_applied && $object instanceof WC_Abstract_Order && $this->should_backfill_post_record() && isset( $meta->key ) ) {
|
||||
self::$backfilling_order_ids[] = $object->get_id();
|
||||
delete_post_meta( $object->get_id(), $meta->key, $meta->value );
|
||||
if ( is_object( $meta->value ) && '__PHP_Incomplete_Class' === get_class( $meta->value ) ) {
|
||||
$meta_value = maybe_serialize( $meta->value );
|
||||
$wpdb->delete(
|
||||
_get_meta_table( 'post' ),
|
||||
array(
|
||||
'post_id' => $object->get_id(),
|
||||
'meta_key' => $meta->key, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
|
||||
'meta_value' => $meta_value, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
|
||||
),
|
||||
array( '%d', '%s', '%s' )
|
||||
);
|
||||
wp_cache_delete( $object->get_id(), 'post_meta' );
|
||||
$logger = wc_get_container()->get( LegacyProxy::class )->call_function( 'wc_get_logger' );
|
||||
$logger->warning( sprintf( 'encountered an order meta value of type __PHP_Incomplete_Class during `delete_meta` in order with ID %d: "%s"', $object->get_id(), var_export( $meta_value, true ) ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export
|
||||
} else {
|
||||
delete_post_meta( $object->get_id(), $meta->key, $meta->value );
|
||||
}
|
||||
self::$backfilling_order_ids = array_diff( self::$backfilling_order_ids, array( $object->get_id() ) );
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,13 @@ class ExtendedContainer extends BaseContainer {
|
||||
Container::class,
|
||||
);
|
||||
|
||||
/**
|
||||
* A list of tags that have already been fully resolved, see 'get' for details.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private array $known_tags = array();
|
||||
|
||||
/**
|
||||
* Register a class in the container.
|
||||
*
|
||||
@@ -155,6 +162,17 @@ class ExtendedContainer extends BaseContainer {
|
||||
throw new ContainerException( "Attempt to get an instance of the non-namespaced class '$id' from the container, did you forget to add a namespace import?" );
|
||||
}
|
||||
|
||||
// This is a workaround for an issue that arises when using service providers inheriting from AbstractInterfaceServiceProvider:
|
||||
// if one of these providers registers classes both by name and by tag, and one of its registered classes is requested
|
||||
// with 'get' by name before a list of classes is requested by tag, then that service provider gets locked as
|
||||
// the only one providing that tag, and the others get ignored. This is due to the fact that container definitions
|
||||
// are created "on the fly" as needed and the base 'get' method won't try to register additional providers
|
||||
// if the requested tag is already provided by at least one of the already existing definitions.
|
||||
if ( $this->definitions->hasTag( $id ) && ! in_array( $id, $this->known_tags, true ) && $this->providers->provides( $id ) ) {
|
||||
$this->providers->register( $id );
|
||||
$this->known_tags[] = $id;
|
||||
}
|
||||
|
||||
return parent::get( $id, $new );
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user