Plugin Updates

This commit is contained in:
Tony Volpe
2024-04-02 20:23:21 +00:00
parent 96800520e8
commit 94170ec2c4
1514 changed files with 133309 additions and 105985 deletions

View File

@@ -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 );
}

View File

@@ -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();

View File

@@ -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'] ) ) {

View File

@@ -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 ) {

View File

@@ -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.

View File

@@ -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' ) );
}

View File

@@ -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' ) );
}

View File

@@ -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' ) );
}

View File

@@ -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];

View File

@@ -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'] );

View File

@@ -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'] );

View File

@@ -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] )

View File

@@ -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 = '';

View File

@@ -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',

View 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;

View File

@@ -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;

View File

@@ -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,
)
);
}
/**

View File

@@ -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;
}

View File

@@ -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,
);
}
}

View File

@@ -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;
}
/**

View File

@@ -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',

View File

@@ -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.
*

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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.

View File

@@ -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 {

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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(

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -56,6 +56,12 @@ class PublishAfterTimeRuleProcessor implements RuleProcessorInterface {
return false;
}
try {
new \DateTime( $rule->publish_after );
} catch ( \Throwable $e ) {
return false;
}
return true;
}
}

View File

@@ -56,6 +56,12 @@ class PublishBeforeTimeRuleProcessor implements RuleProcessorInterface {
return false;
}
try {
new \DateTime( $rule->publish_before );
} catch ( \Throwable $e ) {
return false;
}
return true;
}
}

View File

@@ -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 );
}
}

View File

@@ -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.

View File

@@ -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
);
}
}

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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,

View File

@@ -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 );
}

View File

@@ -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 ];

View File

@@ -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 );
}

View File

@@ -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 );
}

View File

@@ -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 ];
}

View File

@@ -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'];
}
/**

View File

@@ -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;
}

View File

@@ -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,
),
);
}
}