Merge branch 'master' into add/woorelease-support

This commit is contained in:
Claudio Sanches 2020-08-18 17:59:47 -03:00
commit 9773675b8d
85 changed files with 6645 additions and 12274 deletions

2
.gitignore vendored
View File

@ -49,6 +49,8 @@ tests/cli/vendor
/tests/e2e/env/docker/wp-cli/initialize.sh
/tests/e2e/env/build/
/tests/e2e/env/build-module/
/tests/e2e/utils/build/
/tests/e2e/utils/build-module/
# Logs
/logs

View File

@ -6163,6 +6163,18 @@ table.bar_chart {
}
}
.post-type-product {
#wp-pointer-2 .wp-pointer-arrow {
left: 240px;
}
#wp-pointer-3 .wp-pointer-arrow,
#wp-pointer-4 .wp-pointer-arrow {
left: 46%;
}
}
/**
* Small screen optimisation
*/

View File

@ -58,8 +58,10 @@
* Fix - After clicking to update WooCommerce, the user will stay in the same page instead of being redirected to the "Settings" page. #27172
* Fix - "Product type" dropdown missing from Product's data meta box on WP 5.5. #27170
* Fix - Removed the JETPACK_AUTOLOAD_DEV define. #27185
* Dev - Update WooCommerce Admin version to v1.4.0-beta.3. #27214
* Dev - Upgraded to the 2.x Jetpack Autoloader. #27123
* Fix - Fixed "virtual" and "downlodable" pointers on product walkthrough. #27145
* Fix - Updated tested up to for WordPress 5.5. #27334
* Dev - Update WooCommerce Admin version to v1.4.0. #27378
* Dev - Upgraded to v2.2 of Jetpack Autoloader. #27358
* Dev - Update jest-preset-default version to ^6.2.0. #27090
* Dev - Added a second $existing_meta_keys parameter to the woocommerce_duplicate_product_exclude_meta filter. #27038
* Dev - Remove leftover note for translators in customer-completed-order.php. #26989
@ -85,14 +87,18 @@
* Dev - Ensure wc_load_cart loads its own dependencies. #26219
* Dev - Clean up deprecated documentation. #27054
* Dev - Update WooCommerce Blocks version to 3.1.0. #27177
* Dev - Added woocommerce_order_item_quantity filter to ReserveStock::reserve_stock_for_order(). #27251
* Dev - Updated docs to make the type in docblock more specific. #27285
**REST API 1.0.11**
**REST API 1.0.15**
* Enhancement - Introduced X-WP-Total header for product attributes GET endpoint listing the number of entries in the response. woocommerce/woocommerce-rest-api#171
* Enhancement - Introduced X-WP-TotalPages header for product attributes GET endpoint listing the number of pages that can be fetched. woocommerce/woocommerce-rest-api#171
* Enhancement - Introduced the modified option for orderby fetch requests in post based resources. woocommerce/woocommerce-rest-api#226
* Enhancement - Compatibility fixes for WordPress 5.5. woocommerce/woocommerce-rest-api#232
* Fix - Ensured Action Scheduler transients are cleared by "Clear Transients" tool. woocommerce/woocommerce-rest-api#152
* Fix - Corrected the schema datatype for coupon expiry_date, date_expires, and date_expires_gmt fields. woocommerce/woocommerce-rest-api#176
* Fix - Query parameters are now passed correctly when using the batch product variation endpoints. woocommerce/woocommerce-rest-api#191
* Fix - Fix regression and restore backward compatibility for date-time and mixed data types. woocommerce/woocommerce-rest-api#238
**WooCommerce Admin 1.4.0**
* Enhancement - Move the WooCommerce > Coupons dashboard menu item to Marketing > Coupons. #4786
@ -107,9 +113,15 @@
* Fix - Polyfill core-data saveUser() on WP 5.3.x. #4869
* Fix - Product types step bugs in onboarding wizard. #4900
* Fix - Center all descriptive text on onboarding wizard steps. #4902
* Fix - Match the requires version to the exact WordPress version number in readme.txt. #4956
* Fix - Change account required text on biz step in onboarding wizard. #4909
* Fix - Fix industry args type in REST API. #4974
* Fix - Update style on shipping banner. #4948
* Fix - CSS Fixes for Business Features Popover ( parts 1&2 ). #4994
* Dev - Add the experimental resolver to WCA data package. #4862
* Dev - Fix linter errors. #4904
* Dev - Fix usage of "package" tag in file headers. #4940
* Dev - Update Jetpack Autoloader to match Woo Core. #4993
**WooCommerce Blocks 3.0.0**
* Build - Updated the automattic/jetpack-autoloader package to the 2.0 branch. #2847

View File

@ -8,19 +8,19 @@
"minimum-stability": "dev",
"require": {
"php": ">=7.0",
"automattic/jetpack-autoloader": "2.0.2",
"automattic/jetpack-autoloader": "2.2.0",
"automattic/jetpack-constants": "1.4.0",
"composer/installers": "1.7.0",
"league/container": "3.3.1",
"maxmind-db/reader": "1.6.0",
"pelago/emogrifier": "3.1.0",
"woocommerce/action-scheduler": "3.1.6",
"woocommerce/woocommerce-admin": "1.4.0-beta.3",
"woocommerce/woocommerce-admin": "1.5.0-rc.1",
"woocommerce/woocommerce-blocks": "3.1.0"
},
"require-dev": {
"phpunit/phpunit": "7.5.20",
"woocommerce/woocommerce-sniffs": "0.0.10",
"woocommerce/woocommerce-sniffs": "^0.1.0",
"wp-cli/i18n-command": "^2.2"
},
"config": {

130
composer.lock generated
View File

@ -4,20 +4,20 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "ae4abaa8d39e860cc6c379cb5f6a0c2f",
"content-hash": "42e4de440843075e3074f98fa1ef5b61",
"packages": [
{
"name": "automattic/jetpack-autoloader",
"version": "v2.0.2",
"version": "v2.2.0",
"source": {
"type": "git",
"url": "https://github.com/Automattic/jetpack-autoloader.git",
"reference": "4502da4b2443fc1b61389cacc94c34876aca2b3d"
"reference": "66a5d150b3928be718d86696f85631a7f0b98a7b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Automattic/jetpack-autoloader/zipball/4502da4b2443fc1b61389cacc94c34876aca2b3d",
"reference": "4502da4b2443fc1b61389cacc94c34876aca2b3d",
"url": "https://api.github.com/repos/Automattic/jetpack-autoloader/zipball/66a5d150b3928be718d86696f85631a7f0b98a7b",
"reference": "66a5d150b3928be718d86696f85631a7f0b98a7b",
"shasum": ""
},
"require": {
@ -40,7 +40,7 @@
"GPL-2.0-or-later"
],
"description": "Creates a custom autoloader for a plugin or theme.",
"time": "2020-07-09T13:18:38+00:00"
"time": "2020-08-14T20:34:36+00:00"
},
{
"name": "automattic/jetpack-constants",
@ -259,12 +259,6 @@
"provider",
"service"
],
"funding": [
{
"url": "https://github.com/philipobenito",
"type": "github"
}
],
"time": "2020-05-18T08:20:23+00:00"
},
{
@ -501,20 +495,6 @@
],
"description": "Symfony CssSelector Component",
"homepage": "https://symfony.com",
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2020-03-16T08:31:04+00:00"
},
{
@ -554,16 +534,16 @@
},
{
"name": "woocommerce/woocommerce-admin",
"version": "v1.4.0-beta.3",
"version": "1.5.0-rc.1",
"source": {
"type": "git",
"url": "https://github.com/woocommerce/woocommerce-admin.git",
"reference": "df2af46a8552cdee15df0030fccbe4cd5a6d270d"
"reference": "97a540e2e114b8c7566dc97109ba04536f905de0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/woocommerce/woocommerce-admin/zipball/df2af46a8552cdee15df0030fccbe4cd5a6d270d",
"reference": "df2af46a8552cdee15df0030fccbe4cd5a6d270d",
"url": "https://api.github.com/repos/woocommerce/woocommerce-admin/zipball/97a540e2e114b8c7566dc97109ba04536f905de0",
"reference": "97a540e2e114b8c7566dc97109ba04536f905de0",
"shasum": ""
},
"require": {
@ -597,7 +577,7 @@
],
"description": "A modern, javascript-driven WooCommerce Admin experience.",
"homepage": "https://github.com/woocommerce/woocommerce-admin",
"time": "2020-08-04T02:21:47+00:00"
"time": "2020-08-18T03:05:31+00:00"
},
{
"name": "woocommerce/woocommerce-blocks",
@ -650,22 +630,22 @@
"packages-dev": [
{
"name": "dealerdirect/phpcodesniffer-composer-installer",
"version": "v0.6.2",
"version": "v0.7.0",
"source": {
"type": "git",
"url": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer.git",
"reference": "8001af8eb107fbfcedc31a8b51e20b07d85b457a"
"reference": "e8d808670b8f882188368faaf1144448c169c0b7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Dealerdirect/phpcodesniffer-composer-installer/zipball/8001af8eb107fbfcedc31a8b51e20b07d85b457a",
"reference": "8001af8eb107fbfcedc31a8b51e20b07d85b457a",
"url": "https://api.github.com/repos/Dealerdirect/phpcodesniffer-composer-installer/zipball/e8d808670b8f882188368faaf1144448c169c0b7",
"reference": "e8d808670b8f882188368faaf1144448c169c0b7",
"shasum": ""
},
"require": {
"composer-plugin-api": "^1.0",
"php": "^5.3|^7",
"squizlabs/php_codesniffer": "^2|^3"
"composer-plugin-api": "^1.0 || ^2.0",
"php": ">=5.3",
"squizlabs/php_codesniffer": "^2 || ^3 || 4.0.x-dev"
},
"require-dev": {
"composer/composer": "*",
@ -712,7 +692,7 @@
"stylecheck",
"tests"
],
"time": "2020-01-29T20:22:20+00:00"
"time": "2020-06-25T14:57:39+00:00"
},
{
"name": "doctrine/instantiator",
@ -768,20 +748,6 @@
"constructor",
"instantiate"
],
"funding": [
{
"url": "https://www.doctrine-project.org/sponsorship.html",
"type": "custom"
},
{
"url": "https://www.patreon.com/phpdoctrine",
"type": "patreon"
},
{
"url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator",
"type": "tidelift"
}
],
"time": "2020-05-29T17:27:14+00:00"
},
{
@ -1044,12 +1010,6 @@
"object",
"object graph"
],
"funding": [
{
"url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
"type": "tidelift"
}
],
"time": "2020-06-29T13:22:24+00:00"
},
{
@ -2478,16 +2438,16 @@
},
{
"name": "squizlabs/php_codesniffer",
"version": "3.5.5",
"version": "3.5.6",
"source": {
"type": "git",
"url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
"reference": "73e2e7f57d958e7228fce50dc0c61f58f017f9f6"
"reference": "e97627871a7eab2f70e59166072a6b767d5834e0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/73e2e7f57d958e7228fce50dc0c61f58f017f9f6",
"reference": "73e2e7f57d958e7228fce50dc0c61f58f017f9f6",
"url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/e97627871a7eab2f70e59166072a6b767d5834e0",
"reference": "e97627871a7eab2f70e59166072a6b767d5834e0",
"shasum": ""
},
"require": {
@ -2525,7 +2485,7 @@
"phpcs",
"standards"
],
"time": "2020-04-17T01:09:41+00:00"
"time": "2020-08-10T04:50:15+00:00"
},
{
"name": "symfony/finder",
@ -2574,20 +2534,6 @@
],
"description": "Symfony Finder Component",
"homepage": "https://symfony.com",
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2020-02-14T07:34:21+00:00"
},
{
@ -2743,23 +2689,23 @@
},
{
"name": "woocommerce/woocommerce-sniffs",
"version": "0.0.10",
"version": "0.1.0",
"source": {
"type": "git",
"url": "https://github.com/woocommerce/woocommerce-sniffs.git",
"reference": "b0e3d69a53b3ffdbb97a0371bd1b43aa17092d65"
"reference": "b72b7dd2e70aa6aed16f80cdae5b1e6cce2e4c79"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/woocommerce/woocommerce-sniffs/zipball/b0e3d69a53b3ffdbb97a0371bd1b43aa17092d65",
"reference": "b0e3d69a53b3ffdbb97a0371bd1b43aa17092d65",
"url": "https://api.github.com/repos/woocommerce/woocommerce-sniffs/zipball/b72b7dd2e70aa6aed16f80cdae5b1e6cce2e4c79",
"reference": "b72b7dd2e70aa6aed16f80cdae5b1e6cce2e4c79",
"shasum": ""
},
"require": {
"dealerdirect/phpcodesniffer-composer-installer": "0.6.2",
"dealerdirect/phpcodesniffer-composer-installer": "0.7.0",
"php": ">=7.0",
"phpcompatibility/phpcompatibility-wp": "2.1.0",
"wp-coding-standards/wpcs": "2.2.1"
"wp-coding-standards/wpcs": "2.3.0"
},
"type": "phpcodesniffer-standard",
"notification-url": "https://packagist.org/downloads/",
@ -2779,7 +2725,7 @@
"woocommerce",
"wordpress"
],
"time": "2020-04-07T20:25:44+00:00"
"time": "2020-08-06T18:23:45+00:00"
},
{
"name": "wp-cli/i18n-command",
@ -3000,16 +2946,16 @@
},
{
"name": "wp-coding-standards/wpcs",
"version": "2.2.1",
"version": "2.3.0",
"source": {
"type": "git",
"url": "https://github.com/WordPress/WordPress-Coding-Standards.git",
"reference": "b5a453203114cc2284b1a614c4953456fbe4f546"
"reference": "7da1894633f168fe244afc6de00d141f27517b62"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/b5a453203114cc2284b1a614c4953456fbe4f546",
"reference": "b5a453203114cc2284b1a614c4953456fbe4f546",
"url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/7da1894633f168fe244afc6de00d141f27517b62",
"reference": "7da1894633f168fe244afc6de00d141f27517b62",
"shasum": ""
},
"require": {
@ -3019,6 +2965,7 @@
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "^0.5 || ^0.6",
"phpcompatibility/php-compatibility": "^9.0",
"phpcsstandards/phpcsdevtools": "^1.0",
"phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0"
},
"suggest": {
@ -3041,7 +2988,7 @@
"standards",
"wordpress"
],
"time": "2020-02-04T02:52:06+00:00"
"time": "2020-05-13T23:57:56+00:00"
}
],
"aliases": [],
@ -3055,6 +3002,5 @@
"platform-dev": [],
"platform-overrides": {
"php": "7.1"
},
"plugin-api-version": "1.1.0"
}
}

View File

@ -2,10 +2,8 @@
/**
* Setup customize items.
*
* @author WooCommerce
* @category Admin
* @package WooCommerce\Admin\Customize
* @version 3.1.0
* @package WooCommerce\Admin\Customize
* @version 3.1.0
*/
if ( ! defined( 'ABSPATH' ) ) {

View File

@ -2,10 +2,8 @@
/**
* Adds and controls pointers for contextual help/tutorials
*
* @author WooThemes
* @category Admin
* @package WooCommerce\Admin
* @version 2.4.0
* @package WooCommerce\Admin\Pointers
* @version 2.4.0
*/
if ( ! defined( 'ABSPATH' ) ) {
@ -28,7 +26,9 @@ class WC_Admin_Pointers {
* Setup pointers for screen.
*/
public function setup_pointers_for_screen() {
if ( ! $screen = get_current_screen() ) {
$screen = get_current_screen();
if ( ! $screen ) {
return;
}
@ -43,9 +43,10 @@ class WC_Admin_Pointers {
* Pointers for creating a product.
*/
public function create_product_tutorial() {
if ( ! isset( $_GET['tutorial'] ) || ! current_user_can( 'manage_options' ) ) {
if ( ! isset( $_GET['tutorial'] ) || ! current_user_can( 'manage_options' ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
return;
}
// These pointers will chain - they will not be shown at once.
$pointers = array(
'pointers' => array(
@ -218,7 +219,7 @@ class WC_Admin_Pointers {
/**
* Enqueue pointers and add script to page.
*
* @param array $pointers
* @param array $pointers Pointers data.
*/
public function enqueue_pointers( $pointers ) {
$pointers = rawurlencode( wp_json_encode( $pointers ) );

View File

@ -1022,6 +1022,95 @@ class WC_Cart extends WC_Legacy_Cart {
return false;
}
if ( $product_data->is_type( 'variation' ) ) {
$missing_attributes = array();
$parent_data = wc_get_product( $product_data->get_parent_id() );
$variation_attributes = $product_data->get_variation_attributes();
// Filter out 'any' variations, which are empty, as they need to be explicitly specified while adding to cart.
$variation_attributes = array_filter( $variation_attributes );
// Gather posted attributes.
$posted_attributes = array();
foreach ( $parent_data->get_attributes() as $attribute ) {
if ( ! $attribute['is_variation'] ) {
continue;
}
$attribute_key = 'attribute_' . sanitize_title( $attribute['name'] );
if ( isset( $variation[ $attribute_key ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( $attribute['is_taxonomy'] ) {
// Don't use wc_clean as it destroys sanitized characters.
$value = sanitize_title( wp_unslash( $variation[ $attribute_key ] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
} else {
$value = html_entity_decode( wc_clean( wp_unslash( $variation[ $attribute_key ] ) ), ENT_QUOTES, get_bloginfo( 'charset' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
}
// Don't include if it's empty.
if ( ! empty( $value ) ) {
$posted_attributes[ $attribute_key ] = $value;
}
}
}
// Merge variation attributes and posted attributes.
$posted_and_variation_attributes = array_merge( $variation_attributes, $posted_attributes );
// If no variation ID is set, attempt to get a variation ID from posted attributes.
if ( empty( $variation_id ) ) {
$data_store = WC_Data_Store::load( 'product' );
$variation_id = $data_store->find_matching_product_variation( $parent_data, $posted_attributes );
}
// Do we have a variation ID?
if ( empty( $variation_id ) ) {
throw new Exception( __( 'Please choose product options…', 'woocommerce' ) );
}
// Check the data we have is valid.
$variation_data = wc_get_product_variation_attributes( $variation_id );
$attributes = array();
foreach ( $parent_data->get_attributes() as $attribute ) {
if ( ! $attribute['is_variation'] ) {
continue;
}
// Get valid value from variation data.
$attribute_key = 'attribute_' . sanitize_title( $attribute['name'] );
$valid_value = isset( $variation_data[ $attribute_key ] ) ? $variation_data[ $attribute_key ] : '';
/**
* If the attribute value was posted, check if it's valid.
*
* If no attribute was posted, only error if the variation has an 'any' attribute which requires a value.
*/
if ( isset( $posted_and_variation_attributes[ $attribute_key ] ) ) {
$value = $posted_and_variation_attributes[ $attribute_key ];
// Allow if valid or show error.
if ( $valid_value === $value ) {
$attributes[ $attribute_key ] = $value;
} elseif ( '' === $valid_value && in_array( $value, $attribute->get_slugs(), true ) ) {
// If valid values are empty, this is an 'any' variation so get all possible values.
$attributes[ $attribute_key ] = $value;
} else {
/* translators: %s: Attribute name. */
throw new Exception( sprintf( __( 'Invalid value posted for %s', 'woocommerce' ), wc_attribute_label( $attribute['name'] ) ) );
}
} elseif ( '' === $valid_value ) {
$missing_attributes[] = wc_attribute_label( $attribute['name'] );
}
$variation = $attributes;
}
if ( ! empty( $missing_attributes ) ) {
/* translators: %s: Attribute name. */
throw new Exception( sprintf( _n( '%s is a required field', '%s are required fields', count( $missing_attributes ), 'woocommerce' ), wc_format_list_of_items( $missing_attributes ) ) );
}
}
// Load cart item data - may be added by other plugins.
$cart_item_data = (array) apply_filters( 'woocommerce_add_cart_item_data', $cart_item_data, $product_id, $variation_id, $quantity );
@ -1039,9 +1128,11 @@ class WC_Cart extends WC_Legacy_Cart {
if ( $found_in_cart ) {
/* translators: %s: product name */
$message = sprintf( __( 'You cannot add another "%s" to your cart.', 'woocommerce' ), $product_data->get_name() );
/**
* Filters message about more than 1 product being added to cart.
*
* @since 4.5.0
* @param string $message Message.
* @param WC_Product $product_data Product data.
*/
@ -1068,9 +1159,11 @@ class WC_Cart extends WC_Legacy_Cart {
if ( ! $product_data->is_in_stock() ) {
/* translators: %s: product name */
$message = sprintf( __( 'You cannot add "%s" to the cart because the product is out of stock.', 'woocommerce' ), $product_data->get_name() );
/**
* Filters message about product being out of stock.
*
* @since 4.5.0
* @param string $message Message.
* @param WC_Product $product_data Product data.
*/
@ -1083,9 +1176,11 @@ class WC_Cart extends WC_Legacy_Cart {
/* translators: 1: product name 2: quantity in stock */
$message = sprintf( __( 'You cannot add that amount of "%1$s" to the cart because there is not enough stock (%2$s remaining).', 'woocommerce' ), $product_data->get_name(), wc_format_stock_quantity_for_display( $stock_quantity, $product_data ) );
/**
* Filters message about product not having enough stock.
*
* @since 4.5.0
* @param string $message Message.
* @param WC_Product $product_data Product data.
* @param int $stock_quantity Quantity remaining.
@ -1425,10 +1520,8 @@ class WC_Cart extends WC_Legacy_Cart {
}
if ( 'yes' === get_option( 'woocommerce_shipping_cost_requires_address' ) ) {
if ( ! $this->get_customer()->has_calculated_shipping() ) {
if ( ! $this->get_customer()->get_shipping_country() || ( ! $this->get_customer()->get_shipping_state() && ! $this->get_customer()->get_shipping_postcode() ) ) {
return false;
}
if ( ! $this->get_customer()->get_shipping_country() || ! $this->get_customer()->get_shipping_state() || ! $this->get_customer()->get_shipping_postcode() ) {
return false;
}
}
@ -1509,7 +1602,7 @@ class WC_Cart extends WC_Legacy_Cart {
if ( 0 < $coupon_usage_limit && 0 === get_current_user_id() ) {
// For guest, usage per user has not been enforced yet. Enforce it now.
$coupon_data_store = $coupon->get_data_store();
$billing_email = strtolower( sanitize_email( $billing_email ) );
$billing_email = strtolower( sanitize_email( $billing_email ) );
if ( $coupon_data_store && $coupon_data_store->get_usage_by_email( $coupon, $billing_email ) >= $coupon_usage_limit ) {
$coupon->add_coupon_message( WC_Coupon::E_WC_COUPON_USAGE_LIMIT_REACHED );
}

View File

@ -357,6 +357,7 @@ class WC_Comments {
WHERE comment_parent = 0
AND comment_post_ID = %d
AND comment_approved = '1'
AND comment_type = 'review'
",
$product->get_id()
)

View File

@ -215,10 +215,12 @@ class WC_Download_Handler {
$filename = current( explode( '?', $filename ) );
}
$filename = apply_filters( 'woocommerce_file_download_filename', $filename, $product_id );
$filename = apply_filters( 'woocommerce_file_download_filename', $filename, $product_id );
/**
* Filter download method.
*
*
* @since 4.5.0
* @param string $method Download method.
* @param int $product_id Product ID.
* @param string $file_path URL to file.

View File

@ -46,7 +46,7 @@ class WC_Form_Handler {
$user = get_user_by( 'login', sanitize_user( wp_unslash( $_GET['login'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$user_id = $user ? $user->ID : 0;
} else {
$user_id = absint( $_GET['id'] );
$user_id = absint( $_GET['id'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
}
$value = sprintf( '%d:%s', $user_id, wp_unslash( $_GET['key'] ) ); // phpcs:ignore
@ -638,7 +638,7 @@ class WC_Form_Handler {
if ( ( ! empty( $_POST['apply_coupon'] ) || ! empty( $_POST['update_cart'] ) || ! empty( $_POST['proceed'] ) ) && wp_verify_nonce( $nonce_value, 'woocommerce-cart' ) ) {
$cart_updated = false;
$cart_totals = isset( $_POST['cart'] ) ? wp_unslash( $_POST['cart'] ) : ''; // PHPCS: input var ok, CSRF ok, sanitization ok.
$cart_totals = isset( $_POST['cart'] ) ? wp_unslash( $_POST['cart'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
if ( ! WC()->cart->is_empty() && is_array( $cart_totals ) ) {
foreach ( WC()->cart->get_cart() as $cart_item_key => $values ) {
@ -868,108 +868,16 @@ class WC_Form_Handler {
* @return bool success or not
*/
private static function add_to_cart_handler_variable( $product_id ) {
try {
$variation_id = empty( $_REQUEST['variation_id'] ) ? '' : absint( wp_unslash( $_REQUEST['variation_id'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$quantity = empty( $_REQUEST['quantity'] ) ? 1 : wc_stock_amount( wp_unslash( $_REQUEST['quantity'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$missing_attributes = array();
$variations = array();
$variation_attributes = array();
$adding_to_cart = wc_get_product( $product_id );
$variation_id = empty( $_REQUEST['variation_id'] ) ? '' : absint( wp_unslash( $_REQUEST['variation_id'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$quantity = empty( $_REQUEST['quantity'] ) ? 1 : wc_stock_amount( wp_unslash( $_REQUEST['quantity'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$variations = array();
if ( ! $adding_to_cart ) {
return false;
foreach ( $_REQUEST as $key => $value ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( 'attribute_' !== substr( $key, 0, 10 ) ) {
continue;
}
// If the $product_id was in fact a variation ID, update the variables.
if ( $adding_to_cart->is_type( 'variation' ) ) {
$variation_attributes = $adding_to_cart->get_variation_attributes();
// Filter out 'any' variations, which are empty, as they need to be explicitly specified while adding to cart.
$variation_attributes = array_filter( $variation_attributes );
$variation_id = $product_id;
$product_id = $adding_to_cart->get_parent_id();
$adding_to_cart = wc_get_product( $product_id );
if ( ! $adding_to_cart ) {
return false;
}
}
// Gather posted attributes.
$posted_attributes = array();
foreach ( $adding_to_cart->get_attributes() as $attribute ) {
if ( ! $attribute['is_variation'] ) {
continue;
}
$attribute_key = 'attribute_' . sanitize_title( $attribute['name'] );
if ( isset( $_REQUEST[ $attribute_key ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( $attribute['is_taxonomy'] ) {
// Don't use wc_clean as it destroys sanitized characters.
$value = sanitize_title( wp_unslash( $_REQUEST[ $attribute_key ] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
} else {
$value = html_entity_decode( wc_clean( wp_unslash( $_REQUEST[ $attribute_key ] ) ), ENT_QUOTES, get_bloginfo( 'charset' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
}
$posted_attributes[ $attribute_key ] = $value;
}
}
// Merge variation attributes and posted attributes.
$posted_and_variation_attributes = array_merge( $variation_attributes, $posted_attributes );
// If no variation ID is set, attempt to get a variation ID from posted attributes.
if ( empty( $variation_id ) ) {
$data_store = WC_Data_Store::load( 'product' );
$variation_id = $data_store->find_matching_product_variation( $adding_to_cart, $posted_attributes );
}
// Do we have a variation ID?
if ( empty( $variation_id ) ) {
throw new Exception( __( 'Please choose product options&hellip;', 'woocommerce' ) );
}
// Check the data we have is valid.
$variation_data = wc_get_product_variation_attributes( $variation_id );
foreach ( $adding_to_cart->get_attributes() as $attribute ) {
if ( ! $attribute['is_variation'] ) {
continue;
}
// Get valid value from variation data.
$attribute_key = 'attribute_' . sanitize_title( $attribute['name'] );
$valid_value = isset( $variation_data[ $attribute_key ] ) ? $variation_data[ $attribute_key ] : '';
/**
* If the attribute value was posted, check if it's valid.
*
* If no attribute was posted, only error if the variation has an 'any' attribute which requires a value.
*/
if ( isset( $posted_and_variation_attributes[ $attribute_key ] ) ) {
$value = $posted_and_variation_attributes[ $attribute_key ];
// Allow if valid or show error.
if ( $valid_value === $value ) {
$variations[ $attribute_key ] = $value;
} elseif ( '' === $valid_value && in_array( $value, $attribute->get_slugs(), true ) ) {
// If valid values are empty, this is an 'any' variation so get all possible values.
$variations[ $attribute_key ] = $value;
} else {
/* translators: %s: Attribute name. */
throw new Exception( sprintf( __( 'Invalid value posted for %s', 'woocommerce' ), wc_attribute_label( $attribute['name'] ) ) );
}
} elseif ( '' === $valid_value ) {
$missing_attributes[] = wc_attribute_label( $attribute['name'] );
}
}
if ( ! empty( $missing_attributes ) ) {
/* translators: %s: Attribute name. */
throw new Exception( sprintf( _n( '%s is a required field', '%s are required fields', count( $missing_attributes ), 'woocommerce' ), wc_format_list_of_items( $missing_attributes ) ) );
}
} catch ( Exception $e ) {
wc_add_notice( $e->getMessage(), 'error' );
return false;
$variations[ sanitize_title( wp_unslash( $key ) ) ] = wp_unslash( $value );
}
$passed_validation = apply_filters( 'woocommerce_add_to_cart_validation', true, $product_id, $quantity, $variation_id, $variations );
@ -1083,7 +991,7 @@ class WC_Form_Handler {
return;
}
if ( in_array( $field, array( 'password_1', 'password_2' ) ) ) {
if ( in_array( $field, array( 'password_1', 'password_2' ), true ) ) {
// Don't unslash password fields
// @see https://github.com/woocommerce/woocommerce/issues/23922.
$posted_fields[ $field ] = $_POST[ $field ]; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash

View File

@ -753,15 +753,8 @@ class WC_Install {
// Add constraint to download logs if the columns matches.
if ( ! empty( $download_permissions_column_type ) && ! empty( $download_log_column_type ) && $download_permissions_column_type === $download_log_column_type ) {
$fk_result = $wpdb->get_row(
"SELECT COUNT(*) AS fk_count
FROM information_schema.TABLE_CONSTRAINTS
WHERE CONSTRAINT_SCHEMA = '{$wpdb->dbname}'
AND CONSTRAINT_NAME = 'fk_{$wpdb->prefix}wc_download_log_permission_id'
AND CONSTRAINT_TYPE = 'FOREIGN KEY'
AND TABLE_NAME = '{$wpdb->prefix}wc_download_log'"
);
if ( 0 === (int) $fk_result->fk_count ) {
$fk_result = $wpdb->get_row( "SHOW CREATE TABLE {$wpdb->prefix}wc_download_log" ); // WPCS: unprepared SQL ok.
if ( false === strpos( $fk_result->{'Create Table'}, "fk_{$wpdb->prefix}wc_download_log_permission_id" ) ) {
$wpdb->query(
"ALTER TABLE `{$wpdb->prefix}wc_download_log`
ADD CONSTRAINT `fk_{$wpdb->prefix}wc_download_log_permission_id`

View File

@ -80,7 +80,7 @@ class WC_Validation {
$valid = (bool) preg_match( '/([AC-FHKNPRTV-Y]\d{2}|D6W)[0-9AC-FHKNPRTV-Y]{4}/', wc_normalize_postcode( $postcode ) );
break;
case 'JP':
$valid = (bool) preg_match( '/^([0-9]{3})([-])([0-9]{4})$/', $postcode );
$valid = (bool) preg_match( '/^([0-9]{3})([-]?)([0-9]{4})$/', $postcode );
break;
case 'PT':
$valid = (bool) preg_match( '/^([0-9]{4})([-])([0-9]{3})$/', $postcode );
@ -105,6 +105,9 @@ class WC_Validation {
case 'SI':
$valid = (bool) preg_match( '/^([1-9][0-9]{3})$/', $postcode );
break;
case 'LI':
$valid = (bool) preg_match( '/^(94[8-9][0-9])$/', $postcode );
break;
default:
$valid = true;
break;

View File

@ -1559,10 +1559,17 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
}
$post_types = $include_variations ? array( 'product', 'product_variation' ) : array( 'product' );
$join_query = '';
$type_where = '';
$status_where = '';
$limit_query = '';
// When searching variations we should include the parent's meta table for use in searches.
if ( $include_variations ) {
$join_query = " LEFT JOIN {$wpdb->wc_product_meta_lookup} parent_wc_product_meta_lookup
ON posts.post_type = 'product_variation' AND parent_wc_product_meta_lookup.product_id = posts.post_parent ";
}
/**
* Hook woocommerce_search_products_post_statuses.
*
@ -1602,8 +1609,16 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
$searchand = '';
foreach ( $search_terms as $search_term ) {
$like = '%' . $wpdb->esc_like( $search_term ) . '%';
$term_group_query .= $wpdb->prepare( " {$searchand} ( ( posts.post_title LIKE %s) OR ( posts.post_excerpt LIKE %s) OR ( posts.post_content LIKE %s ) OR ( wc_product_meta_lookup.sku LIKE %s ) )", $like, $like, $like, $like ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$like = '%' . $wpdb->esc_like( $search_term ) . '%';
// Variations should also search the parent's meta table for fallback fields.
if ( $include_variations ) {
$variation_query = $wpdb->prepare( ' OR ( wc_product_meta_lookup.sku = "" AND parent_wc_product_meta_lookup.sku LIKE %s ) ', $like );
} else {
$variation_query = '';
}
$term_group_query .= $wpdb->prepare( " {$searchand} ( ( posts.post_title LIKE %s) OR ( posts.post_excerpt LIKE %s) OR ( posts.post_content LIKE %s ) OR ( wc_product_meta_lookup.sku LIKE %s ) $variation_query)", $like, $like, $like, $like ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$searchand = ' AND ';
}
@ -1643,6 +1658,7 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
// phpcs:disable
"SELECT DISTINCT posts.ID as product_id, posts.post_parent as parent_id FROM {$wpdb->posts} posts
LEFT JOIN {$wpdb->wc_product_meta_lookup} wc_product_meta_lookup ON posts.ID = wc_product_meta_lookup.product_id
$join_query
WHERE posts.post_type IN ('" . implode( "','", $post_types ) . "')
$search_where
$status_where

View File

@ -511,7 +511,7 @@ class WC_REST_Coupons_V2_Controller extends WC_REST_CRUD_Controller {
),
'value' => array(
'description' => __( 'Meta value.', 'woocommerce' ),
'type' => array( 'string', 'null' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
),
),

View File

@ -350,7 +350,7 @@ class WC_REST_Customers_V2_Controller extends WC_REST_Customers_V1_Controller {
),
'value' => array(
'description' => __( 'Meta value.', 'woocommerce' ),
'type' => array( 'string', 'null' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
),
),

View File

@ -410,7 +410,7 @@ class WC_REST_Order_Refunds_V2_Controller extends WC_REST_Orders_V2_Controller {
),
'value' => array(
'description' => __( 'Meta value.', 'woocommerce' ),
'type' => array( 'string', 'null' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
),
),
@ -432,13 +432,13 @@ class WC_REST_Order_Refunds_V2_Controller extends WC_REST_Orders_V2_Controller {
),
'name' => array(
'description' => __( 'Product name.', 'woocommerce' ),
'type' => array( 'string', 'null' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
'product_id' => array(
'description' => __( 'Product ID.', 'woocommerce' ),
'type' => array( 'integer', 'null' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
@ -535,7 +535,7 @@ class WC_REST_Order_Refunds_V2_Controller extends WC_REST_Orders_V2_Controller {
),
'value' => array(
'description' => __( 'Meta value.', 'woocommerce' ),
'type' => array( 'string', 'null' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),

View File

@ -1170,7 +1170,7 @@ class WC_REST_Orders_V2_Controller extends WC_REST_CRUD_Controller {
),
'value' => array(
'description' => __( 'Meta value.', 'woocommerce' ),
'type' => array( 'string', 'null' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
),
),
@ -1191,12 +1191,12 @@ class WC_REST_Orders_V2_Controller extends WC_REST_CRUD_Controller {
),
'name' => array(
'description' => __( 'Product name.', 'woocommerce' ),
'type' => array( 'string', 'null' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
),
'product_id' => array(
'description' => __( 'Product ID.', 'woocommerce' ),
'type' => array( 'integer' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
),
'variation_id' => array(
@ -1282,7 +1282,7 @@ class WC_REST_Orders_V2_Controller extends WC_REST_CRUD_Controller {
),
'value' => array(
'description' => __( 'Meta value.', 'woocommerce' ),
'type' => array( 'string', 'null' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
),
),
@ -1373,7 +1373,7 @@ class WC_REST_Orders_V2_Controller extends WC_REST_CRUD_Controller {
),
'value' => array(
'description' => __( 'Meta value.', 'woocommerce' ),
'type' => array( 'string', 'null' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
),
),
@ -1397,12 +1397,12 @@ class WC_REST_Orders_V2_Controller extends WC_REST_CRUD_Controller {
),
'method_title' => array(
'description' => __( 'Shipping method name.', 'woocommerce' ),
'type' => array( 'string', 'null' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
),
'method_id' => array(
'description' => __( 'Shipping method ID.', 'woocommerce' ),
'type' => array( 'string', 'null' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
),
'instance_id' => array(
@ -1464,7 +1464,7 @@ class WC_REST_Orders_V2_Controller extends WC_REST_CRUD_Controller {
),
'value' => array(
'description' => __( 'Meta value.', 'woocommerce' ),
'type' => array( 'string', 'null' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
),
),
@ -1488,7 +1488,7 @@ class WC_REST_Orders_V2_Controller extends WC_REST_CRUD_Controller {
),
'name' => array(
'description' => __( 'Fee name.', 'woocommerce' ),
'type' => array( 'string', 'null' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
),
'tax_class' => array(
@ -1562,7 +1562,7 @@ class WC_REST_Orders_V2_Controller extends WC_REST_CRUD_Controller {
),
'value' => array(
'description' => __( 'Meta value.', 'woocommerce' ),
'type' => array( 'string', 'null' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
),
),
@ -1586,7 +1586,7 @@ class WC_REST_Orders_V2_Controller extends WC_REST_CRUD_Controller {
),
'code' => array(
'description' => __( 'Coupon code.', 'woocommerce' ),
'type' => array( 'string', 'null' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
),
'discount' => array(
@ -1620,7 +1620,7 @@ class WC_REST_Orders_V2_Controller extends WC_REST_CRUD_Controller {
),
'value' => array(
'description' => __( 'Meta value.', 'woocommerce' ),
'type' => array( 'string', 'null' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
),
),

View File

@ -799,7 +799,7 @@ class WC_REST_Product_Variations_V2_Controller extends WC_REST_Products_V2_Contr
),
'manage_stock' => array(
'description' => __( 'Stock management at variation level.', 'woocommerce' ),
'type' => array( 'boolean', 'null' ),
'type' => 'mixed',
'default' => false,
'context' => array( 'view', 'edit' ),
),
@ -982,7 +982,7 @@ class WC_REST_Product_Variations_V2_Controller extends WC_REST_Products_V2_Contr
),
'value' => array(
'description' => __( 'Meta value.', 'woocommerce' ),
'type' => array( 'string', 'null' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
),
),

View File

@ -2084,7 +2084,7 @@ class WC_REST_Products_V2_Controller extends WC_REST_CRUD_Controller {
),
'value' => array(
'description' => __( 'Meta value.', 'woocommerce' ),
'type' => array( 'string', 'null' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
),
),

View File

@ -530,18 +530,12 @@ class WC_REST_Setting_Options_V2_Controller extends WC_REST_Controller {
),
'value' => array(
'description' => __( 'Setting value.', 'woocommerce' ),
'type' => array( 'string', 'array', 'null' ),
'items' => array(
'type' => array( 'string', 'null' ),
),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
),
'default' => array(
'description' => __( 'Default value for the setting.', 'woocommerce' ),
'type' => array( 'string', 'array', 'null' ),
'items' => array(
'type' => array( 'string', 'null' ),
),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),

View File

@ -93,16 +93,57 @@ abstract class WC_REST_Controller extends WP_REST_Controller {
return $endpoint_args;
}
foreach ( $endpoint_args as $field_id => $params ) {
/**
* Custom types are not supported as of WP 5.5, this translates type => 'date-time' to type => 'string' with format date-time.
*/
if ( 'date-time' === $params['type'] ) {
$endpoint_args[ $field_id ]['type'] = 'string';
$endpoint_args[ $field_id ]['format'] = 'date-time';
}
$endpoint_args = $this->adjust_wp_5_5_datatype_compatibility( $endpoint_args );
return $endpoint_args;
}
/**
* Change datatypes `date-time` to string, and `mixed` to composite of all built in types. This is required for maintaining forward compatibility with WP 5.5 since custom post types are not supported anymore.
*
* See @link https://core.trac.wordpress.org/changeset/48306
*
* We still use the 'mixed' type, since if we convert to composite type everywhere, it won't work in 5.4 anymore because they require to define the full schema.
*
* @param array $endpoint_args Schema with datatypes to convert.
* @return mixed Schema with converted datatype.
*/
protected function adjust_wp_5_5_datatype_compatibility( $endpoint_args ) {
if ( version_compare( get_bloginfo( 'version' ), '5.5', '<' ) ) {
return $endpoint_args;
}
foreach ( $endpoint_args as $field_id => $params ) {
if ( ! isset( $params['type'] ) ) {
continue;
}
/**
* Custom types are not supported as of WP 5.5, this translates type => 'date-time' to type => 'string'.
*/
if ( 'date-time' === $params['type'] ) {
$params['type'] = array( 'null', 'string' );
}
/**
* WARNING: Order of fields here is important, types of fields are ordered from most specific to least specific as perceived by core's built-in type validation methods.
*/
if ( 'mixed' === $params['type'] ) {
$params['type'] = array( 'null', 'object', 'string', 'number', 'boolean', 'integer', 'array' );
}
if ( isset( $params['properties'] ) ) {
$params['properties'] = $this->adjust_wp_5_5_datatype_compatibility( $params['properties'] );
}
if ( isset( $params['items'] ) && isset( $params['items']['properties'] ) ) {
$params['items']['properties'] = $this->adjust_wp_5_5_datatype_compatibility( $params['items']['properties'] );
}
$endpoint_args[ $field_id ] = $params;
}
return $endpoint_args;
}

View File

@ -91,8 +91,6 @@ abstract class WC_REST_CRUD_Controller extends WC_REST_Posts_Controller {
return true;
}
/**
* Get object permalink.
*

View File

@ -293,7 +293,7 @@ class WC_REST_Customers_Controller extends WC_REST_Customers_V2_Controller {
),
'value' => array(
'description' => __( 'Meta value.', 'woocommerce' ),
'type' => array( 'string', 'null' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
),
),

View File

@ -736,7 +736,7 @@ class WC_REST_Product_Variations_Controller extends WC_REST_Product_Variations_V
),
'value' => array(
'description' => __( 'Meta value.', 'woocommerce' ),
'type' => array( 'string', 'null' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
),
),

View File

@ -1288,7 +1288,7 @@ class WC_REST_Products_Controller extends WC_REST_Products_V2_Controller {
),
'value' => array(
'description' => __( 'Meta value.', 'woocommerce' ),
'type' => array( 'string', 'null' ),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
),
),

View File

@ -199,18 +199,12 @@ class WC_REST_Setting_Options_Controller extends WC_REST_Setting_Options_V2_Cont
),
'value' => array(
'description' => __( 'Setting value.', 'woocommerce' ),
'type' => array( 'string', 'array', 'null' ),
'items' => array(
'type' => array( 'string', 'null' ),
),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
),
'default' => array(
'description' => __( 'Default value for the setting.', 'woocommerce' ),
'type' => array( 'string', 'array', 'null' ),
'items' => array(
'type' => array( 'string', 'null' ),
),
'type' => 'mixed',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),

View File

@ -778,6 +778,7 @@ function wc_order_fully_refunded( $order_id ) {
}
// Create the refund object.
wc_switch_to_site_locale();
wc_create_refund(
array(
'amount' => $max_refund,
@ -786,6 +787,7 @@ function wc_order_fully_refunded( $order_id ) {
'line_items' => array(),
)
);
wc_restore_locale();
$order->add_order_note( __( 'Order status set to refunded. To return funds to the customer you will need to issue a refund through your payment gateway.', 'woocommerce' ) );
}

16782
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -41,8 +41,8 @@
"@woocommerce/model-factories": "file:tests/e2e/factories",
"@wordpress/babel-plugin-import-jsx-pragma": "1.1.3",
"@wordpress/babel-preset-default": "3.0.2",
"@wordpress/e2e-test-utils": "4.6.0",
"@wordpress/eslint-plugin": "7.1.0",
"@woocommerce/e2e-utils": "file:tests/e2e/utils",
"autoprefixer": "9.8.6",
"babel-eslint": "10.1.0",
"chai": "4.2.0",

View File

@ -31,6 +31,14 @@
<!-- Rules -->
<rule ref="WooCommerce-Core" />
<rule ref="WooCommerce.Functions.InternalInjectionMethod">
<include-pattern>src/</include-pattern>
<include-pattern>tests/php/src/</include-pattern>
<properties>
<property name="injectionMethod" value="init"/>
</properties>
</rule>
<rule ref="WordPress.WP.I18n">
<properties>
<property name="text_domain" type="array" value="woocommerce" />

View File

@ -1,10 +1,10 @@
=== WooCommerce ===
Contributors: automattic, mikejolley, jameskoster, claudiosanches, kloon, rodrigosprimo, peterfabian1000, vedjain, jamosova, obliviousharmony
Contributors: automattic, mikejolley, jameskoster, claudiosanches, kloon, rodrigosprimo, peterfabian1000, vedjain, jamosova, obliviousharmony, konamiman
Tags: e-commerce, store, sales, sell, woo, shop, cart, checkout, downloadable, downloads, payments, paypal, storefront, stripe, woo commerce
Requires at least: 5.2
Tested up to: 5.4
Tested up to: 5.5
Requires PHP: 7.0
Stable tag: 4.3.1
Stable tag: 4.4.0
License: GPLv3
License URI: https://www.gnu.org/licenses/gpl-3.0.html
@ -237,8 +237,10 @@ INTERESTED IN DEVELOPMENT?
* Fix - After clicking to update WooCommerce, the user will stay in the same page instead of being redirected to the "Settings" page. #27172
* Fix - "Product type" dropdown missing from Product's data meta box on WP 5.5. #27170
* Fix - Removed the JETPACK_AUTOLOAD_DEV define. #27185
* Dev - Update WooCommerce Admin version to v1.4.0-beta.3. #27214
* Dev - Upgraded to the 2.x Jetpack Autoloader. #27123
* Fix - Fixed "virtual" and "downlodable" pointers on product walkthrough. #27145
* Fix - Updated tested up to for WordPress 5.5. #27334
* Dev - Update WooCommerce Admin version to v1.4.0. #27378
* Dev - Upgraded to v2.2 of Jetpack Autoloader. #27358
* Dev - Update jest-preset-default version to ^6.2.0. #27090
* Dev - Added a second $existing_meta_keys parameter to the woocommerce_duplicate_product_exclude_meta filter. #27038
* Dev - Remove leftover note for translators in customer-completed-order.php. #26989
@ -264,14 +266,18 @@ INTERESTED IN DEVELOPMENT?
* Dev - Ensure wc_load_cart loads its own dependencies. #26219
* Dev - Clean up deprecated documentation. #27054
* Dev - Update WooCommerce Blocks version to 3.1.0. #27177
* Dev - Added woocommerce_order_item_quantity filter to ReserveStock::reserve_stock_for_order(). #27251
* Dev - Updated docs to make the type in docblock more specific. #27285
**REST API 1.0.11**
**REST API 1.0.15**
* Enhancement - Introduced X-WP-Total header for product attributes GET endpoint listing the number of entries in the response. woocommerce/woocommerce-rest-api#171
* Enhancement - Introduced X-WP-TotalPages header for product attributes GET endpoint listing the number of pages that can be fetched. woocommerce/woocommerce-rest-api#171
* Enhancement - Introduced the modified option for orderby fetch requests in post based resources. woocommerce/woocommerce-rest-api#226
* Enhancement - Compatibility fixes for WordPress 5.5. woocommerce/woocommerce-rest-api#232
* Fix - Ensured Action Scheduler transients are cleared by "Clear Transients" tool. woocommerce/woocommerce-rest-api#152
* Fix - Corrected the schema datatype for coupon expiry_date, date_expires, and date_expires_gmt fields. woocommerce/woocommerce-rest-api#176
* Fix - Query parameters are now passed correctly when using the batch product variation endpoints. woocommerce/woocommerce-rest-api#191
* Fix - Fix regression and restore backward compatibility for date-time and mixed data types. woocommerce/woocommerce-rest-api#238
**WooCommerce Admin 1.4.0**
* Enhancement - Move the WooCommerce > Coupons dashboard menu item to Marketing > Coupons. #4786
@ -286,9 +292,15 @@ INTERESTED IN DEVELOPMENT?
* Fix - Polyfill core-data saveUser() on WP 5.3.x. #4869
* Fix - Product types step bugs in onboarding wizard. #4900
* Fix - Center all descriptive text on onboarding wizard steps. #4902
* Fix - Match the requires version to the exact WordPress version number in readme.txt. #4956
* Fix - Change account required text on biz step in onboarding wizard. #4909
* Fix - Fix industry args type in REST API. #4974
* Fix - Update style on shipping banner. #4948
* Fix - CSS Fixes for Business Features Popover ( parts 1&2 ). #4994
* Dev - Add the experimental resolver to WCA data package. #4862
* Dev - Fix linter errors. #4904
* Dev - Fix usage of "package" tag in file headers. #4940
* Dev - Update Jetpack Autoloader to match Woo Core. #4993
**WooCommerce Blocks 3.0.0**
* Build - Updated the automattic/jetpack-autoloader package to the 2.0 branch. #2847

View File

@ -39,8 +39,8 @@ final class ReserveStock {
/**
* Query for any existing holds on stock for this item.
*
* @param \WC_Product|object $product Product to get reserved stock for.
* @param integer $exclude_order_id Optional order to exclude from the results.
* @param \WC_Product $product Product to get reserved stock for.
* @param integer $exclude_order_id Optional order to exclude from the results.
*
* @return integer Amount of stock already reserved.
*/
@ -60,8 +60,8 @@ final class ReserveStock {
*
* @throws ReserveStockException If stock cannot be reserved.
*
* @param \WC_Order|object $order Order object.
* @param int $minutes How long to reserve stock in minutes. Defaults to woocommerce_hold_stock_minutes.
* @param \WC_Order $order Order object.
* @param int $minutes How long to reserve stock in minutes. Defaults to woocommerce_hold_stock_minutes.
*/
public function reserve_stock_for_order( $order, $minutes = 0 ) {
$minutes = $minutes ? $minutes : (int) get_option( 'woocommerce_hold_stock_minutes', 60 );
@ -127,7 +127,7 @@ final class ReserveStock {
/**
* Release a temporary hold on stock for an order.
*
* @param \WC_Order|object $order Order object.
* @param \WC_Order $order Order object.
*/
public function release_stock_for_order( $order ) {
global $wpdb;
@ -149,10 +149,10 @@ final class ReserveStock {
*
* @throws ReserveStockException If a row cannot be inserted.
*
* @param int $product_id Product ID which is having stock reserved.
* @param int $stock_quantity Stock amount to reserve.
* @param \WC_Order|object $order Order object which contains the product.
* @param int $minutes How long to reserve stock in minutes.
* @param int $product_id Product ID which is having stock reserved.
* @param int $stock_quantity Stock amount to reserve.
* @param \WC_Order $order Order object which contains the product.
* @param int $minutes How long to reserve stock in minutes.
*/
private function reserve_stock_for_product( $product_id, $stock_quantity, $order, $minutes ) {
global $wpdb;
@ -219,7 +219,7 @@ final class ReserveStock {
* Filter: woocommerce_query_for_reserved_stock
* Allows to filter the query for getting reserved stock of a product.
*
* @since 4.4.0
* @since 4.5.0
* @param string $query The query for getting reserved stock of a product.
* @param int $product_id Product ID.
* @param int $exclude_order_id Order to exclude from the results.

View File

@ -21,17 +21,12 @@ use Automattic\WooCommerce\Internal\DependencyManagement\ExtendedContainer;
* Classes in the `includes` directory should use the `wc_get_container` function to get the instance of the container when
* they need to get an instance of a class from the `src` directory.
*
* Class registration should be done via service providers that inherit from Automattic\WooCommerce\Tools\DependencyManagement
* and those should go in the `src\Tools\DependencyManagement\ServiceProviders` folder unless there's a good reason
* Class registration should be done via service providers that inherit from Automattic\WooCommerce\Internal\DependencyManagement
* and those should go in the `src\Internal\DependencyManagement\ServiceProviders` folder unless there's a good reason
* to put them elsewhere. All the service provider class names must be in the `SERVICE_PROVIDERS` constant.
*/
final class Container implements \Psr\Container\ContainerInterface {
/**
* The root namespace of all WooCommerce classes in the `src` directory.
*/
const WOOCOMMERCE_ROOT_NAMESPACE = 'Automattic\\WooCommerce';
/**
* The list of service provider classes to register.
*

View File

@ -7,7 +7,6 @@ namespace Automattic\WooCommerce\Internal\DependencyManagement;
use League\Container\Argument\RawArgument;
use League\Container\Definition\DefinitionInterface;
use League\Container\Definition\Definition;
/**
* Base class for the service providers used to register classes in the container.
@ -15,14 +14,14 @@ use League\Container\Definition\Definition;
* See the documentation of the original class this one is based on (https://container.thephpleague.com/3.x/service-providers)
* for basic usage details. What this class adds is:
*
* - The `add_with_auto_arguments` method that allows to register classes without having to specify the constructor arguments.
* - The `add_with_auto_arguments` method that allows to register classes without having to specify the injection method arguments.
* - The `share_with_auto_arguments` method, sibling of the above.
* - Convenience `add` and `share` methods that are just proxies for the same methods in `$this->getContainer()`.
*/
abstract class AbstractServiceProvider extends \League\Container\ServiceProvider\AbstractServiceProvider {
/**
* Register a class in the container and use reflection to guess the constructor arguments.
* Register a class in the container and use reflection to guess the injection method arguments.
*
* WARNING: this method uses reflection, so please have performance in mind when using it.
*
@ -32,7 +31,7 @@ abstract class AbstractServiceProvider extends \League\Container\ServiceProvider
*
* @return DefinitionInterface The generated container definition.
*
* @throws ContainerException Error when reflecting the class, or class constructor is not public, or an argument has no valid type hint.
* @throws ContainerException Error when reflecting the class, or class injection method is not public, or an argument has no valid type hint.
*/
protected function add_with_auto_arguments( string $class_name, $concrete = null, bool $shared = false ) : DefinitionInterface {
$definition = new Definition( $class_name, $concrete );
@ -48,7 +47,7 @@ abstract class AbstractServiceProvider extends \League\Container\ServiceProvider
} else {
$argument_class = $argument->getClass();
if ( is_null( $argument_class ) ) {
throw new ContainerException( "AbstractServiceProvider::add_with_auto_arguments: constructor argument '{$argument->getName()}' of class '$class_name' doesn't have a type hint or has one that doesn't specify a class." );
throw new ContainerException( "Argument '{$argument->getName()}' of class '$class_name' doesn't have a type hint or has one that doesn't specify a class." );
}
$definition->addArgument( $argument_class->name );
@ -57,7 +56,6 @@ abstract class AbstractServiceProvider extends \League\Container\ServiceProvider
}
// Register the definition only after being sure that no exception will be thrown.
$this->getContainer()->add( $definition->getAlias(), $definition, $shared );
return $definition;
@ -65,32 +63,43 @@ abstract class AbstractServiceProvider extends \League\Container\ServiceProvider
/**
* Check if a combination of class name and concrete is valid for registration.
* Also return the class constructor if the concrete is either a class name or null (then use the supplied class name).
* Also return the class injection method if the concrete is either a class name or null (then use the supplied class name).
*
* @param string $class_name The class name to check.
* @param mixed $concrete The concrete to check.
*
* @return \ReflectionFunctionAbstract|null A reflection instance for the $class_name constructor or $concrete constructor or callable; null otherwise.
* @throws ContainerException Class has a private constructor, can't reflect class, or the concrete is invalid.
* @return \ReflectionFunctionAbstract|null A reflection instance for the $class_name injection method or $concrete injection method or callable; null otherwise.
* @throws ContainerException Class has a private injection method, can't reflect class, or the concrete is invalid.
*/
private function reflect_class_or_callable( string $class_name, $concrete ) {
if ( ! isset( $concrete ) || is_string( $concrete ) && class_exists( $concrete ) ) {
try {
$class = $concrete ?? $class_name;
$reflector = new \ReflectionClass( $class );
$constructor = $reflector->getConstructor();
if ( isset( $constructor ) && ! $constructor->isPublic() ) {
throw new ContainerException( "AbstractServiceProvider::add_with_auto_arguments: constructor of class '$class' isn't public, instances can't be created." );
$class = $concrete ?? $class_name;
$method = new \ReflectionMethod( $class, Definition::INJECTION_METHOD );
if ( ! isset( $method ) ) {
return null;
}
return $constructor;
$missing_modifiers = array();
if ( ! $method->isFinal() ) {
$missing_modifiers[] = 'final';
}
if ( ! $method->isPublic() ) {
$missing_modifiers[] = 'public';
}
if ( ! empty( $missing_modifiers ) ) {
throw new ContainerException( "Method '" . Definition::INJECTION_METHOD . "' of class '$class' isn't '" . implode( ' ', $missing_modifiers ) . "', instances can't be created." );
}
return $method;
} catch ( \ReflectionException $ex ) {
throw new ContainerException( "AbstractServiceProvider::add_with_auto_arguments: error when reflecting class '$class': {$ex->getMessage()}" );
return null;
}
} elseif ( is_callable( $concrete ) ) {
try {
return new \ReflectionFunction( $concrete );
} catch ( \ReflectionException $ex ) {
throw new ContainerException( "AbstractServiceProvider::add_with_auto_arguments: error when reflecting callable: {$ex->getMessage()}" );
throw new ContainerException( "Error when reflecting callable: {$ex->getMessage()}" );
}
}
@ -98,7 +107,7 @@ abstract class AbstractServiceProvider extends \League\Container\ServiceProvider
}
/**
* Register a class in the container and use reflection to guess the constructor arguments.
* Register a class in the container and use reflection to guess the injection method arguments.
* The class is registered as shared, so `get` on the container always returns the same instance.
*
* WARNING: this method uses reflection, so please have performance in mind when using it.
@ -108,7 +117,7 @@ abstract class AbstractServiceProvider extends \League\Container\ServiceProvider
*
* @return DefinitionInterface The generated container definition.
*
* @throws ContainerException Error when reflecting the class, or class constructor is not public, or an argument has no valid type hint.
* @throws ContainerException Error when reflecting the class, or class injection method is not public, or an argument has no valid type hint.
*/
protected function share_with_auto_arguments( string $class_name, $concrete = null ) : DefinitionInterface {
return $this->add_with_auto_arguments( $class_name, $concrete, true );

View File

@ -0,0 +1,39 @@
<?php
/**
* An extension to the Definition class to prevent constructor injection from being possible.
*/
namespace Automattic\WooCommerce\Internal\DependencyManagement;
use \League\Container\Definition\Definition as BaseDefinition;
/**
* An extension of the definition class that replaces constructor injection with method injection.
*/
class Definition extends BaseDefinition {
/**
* The standard method that we use for dependency injection.
*/
const INJECTION_METHOD = 'init';
/**
* Resolve a class using method injection instead of constructor injection.
*
* @param string $concrete The concrete to instantiate.
*
* @return object
*/
protected function resolveClass( string $concrete ) {
$resolved = $this->resolveArguments( $this->arguments );
$concrete = new $concrete();
// Constructor injection causes backwards compatibility problems
// so we will rely on method injection via an internal method.
if ( method_exists( $concrete, static::INJECTION_METHOD ) ) {
call_user_func_array( array( $concrete, static::INJECTION_METHOD ), $resolved );
}
return $concrete;
}
}

View File

@ -5,14 +5,22 @@
namespace Automattic\WooCommerce\Internal\DependencyManagement;
use Automattic\WooCommerce\Container;
use Automattic\WooCommerce\Utilities\StringUtil;
use League\Container\Container as BaseContainer;
use League\Container\Definition\DefinitionInterface;
/**
* This class extends the original League's Container object by adding some functionality
* that we need for WooCommerce.
*/
class ExtendedContainer extends \League\Container\Container {
class ExtendedContainer extends BaseContainer {
/**
* The root namespace of all WooCommerce classes in the `src` directory.
*
* @var string
*/
private $woocommerce_namespace = 'Automattic\\WooCommerce\\';
/**
* Whitelist of classes that we can register using the container
@ -41,24 +49,23 @@ class ExtendedContainer extends \League\Container\Container {
* @throws ContainerException Invalid parameters.
*/
public function add( string $class_name, $concrete = null, bool $shared = null ) : DefinitionInterface {
if ( ! $this->class_is_in_root_namespace( $class_name ) && ! in_array( $class_name, $this->registration_whitelist, true ) ) {
throw new ContainerException( "Can't use the container to register '$class_name', only objects in the " . Container::WOOCOMMERCE_ROOT_NAMESPACE . ' namespace are allowed for registration.' );
if ( ! $this->is_class_allowed( $class_name ) ) {
throw new ContainerException( "You cannot add '$class_name', only classes in the {$this->woocommerce_namespace} namespace are allowed." );
}
$concrete_class = $this->get_class_from_concrete( $concrete );
if ( isset( $concrete_class ) && ! $this->is_class_allowed( $concrete_class ) ) {
throw new ContainerException( "You cannot add concrete '$concrete_class', only classes in the {$this->woocommerce_namespace} namespace are allowed." );
}
// We want to use a definition class that does not support constructor injection to avoid accidental usage.
if ( ! $concrete instanceof DefinitionInterface ) {
$concrete = new Definition( $class_name, $concrete );
}
return parent::add( $class_name, $concrete, $shared );
}
/**
* Does a class belong to the WooCommerce root namespace?
*
* @param string $class_name The class name to check.
*
* @return bool True if the class belongs to the WooCommerce root namespace.
*/
private function class_is_in_root_namespace( $class_name ) {
return substr( $class_name, 0, strlen( Container::WOOCOMMERCE_ROOT_NAMESPACE ) + 1 ) === Container::WOOCOMMERCE_ROOT_NAMESPACE . '\\';
}
/**
* Replace an existing registration with a different concrete.
*
@ -68,9 +75,14 @@ class ExtendedContainer extends \League\Container\Container {
* @return DefinitionInterface The modified definition.
* @throws ContainerException Invalid parameters.
*/
public function replace( string $class_name, $concrete ) {
public function replace( string $class_name, $concrete ) : DefinitionInterface {
if ( ! $this->has( $class_name ) ) {
throw new ContainerException( "ExtendedContainer::replace: The container doesn't have '$class_name' registered, please use 'add' instead of 'replace'." );
throw new ContainerException( "The container doesn't have '$class_name' registered, please use 'add' instead of 'replace'." );
}
$concrete_class = $this->get_class_from_concrete( $concrete );
if ( isset( $concrete_class ) && ! $this->is_class_allowed( $concrete_class ) ) {
throw new ContainerException( "You cannot use concrete '$concrete_class', only classes in the {$this->woocommerce_namespace} namespace are allowed." );
}
return $this->extend( $class_name )->setConcrete( $concrete );
@ -103,4 +115,38 @@ class ExtendedContainer extends \League\Container\Container {
return parent::get( $id, $new );
}
/**
* Gets the class from the concrete regardless of type.
*
* @param mixed $concrete The concrete that we want the class from..
*
* @return string|null The class from the concrete if one is available, null otherwise.
*/
protected function get_class_from_concrete( $concrete ) {
if ( is_object( $concrete ) && ! is_callable( $concrete ) ) {
if ( $concrete instanceof DefinitionInterface ) {
return $this->get_class_from_concrete( $concrete->getConcrete() );
}
return get_class( $concrete );
}
if ( is_string( $concrete ) && class_exists( $concrete ) ) {
return $concrete;
}
return null;
}
/**
* Checks to see whether or not a class is allowed to be registered.
*
* @param string $class_name The class to check.
*
* @return bool True if the class is allowed to be registered, false otherwise.
*/
protected function is_class_allowed( string $class_name ): bool {
return StringUtil::starts_with( $class_name, $this->woocommerce_namespace, false ) || in_array( $class_name, $this->registration_whitelist, true );
}
}

View File

@ -10,7 +10,7 @@ use Automattic\WooCommerce\Proxies\LegacyProxy;
use Automattic\WooCommerce\Proxies\ActionsProxy;
/**
* Service provider for the classes in the Automattic\WooCommerce\Tools\Proxies namespace.
* Service provider for the classes in the Automattic\WooCommerce\Proxies namespace.
*/
class ProxiesServiceProvider extends AbstractServiceProvider {

View File

@ -5,7 +5,8 @@
namespace Automattic\WooCommerce\Proxies;
use \Psr\Container\ContainerInterface as Container;
use Automattic\WooCommerce\Internal\DependencyManagement\Definition;
use \Psr\Container\ContainerInterface;
/**
* Proxy class to access legacy WooCommerce functionality.
@ -34,7 +35,10 @@ class LegacyProxy {
*/
public function get_instance_of( string $class_name, ...$args ) {
if ( false !== strpos( $class_name, '\\' ) ) {
throw new \Exception( 'The LegacyProxy class is not intended for getting instances of classes in the src directory, please use constructor injection or the instance of \\Psr\\Container\\ContainerInterface for that.' );
throw new \Exception(
'The LegacyProxy class is not intended for getting instances of classes in the src directory, please use ' .
Definition::INJECTION_METHOD . ' method injection or the instance of ' . ContainerInterface::class . ' for that.'
);
}
// If a class has a dedicated method to obtain a instance, use it.

View File

@ -69,7 +69,7 @@ _Resolving_ a class means asking the container to provide an instance of the cla
In principle, the container should be used to register and resolve all the classes in the `src` directory. The exception might be data-only classes that could be created the old way (using a plain `new` statement); but as a rule of thumb, the container should always be used.
There are two ways to resolve registered classes, depending on from where they are resolved:
* Classes in the `src` directory specify their dependencies as constructor arguments, which are automatically supplied by the container when the class is resolved (this is called _constructor injection_).
* Classes in the `src` directory specify their dependencies as `init` arguments, which are automatically supplied by the container when the class is resolved (this is called _dependency injection_).
* For code in the `includes` directory there's a `wc_get_container` function that will return the container, then its `get` method can be used to resolve any class.
### Resolving classes
@ -78,7 +78,7 @@ There are two ways to resolve registered classes, depending on from where they n
#### 1. Other classes in the `src` directory
When a class in the `src` directory depends on other one classes from the same directory, it should use constructor injection. This means specifying these dependencies as constructor arguments with appropriate type hints, and storing these in private variables, ready to be used when needed:
When a class in the `src` directory depends on other one classes from the same directory, it should use method injection. This means specifying these dependencies as arguments in a `init` method with appropriate type hints, and storing these in private variables, ready to be used when needed:
```php
use TheService1Namespace\Service1;
@ -89,7 +89,7 @@ class TheClassWithDependencies {
private $service2;
public function __construct( Service1Class $service1, Service2Class $service2 ) {
public function init( Service1Class $service1, Service2Class $service2 ) {
$this->$service1 = $service1;
$this->$service2 = $service2;
}
@ -100,9 +100,9 @@ class TheClassWithDependencies {
}
```
Whenever the container is about to resolve `TheClassWithDependencies` it will also resolve `Service1Class` and `Service2Class` and pass them as constructor arguments to the requested class. If these service classes have constructor arguments too then those will also be appropriately resolved recursively.
Whenever the container is about to resolve `TheClassWithDependencies` it will also resolve `Service1Class` and `Service2Class` and pass them as method arguments to the requested class. If these service classes have method arguments too then those will also be appropriately resolved recursively.
A "lazy" approach is also possible if needed: you can specify the container itself as a constructor argument (using `\Psr\Container\ContainerInterface` as type hint), and use its `get` method to obtain the required instance at the appropriate time:
A "lazy" approach is also possible if needed: you can specify the container itself as a method argument (using `\Psr\Container\ContainerInterface` as type hint), and use its `get` method to obtain the required instance at the appropriate time:
```php
use TheService1Namespace\Service1;
@ -110,7 +110,7 @@ use TheService1Namespace\Service1;
class TheClassWithDependencies {
private $container;
public function __construct( \Psr\Container\ContainerInterface $container ) {
public function init( \Psr\Container\ContainerInterface $container ) {
$this->$container = $container;
}
@ -120,7 +120,7 @@ class TheClassWithDependencies {
}
```
In general, however, constructor injection is preferred and the lazy approach should be used only when really necessary.
In general, however, method injection is strongly preferred and the lazy approach should be used only when really necessary.
#### 2. Code in the `includes` directory
@ -146,7 +146,7 @@ For a class to be resolvable using the container, it needs to have been previous
The `Container` class is "read-only", in that it has a `get` method to resolve classes but it doesn't have any method to register classes. Instead, class registration is done by using [service providers](https://container.thephpleague.com/3.x/service-providers/). That's how the whole process would go when creating a new class:
First, create the class in the appropriate namespace (and thus in the matching folder), remember that the base namespace for the classes in the `src` directory is `Atuomattic\WooCommerce`. If the class depends on other classes from `src`, specify these dependencies as constructor arguments in detailed above.
First, create the class in the appropriate namespace (and thus in the matching folder), remember that the base namespace for the classes in the `src` directory is `Atuomattic\WooCommerce`. If the class depends on other classes from `src`, specify these dependencies as `init` arguments in detailed above.
Example of such a class:
@ -158,7 +158,7 @@ use Automattic\WooCommerce\TheDependencyNamespace\TheDependencyClass;
class TheClass {
private $the_dependency;
public function __construct( TheDependencyClass $dependency ) {
public function init( TheDependencyClass $dependency ) {
$this->the_dependency = $dependency;
}
@ -195,7 +195,7 @@ Worth noting:
* If you look at [the service provider documentation](https://container.thephpleague.com/3.x/service-providers/) you will see that classes are registered using `this->getContainer()->add`. WooCommerce's `AbstractServiceProvider` adds a utility `add` method itself that serves the same purpose.
* You can use `share` instead of `add` to register single-instance classes (the class is instantiated only once and cached, so the same instance is returned every time the class is resolved).
If the class being registered has constructor arguments then the `add` (or `share`) method must be followed by as many `addArguments` calls as needed. WooCommerce's `AbstractServiceProvider` adds a utility `add_with_auto_arguments` method (and a sibling `share_with_auto_arguments` method) that uses reflection to figure out and register all the constructor arguments (which need to have type hints). Please have in mind the possible performance penalty incurred by the usage of reflection when using this helper method.
If the class being registered has `init` arguments then the `add` (or `share`) method must be followed by as many `addArguments` calls as needed. WooCommerce's `AbstractServiceProvider` adds a utility `add_with_auto_arguments` method (and a sibling `share_with_auto_arguments` method) that uses reflection to figure out and register all the `init` arguments (which need to have type hints). Please have in mind the possible performance penalty incurred by the usage of reflection when using this helper method.
An alternative version of the service provider, which is used to register both the class and its dependency, and which takes advantage of `add_with_auto_arguments`, could be as follows:
@ -259,7 +259,7 @@ Note that if the closure is defined as a function with arguments, the supplied p
The container is intended for registering **only** classes in the `src` folder. There is a check in place to prevent classes outside the root `Automattic\Woocommerce` namespace from being registered.
This implies that classes outside `src` can't be constructor-injected, and thus must not be used as type hints in constructor arguments. There are mechanisms in place to interact with "outside" code (including code from the `includes` folder and third-party code) in a way that makes it easy to write unit tests.
This implies that classes outside `src` can't be dependency-injected, and thus must not be used as type hints in `init` arguments. There are mechanisms in place to interact with "outside" code (including code from the `includes` folder and third-party code) in a way that makes it easy to write unit tests.
## The `Internal` namespace
@ -298,7 +298,7 @@ But how does using `LegacyProxy` help in making the code testable? The trick is
### Using the legacy proxy
`LegacyProxy` is a class that is registered in the container as any other class, so an instance can be obtained by using constructor injection:
`LegacyProxy` is a class that is registered in the container as any other class, so an instance can be obtained by using dependency-injection:
```php
use Automattic\WooCommerce\Proxies\LegacyProxy;
@ -306,7 +306,7 @@ use Automattic\WooCommerce\Proxies\LegacyProxy;
class TheClass {
private $legacy_proxy;
public function __construct( LegacyProxy $legacy_proxy ) {
public function init( LegacyProxy $legacy_proxy ) {
$this->legacy_proxy = $legacy_proxy;
}
@ -316,7 +316,7 @@ class TheClass {
}
```
However, the recommended way (especially when no other dependencies need to be constructor-injected) is to use the equivalent methods in the `WooCommerce` class via the `WC()` helper, like this:
However, the recommended way (especially when no other dependencies need to be dependency-injected) is to use the equivalent methods in the `WooCommerce` class via the `WC()` helper, like this:
```php
class TheClass {
@ -382,14 +382,14 @@ class ActionsProxy {
}
```
Note however that such a class would have to be explicitly constructor-injected (unless additional helper methods are defined in the `WooCommerce` class), and that you would need to create a pairing mock class (e.g. `MockableActionsProxy`) and replace the original registration using `wc_get_container()->replace( ActionsProxy::class, MockableActionsProxy::class )`.
Note however that such a class would have to be explicitly dependency-injected (unless additional helper methods are defined in the `WooCommerce` class), and that you would need to create a pairing mock class (e.g. `MockableActionsProxy`) and replace the original registration using `wc_get_container()->replace( ActionsProxy::class, MockableActionsProxy::class )`.
## Defining new actions and filters
WordPress' hooks (actions and filters) are a very powerful extensibility mechanism and it's the core tool that allows WooCommerce extensions to be developer. However it has been often (ab)used in the WooCommerce core codebase to drive internal logic, e.g. an action is triggered from within one class or function with the assumption that somewhere there's some other class or function that will handle it and continue whatever processing is supposed to happen.
In order to keep the code as easy as reasonably possible to read and maintain, **hooks shouldn't be used to drive WooCommerce's internal logic and processes**. If you need the services of a given class or function, please call these directly (by using constructor injection or the legacy proxy as appropriate to get access to the desired service). **New hooks should be introduced only if they provide a valuable extension point for plugins**.
In order to keep the code as easy as reasonably possible to read and maintain, **hooks shouldn't be used to drive WooCommerce's internal logic and processes**. If you need the services of a given class or function, please call these directly (by using dependency-injection or the legacy proxy as appropriate to get access to the desired service). **New hooks should be introduced only if they provide a valuable extension point for plugins**.
As usual, there might be reasonable exceptions to this; but please keep this rule in mind whenever you consider creating a new hook.
@ -400,11 +400,11 @@ Unit tests are a fundamental tool to keep the code reliable and reasonably safe
**If you are a WooCommerce core team member or a contributor from other team at Automattic:** Please write unit tests to cover any code addition or modification that you make to the `src` directory (and ideally the same for the `includes` directory, by the way). There are always reasonable exceptions, but the rule of thumb is that all code should be covered by tests.
**If you are an external contributor:** When adding or changing code on the WooCommerce codebase, and especially in the `src` directory, adding unit tests is recommended but not mandatory: no contributions will be rejected solely for lacking unit tests. However, please try to at least make the code easily testable by honoring the container and constructor injection mechanism, and by using the legacy proxy to interact with legacy code when needed. If you do so, the WooCommerce team or other contributors will be able to add the missing tests.
**If you are an external contributor:** When adding or changing code on the WooCommerce codebase, and especially in the `src` directory, adding unit tests is recommended but not mandatory: no contributions will be rejected solely for lacking unit tests. However, please try to at least make the code easily testable by honoring the container and dependency-injection mechanism, and by using the legacy proxy to interact with legacy code when needed. If you do so, the WooCommerce team or other contributors will be able to add the missing tests.
### Mocking dependencies
Since all the dependencies for classes in this directory are constructor-injected or retrieved lazily by directly accessing the container, it's easy to mock them by either manually creating a mock class with the same public surface or by using [PHPUnit's test doubles](https://phpunit.readthedocs.io/en/9.2/test-doubles.html):
Since all the dependencies for classes in this directory are dependency-injected or retrieved lazily by directly accessing the container, it's easy to mock them by either manually creating a mock class with the same public surface or by using [PHPUnit's test doubles](https://phpunit.readthedocs.io/en/9.2/test-doubles.html):
```php
$dependency_mock = somehow_create_mock();

View File

@ -0,0 +1,60 @@
<?php
/**
* A class of utilities for dealing with strings.
*/
namespace Automattic\WooCommerce\Utilities;
/**
* A class of utilities for dealing with strings.
*/
final class StringUtil {
/**
* Checks to see whether or not a string starts with another.
*
* @param string $string The string we want to check.
* @param string $starts_with The string we're looking for at the start of $string.
* @param bool $case_sensitive Indicates whether the comparison should be case-sensitive.
*
* @return bool True if the $string starts with $starts_with, false otherwise.
*/
public static function starts_with( string $string, string $starts_with, bool $case_sensitive = true ): bool {
$len = strlen( $starts_with );
if ( $len > strlen( $string ) ) {
return false;
}
$string = substr( $string, 0, $len );
if ( $case_sensitive ) {
return strcmp( $string, $starts_with ) === 0;
}
return strcasecmp( $string, $starts_with ) === 0;
}
/**
* Checks to see whether or not a string ends with another.
*
* @param string $string The string we want to check.
* @param string $ends_with The string we're looking for at the end of $string.
* @param bool $case_sensitive Indicates whether the comparison should be case-sensitive.
*
* @return bool True if the $string ends with $ends_with, false otherwise.
*/
public static function ends_with( string $string, string $ends_with, bool $case_sensitive = true ): bool {
$len = strlen( $ends_with );
if ( $len > strlen( $string ) ) {
return false;
}
$string = substr( $string, -$len );
if ( $case_sensitive ) {
return strcmp( $string, $ends_with ) === 0;
}
return strcasecmp( $string, $ends_with ) === 0;
}
}

View File

@ -97,9 +97,7 @@ do_action( 'woocommerce_before_account_orders', $has_orders ); ?>
<?php else : ?>
<div class="woocommerce-message woocommerce-message--info woocommerce-Message woocommerce-Message--info woocommerce-info">
<a class="woocommerce-Button button" href="<?php echo esc_url( apply_filters( 'woocommerce_return_to_shop_redirect', wc_get_page_permalink( 'shop' ) ) ); ?>">
<?php esc_html_e( 'Browse products', 'woocommerce' ); ?>
</a>
<a class="woocommerce-Button button" href="<?php echo esc_url( apply_filters( 'woocommerce_return_to_shop_redirect', wc_get_page_permalink( 'shop' ) ) ); ?>"><?php esc_html_e( 'Browse products', 'woocommerce' ); ?></a>
<?php esc_html_e( 'No order has been made yet.', 'woocommerce' ); ?>
</div>
<?php endif; ?>

View File

@ -5,6 +5,11 @@ if [[ ${RUN_PHPCS} == 1 ]]; then
IGNORE="tests/cli/,includes/libraries/,includes/api/legacy/"
if [ "$CHANGED_FILES" != "" ]; then
if [ ! -f "./vendor/bin/phpcs" ]; then
# Install wpcs globally
composer require woocommerce/woocommerce-sniffs --update-with-all-dependencies
fi
echo "Running Code Sniffer."
./vendor/bin/phpcs --ignore=$IGNORE --encoding=utf-8 -s -n -p $CHANGED_FILES
fi

View File

@ -5,15 +5,16 @@
/**
* Internal dependencies
*/
import { StoreOwnerFlow } from '../../utils/flows';
import { completeOnboardingWizard } from '../../utils/components';
import {
StoreOwnerFlow,
completeOldSetupWizard,
completeOnboardingWizard,
permalinkSettingsPageSaveChanges,
setCheckbox,
settingsPageSaveChanges,
verifyCheckboxIsSet,
verifyValueOfInputField
} from '../../utils';
} from '@woocommerce/e2e-utils';
describe( 'Store owner can login and make sure WooCommerce is activated', () => {
beforeAll( async () => {

View File

@ -5,9 +5,12 @@
/**
* Internal dependencies
*/
import { createSimpleProduct } from '../../utils/components';
import { CustomerFlow, StoreOwnerFlow } from '../../utils/flows';
import { uiUnblocked } from '../../utils';
import {
CustomerFlow,
StoreOwnerFlow,
createSimpleProduct,
uiUnblocked
} from '@woocommerce/e2e-utils';
describe( 'Cart page', () => {
beforeAll( async () => {

View File

@ -5,9 +5,15 @@
/**
* Internal dependencies
*/
import { createSimpleProduct } from '../../utils/components';
import { CustomerFlow, StoreOwnerFlow } from '../../utils/flows';
import { setCheckbox, settingsPageSaveChanges, uiUnblocked, verifyCheckboxIsSet } from '../../utils';
import {
CustomerFlow,
StoreOwnerFlow,
createSimpleProduct,
setCheckbox,
settingsPageSaveChanges,
uiUnblocked,
verifyCheckboxIsSet
} from '@woocommerce/e2e-utils';
const config = require( 'config' );
const simpleProductName = config.get( 'products.simple.name' );

View File

@ -5,7 +5,10 @@
/**
* Internal dependencies
*/
import { CustomerFlow, StoreOwnerFlow } from '../../utils/flows';
import {
CustomerFlow,
StoreOwnerFlow
} from '@woocommerce/e2e-utils';
describe( 'My account page', () => {
it( 'allows customer to login', async () => {

View File

@ -5,9 +5,13 @@
/**
* Internal dependencies
*/
import { createSimpleProduct, createVariableProduct } from '../../utils/components';
import { CustomerFlow, StoreOwnerFlow } from '../../utils/flows';
import { uiUnblocked } from '../../utils';
import {
CustomerFlow,
StoreOwnerFlow,
createSimpleProduct,
createVariableProduct,
uiUnblocked
} from '@woocommerce/e2e-utils';
let simplePostIdValue;
let variablePostIdValue;

View File

@ -5,8 +5,11 @@
/**
* Internal dependencies
*/
import { StoreOwnerFlow } from '../../utils/flows';
import { clickTab, verifyPublishAndTrash } from '../../utils';
import {
StoreOwnerFlow,
clickTab,
verifyPublishAndTrash
} from '@woocommerce/e2e-utils';
describe( 'Add New Coupon Page', () => {
beforeAll( async () => {

View File

@ -5,8 +5,10 @@
/**
* Internal dependencies
*/
import { StoreOwnerFlow } from '../../utils/flows';
import { verifyPublishAndTrash } from '../../utils';
import {
StoreOwnerFlow,
verifyPublishAndTrash
} from '@woocommerce/e2e-utils';
describe( 'Add New Order Page', () => {
beforeAll( async () => {

View File

@ -5,8 +5,11 @@
/**
* Internal dependencies
*/
import { StoreOwnerFlow } from '../../utils/flows';
import { clickTab, uiUnblocked } from '../../utils';
import {
StoreOwnerFlow,
clickTab,
uiUnblocked
} from '@woocommerce/e2e-utils';
const config = require( 'config' );
const simpleProductName = config.get( 'products.simple.name' );

View File

@ -5,8 +5,11 @@
/**
* Internal dependencies
*/
import { StoreOwnerFlow } from '../../utils/flows';
import { settingsPageSaveChanges, verifyValueOfInputField } from '../../utils';
import {
StoreOwnerFlow,
settingsPageSaveChanges,
verifyValueOfInputField
} from '@woocommerce/e2e-utils';
describe( 'WooCommerce General Settings', () => {
beforeAll( async () => {

View File

@ -5,8 +5,14 @@
/**
* Internal dependencies
*/
import { StoreOwnerFlow } from '../../utils/flows';
import { setCheckbox, settingsPageSaveChanges, unsetCheckbox, verifyCheckboxIsSet, verifyCheckboxIsUnset } from '../../utils';
import {
StoreOwnerFlow,
setCheckbox,
settingsPageSaveChanges,
unsetCheckbox,
verifyCheckboxIsSet,
verifyCheckboxIsUnset
} from '@woocommerce/e2e-utils';
describe( 'WooCommerce Products > Downloadable Products Settings', () => {
beforeAll( async () => {

View File

@ -5,15 +5,15 @@
/**
* Internal dependencies
*/
import { StoreOwnerFlow } from '../../utils/flows';
import {
StoreOwnerFlow,
clearAndFillInput,
setCheckbox,
settingsPageSaveChanges,
uiUnblocked,
verifyCheckboxIsSet,
verifyValueOfInputField
} from '../../utils';
} from '@woocommerce/e2e-utils';
describe( 'WooCommerce Tax Settings', () => {
beforeAll( async () => {

35
tests/e2e/utils/README.md Normal file
View File

@ -0,0 +1,35 @@
# WooCommerce End to End Test Utilities
This package contains utilities to simplify writing e2e tests specific to WooCommmerce.
## Installation
```bash
npm install @woocommerce/e2e-utils --save
```
## Usage
Example:
~~~js
import {
CustomerFlow,
StoreOwnerFlow,
createSimpleProduct,
uiUnblocked
} from '@woocommerce/e2e-utils';
describe( 'Cart page', () => {
beforeAll( async () => {
await StoreOwnerFlow.login();
await createSimpleProduct();
await StoreOwnerFlow.logout();
} );
it( 'should display no item in the cart', async () => {
await CustomerFlow.goToCart();
await expect( page ).toMatchElement( '.cart-empty', { text: 'Your cart is currently empty.' } );
} );
} );
~~~

View File

@ -1,166 +1,14 @@
/**
* External dependencies
*/
import { pressKeyWithModifier } from '@wordpress/e2e-test-utils';
import { CustomerFlow, StoreOwnerFlow } from './src/flows';
/**
* Internal dependencies
*/
const flows = require( './flows' );
import {
completeOnboardingWizard,
completeOldSetupWizard,
createSimpleProduct,
createVariableProduct,
verifyAndPublish,
} from './src/components';
/**
* Perform a "select all" and then fill a input.
*
* @param {string} selector
* @param {string} value
*/
const clearAndFillInput = async ( selector, value ) => {
await page.focus( selector );
await pressKeyWithModifier( 'primary', 'a' );
await page.type( selector, value );
};
/**
* Click a tab (on post type edit screen).
*
* @param {string} tabName Tab label
*/
const clickTab = async ( tabName ) => {
await expect( page ).toClick( '.wc-tabs > li > a', { text: tabName } );
};
/**
* Save changes on a WooCommerce settings page.
*/
const settingsPageSaveChanges = async () => {
await page.focus( 'button.woocommerce-save-button' );
await Promise.all( [
page.waitForNavigation( { waitUntil: 'networkidle0' } ),
page.click( 'button.woocommerce-save-button' ),
] );
};
/**
* Save changes on Permalink settings page.
*/
const permalinkSettingsPageSaveChanges = async () => {
await page.focus( '.wp-core-ui .button-primary' );
await Promise.all( [
page.waitForNavigation( { waitUntil: 'networkidle0' } ),
page.click( '.wp-core-ui .button-primary' ),
] );
};
/**
* Set checkbox.
*
* @param {string} selector
*/
const setCheckbox = async( selector ) => {
await page.focus( selector );
const checkbox = await page.$( selector );
const checkboxStatus = ( await ( await checkbox.getProperty( 'checked' ) ).jsonValue() );
if ( checkboxStatus !== true ) {
await page.click( selector );
}
};
/**
* Unset checkbox.
*
* @param {string} selector
*/
const unsetCheckbox = async( selector ) => {
await page.focus( selector );
const checkbox = await page.$( selector );
const checkboxStatus = ( await ( await checkbox.getProperty( 'checked' ) ).jsonValue() );
if ( checkboxStatus === true ) {
await page.click( selector );
}
};
/**
* Wait for UI blocking to end.
*/
const uiUnblocked = async () => {
await page.waitForFunction( () => ! Boolean( document.querySelector( '.blockUI' ) ) );
};
/**
* Publish, verify that item was published. Trash, verify that item was trashed.
*
* @param {string} button (Publish)
* @param {string} publishNotice
* @param {string} publishVerification
* @param {string} trashVerification
*/
const verifyPublishAndTrash = async ( button, publishNotice, publishVerification, trashVerification ) => {
// Wait for auto save
await page.waitFor( 2000 );
// Publish
await expect( page ).toClick( button );
await page.waitForSelector( publishNotice );
// Verify
await expect( page ).toMatchElement( publishNotice, { text: publishVerification } );
if ( button === '.order_actions li .save_order' ) {
await expect( page ).toMatchElement( '#select2-order_status-container', { text: 'Processing' } );
await expect( page ).toMatchElement(
'#woocommerce-order-notes .note_content',
{
text: 'Order status changed from Pending payment to Processing.',
}
);
}
// Trash
await expect( page ).toClick( 'a', { text: "Move to Trash" } );
await page.waitForSelector( '#message' );
// Verify
await expect( page ).toMatchElement( publishNotice, { text: trashVerification } );
};
/**
* Verify that checkbox is set.
*
* @param {string} selector Selector of the checkbox that needs to be verified.
*/
const verifyCheckboxIsSet = async( selector ) => {
await page.focus( selector );
const checkbox = await page.$( selector );
const checkboxStatus = ( await ( await checkbox.getProperty( 'checked' ) ).jsonValue() );
await expect( checkboxStatus ).toBe( true );
};
/**
* Verify that checkbox is unset.
*
* @param {string} selector Selector of the checkbox that needs to be verified.
*/
const verifyCheckboxIsUnset = async( selector ) => {
await page.focus( selector );
const checkbox = await page.$( selector );
const checkboxStatus = ( await ( await checkbox.getProperty( 'checked' ) ).jsonValue() );
await expect( checkboxStatus ).not.toBe( true );
};
/**
* Verify the value of input field once it was saved (can be used for radio buttons verification as well).
*
* @param {string} selector Selector of the input field that needs to be verified.
* @param {string} value Value of the input field that needs to be verified.
*/
const verifyValueOfInputField = async( selector, value ) => {
await page.focus( selector );
const field = await page.$( selector );
const fieldValue = ( await ( await field.getProperty( 'value' ) ).jsonValue() );
await expect( fieldValue ).toBe( value );
};
module.exports = {
...flows,
import {
clearAndFillInput,
clickTab,
settingsPageSaveChanges,
@ -172,4 +20,25 @@ module.exports = {
verifyCheckboxIsSet,
verifyCheckboxIsUnset,
verifyValueOfInputField,
};
} from './src/page-utils';
module.exports = {
CustomerFlow,
StoreOwnerFlow,
completeOnboardingWizard,
completeOldSetupWizard,
createSimpleProduct,
createVariableProduct,
verifyAndPublish,
clearAndFillInput,
clickTab,
settingsPageSaveChanges,
permalinkSettingsPageSaveChanges,
setCheckbox,
unsetCheckbox,
uiUnblocked,
verifyPublishAndTrash,
verifyCheckboxIsSet,
verifyCheckboxIsUnset,
verifyValueOfInputField
}

View File

@ -0,0 +1,16 @@
{
"name": "@woocommerce/e2e-utils",
"version": "0.1.0",
"description": "End-To-End (E2E) test utils for WooCommerce",
"homepage": "https://github.com/woocommerce/woocommerce/tree/master/tests/e2e-utils/README.md",
"repository": {
"type": "git",
"url": "https://github.com/woocommerce/woocommerce.git"
},
"license": "GPL-3.0+",
"main": "build/index.js",
"module": "build-module/index.js",
"dependencies": {
"@wordpress/e2e-test-utils": "4.6.0"
}
}

View File

@ -6,9 +6,9 @@
* Internal dependencies
*/
import { StoreOwnerFlow } from './flows';
import { clickTab, uiUnblocked, verifyCheckboxIsUnset } from './index';
import modelRegistry from './factories';
import modelRegistry from '../../utils/factories';
import { SimpleProduct } from '@woocommerce/model-factories';
import { clickTab, uiUnblocked, verifyCheckboxIsUnset } from './page-utils';
const config = require( 'config' );
const simpleProductName = config.get( 'products.simple.name' );

View File

@ -10,7 +10,7 @@ import { pressKeyWithModifier } from '@wordpress/e2e-test-utils';
/**
* Internal dependencies
*/
import { clearAndFillInput } from './index';
import { clearAndFillInput } from './page-utils';
const config = require( 'config' );
const baseUrl = config.get( 'url' );

View File

@ -0,0 +1,169 @@
/**
* External dependencies
*/
import { pressKeyWithModifier } from '@wordpress/e2e-test-utils';
/**
* Perform a "select all" and then fill a input.
*
* @param {string} selector
* @param {string} value
*/
const clearAndFillInput = async ( selector, value ) => {
await page.focus( selector );
await pressKeyWithModifier( 'primary', 'a' );
await page.type( selector, value );
};
/**
* Click a tab (on post type edit screen).
*
* @param {string} tabName Tab label
*/
const clickTab = async ( tabName ) => {
await expect( page ).toClick( '.wc-tabs > li > a', { text: tabName } );
};
/**
* Save changes on a WooCommerce settings page.
*/
const settingsPageSaveChanges = async () => {
await page.focus( 'button.woocommerce-save-button' );
await Promise.all( [
page.waitForNavigation( { waitUntil: 'networkidle0' } ),
page.click( 'button.woocommerce-save-button' ),
] );
};
/**
* Save changes on Permalink settings page.
*/
const permalinkSettingsPageSaveChanges = async () => {
await page.focus( '.wp-core-ui .button-primary' );
await Promise.all( [
page.waitForNavigation( { waitUntil: 'networkidle0' } ),
page.click( '.wp-core-ui .button-primary' ),
] );
};
/**
* Set checkbox.
*
* @param {string} selector
*/
const setCheckbox = async( selector ) => {
await page.focus( selector );
const checkbox = await page.$( selector );
const checkboxStatus = ( await ( await checkbox.getProperty( 'checked' ) ).jsonValue() );
if ( checkboxStatus !== true ) {
await page.click( selector );
}
};
/**
* Unset checkbox.
*
* @param {string} selector
*/
const unsetCheckbox = async( selector ) => {
await page.focus( selector );
const checkbox = await page.$( selector );
const checkboxStatus = ( await ( await checkbox.getProperty( 'checked' ) ).jsonValue() );
if ( checkboxStatus === true ) {
await page.click( selector );
}
};
/**
* Wait for UI blocking to end.
*/
const uiUnblocked = async () => {
await page.waitForFunction( () => ! Boolean( document.querySelector( '.blockUI' ) ) );
};
/**
* Publish, verify that item was published. Trash, verify that item was trashed.
*
* @param {string} button (Publish)
* @param {string} publishNotice
* @param {string} publishVerification
* @param {string} trashVerification
*/
const verifyPublishAndTrash = async ( button, publishNotice, publishVerification, trashVerification ) => {
// Wait for auto save
await page.waitFor( 2000 );
// Publish
await expect( page ).toClick( button );
await page.waitForSelector( publishNotice );
// Verify
await expect( page ).toMatchElement( publishNotice, { text: publishVerification } );
if ( button === '.order_actions li .save_order' ) {
await expect( page ).toMatchElement( '#select2-order_status-container', { text: 'Processing' } );
await expect( page ).toMatchElement(
'#woocommerce-order-notes .note_content',
{
text: 'Order status changed from Pending payment to Processing.',
}
);
}
// Trash
await expect( page ).toClick( 'a', { text: "Move to Trash" } );
await page.waitForSelector( '#message' );
// Verify
await expect( page ).toMatchElement( publishNotice, { text: trashVerification } );
};
/**
* Verify that checkbox is set.
*
* @param {string} selector Selector of the checkbox that needs to be verified.
*/
const verifyCheckboxIsSet = async( selector ) => {
await page.focus( selector );
const checkbox = await page.$( selector );
const checkboxStatus = ( await ( await checkbox.getProperty( 'checked' ) ).jsonValue() );
await expect( checkboxStatus ).toBe( true );
};
/**
* Verify that checkbox is unset.
*
* @param {string} selector Selector of the checkbox that needs to be verified.
*/
const verifyCheckboxIsUnset = async( selector ) => {
await page.focus( selector );
const checkbox = await page.$( selector );
const checkboxStatus = ( await ( await checkbox.getProperty( 'checked' ) ).jsonValue() );
await expect( checkboxStatus ).not.toBe( true );
};
/**
* Verify the value of input field once it was saved (can be used for radio buttons verification as well).
*
* @param {string} selector Selector of the input field that needs to be verified.
* @param {string} value Value of the input field that needs to be verified.
*/
const verifyValueOfInputField = async( selector, value ) => {
await page.focus( selector );
const field = await page.$( selector );
const fieldValue = ( await ( await field.getProperty( 'value' ) ).jsonValue() );
await expect( fieldValue ).toBe( value );
};
export {
clearAndFillInput,
clickTab,
settingsPageSaveChanges,
permalinkSettingsPageSaveChanges,
setCheckbox,
unsetCheckbox,
uiUnblocked,
verifyPublishAndTrash,
verifyCheckboxIsSet,
verifyCheckboxIsUnset,
verifyValueOfInputField,
};

View File

@ -0,0 +1,11 @@
/**
* External dependencies
*/
const path = require( 'path' );
module.exports = {
'@woocommerce/e2e-tests': path.resolve(
__dirname,
'node_modules/woocommerce/tests/e2e'
),
};

View File

@ -222,6 +222,9 @@ class WC_Unit_Tests_Bootstrap {
require_once $this->tests_dir . '/framework/helpers/class-wc-helper-shipping-zones.php';
require_once $this->tests_dir . '/framework/helpers/class-wc-helper-payment-token.php';
require_once $this->tests_dir . '/framework/helpers/class-wc-helper-settings.php';
// Traits.
require_once $this->tests_dir . '/framework/traits/trait-wc-rest-api-complex-meta.php';
}
/**

View File

@ -0,0 +1,83 @@
<?php
/**
* Trait for easier testing of objects that have `mixed` data type somewhere.
*
* @package Automattic/WooCommerce/Tests/WC_REST_API_Complex_Meta.
*/
/**
* Trait WC_REST_API_Complex_Meta
*/
trait WC_REST_API_Complex_Meta {
/**
* Sample data of different built in data types.
*
* @var array
*/
public static $sample_meta = array(
array(
'key' => 'string_meta',
'value' => 'string_value',
),
array(
'key' => 'int_meta',
'value' => 1,
),
array(
'key' => 'bool_meta',
'value' => true,
),
array(
'key' => 'array_meta',
'value' => array( 1, 2, 'string' ),
),
array(
'key' => 'null_meta',
'value' => 'null',
),
array(
'key' => 'object_meta',
'value' => array(
'nested_key1' => 'nested_value1',
'nested_key2' => 0,
'nested_key3' => true,
'nested_key4' => array( 2, 3, 4 ),
'nested_key5' => array( 2, 3, array( 'deep' => 'nesting' ) ),
),
),
);
/**
* Test to update `meta_data` field with a complex data type.
*
* @param string $url URL to send request against.
* @param array $options Options for customizations.
*/
public function assert_update_complex_meta( $url, $options = array() ) {
$meta = $options['meta'] ?? self::$sample_meta;
$request = new WP_REST_Request( 'PUT', $url );
$request->set_body_params( array( 'meta_data' => $meta ) );
$response = $this->server->dispatch( $request );
$data = $response->get_data();
$this->assertEquals( 200, $response->get_status() );
$response_meta = $data['meta_data'];
foreach ( $meta as $meta_object ) {
$found = false;
foreach ( $response_meta as $response_meta_object ) {
if ( $response_meta_object->key === $meta_object['key'] ) {
$response_value = $response_meta_object->value;
$this->assertEquals( $meta_object['value'], $response_value );
$found = true;
break;
}
}
$this->assertEquals( true, $found, sprintf( 'Meta key %s was not found in response.', $meta_object['key'] ) );
}
}
}

View File

@ -1299,7 +1299,16 @@ class WC_Tests_Cart extends WC_Unit_Test_Case {
$variation = array_shift( $variations );
// Add the product to the cart. Methods returns boolean on failure, string on success.
$this->assertNotFalse( WC()->cart->add_to_cart( $product->get_id(), 1, $variation['variation_id'], array( 'Size' => ucfirst( $variation['attributes']['attribute_pa_size'] ) ) ) );
$result = WC()->cart->add_to_cart(
$product->get_id(),
1,
$variation['variation_id'],
array(
'attribute_pa_colour' => 'red', // Set a value since this is an 'any' attribute.
'attribute_pa_number' => '2', // Set a value since this is an 'any' attribute.
)
);
$this->assertNotFalse( $result );
// Check if the item is in the cart.
$this->assertEquals( 1, WC()->cart->get_cart_contents_count() );
@ -2252,10 +2261,10 @@ class WC_Tests_Cart extends WC_Unit_Test_Case {
$this->assertCount( 0, WC()->cart->get_cart_contents() );
$this->assertEquals( 0, WC()->cart->get_cart_contents_count() );
// Check that the notices contain an error message about an invalid colour.
// Check that the notices contain an error message about invalid colour and number.
$this->assertArrayHasKey( 'error', $notices );
$this->assertCount( 1, $notices['error'] );
$this->assertEquals( 'colour is a required field', $notices['error'][0]['notice'] );
$this->assertEquals( 'colour and number are required fields', $notices['error'][0]['notice'] );
}
/**

View File

@ -279,7 +279,15 @@ class WC_Tests_Checkout extends WC_Unit_Test_Case {
$variation->set_manage_stock( true );
$variation->set_stock_quantity( 10 );
$variation->save();
WC()->cart->add_to_cart( $variation->get_id(), 9 );
WC()->cart->add_to_cart(
$variation->get_id(),
9,
0,
array(
'attribute_pa_colour' => 'red', // Set a value since this is an 'any' attribute.
'attribute_pa_number' => '2', // Set a value since this is an 'any' attribute.
)
);
$this->assertEquals( true, WC()->cart->check_cart_items() );
$checkout = WC_Checkout::instance();
@ -299,7 +307,15 @@ class WC_Tests_Checkout extends WC_Unit_Test_Case {
$this->assertEquals( 9, wc_get_held_stock_quantity( $variation ) );
WC()->cart->empty_cart();
WC()->cart->add_to_cart( $variation->get_stock_managed_by_id(), 2 );
WC()->cart->add_to_cart(
$variation->get_stock_managed_by_id(),
2,
0,
array(
'attribute_pa_colour' => 'red',
'attribute_pa_number' => '2',
)
);
$this->assertEquals( false, WC()->cart->check_cart_items() );
}

View File

@ -7,6 +7,7 @@
*/
class Product_Variations_API_V2 extends WC_REST_Unit_Test_Case {
use WC_REST_API_Complex_Meta;
/**
* Setup our test server, endpoints, and user info.
@ -299,6 +300,19 @@ class Product_Variations_API_V2 extends WC_REST_Unit_Test_Case {
$this->assertEquals( 3, count( $variations ) );
}
/**
* Test updating complex meta object.
*/
public function test_update_complex_meta_27282() {
wp_set_current_user( $this->user );
$product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_variation_product();
$product->save();
$variations = $product->get_available_variations( 'objects' );
$first_variation_id = $variations[0]->get_id();
$url = '/wc/v2/products/' . $product->get_id() . '/variations/' . $first_variation_id;
$this->assert_update_complex_meta( $url );
}
/**
* Test creating a single variation without permission.
*

View File

@ -10,6 +10,7 @@
* Class WC_Tests_API_Orders
*/
class WC_Tests_API_Orders extends WC_REST_Unit_Test_Case {
use WC_REST_API_Complex_Meta;
/**
* Array of order to track
@ -76,12 +77,22 @@ class WC_Tests_API_Orders extends WC_REST_Unit_Test_Case {
$order2->save();
$request = new WP_REST_Request( 'GET', '/wc/v3/orders' );
$request->set_query_params( array( 'orderby' => 'modified', 'order' => 'asc' ) );
$request->set_query_params(
array(
'orderby' => 'modified',
'order' => 'asc',
)
);
$response = $this->server->dispatch( $request );
$orders = $response->get_data();
$this->assertEquals( $order1->get_id(), $orders[0]['id'] );
$request->set_query_params( array( 'orderby' => 'modified', 'order' => 'desc' ) );
$request->set_query_params(
array(
'orderby' => 'modified',
'order' => 'desc',
)
);
$response = $this->server->dispatch( $request );
$orders = $response->get_data();
$this->assertEquals( $order2->get_id(), $orders[0]['id'] );
@ -208,6 +219,20 @@ class WC_Tests_API_Orders extends WC_REST_Unit_Test_Case {
'method_title' => 'Flat rate',
'total' => '10.00',
'instance_id' => '1',
'meta_data' => array(
array(
'key' => 'string',
'value' => 'string_val',
),
array(
'key' => 'integer',
'value' => 1,
),
array(
'key' => 'array',
'value' => array( 1, 2 ),
),
),
),
),
)

View File

@ -10,6 +10,7 @@
* WC_Tests_API_Product class.
*/
class WC_Tests_API_Product extends WC_REST_Unit_Test_Case {
use WC_REST_API_Complex_Meta;
/**
* Setup our test server, endpoints, and user info.
@ -218,7 +219,7 @@ class WC_Tests_API_Product extends WC_REST_Unit_Test_Case {
$product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product();
$response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/products/' . $product->get_id() ) );
$data = $response->get_data();
$date_created = date( 'Y-m-d\TH:i:s', current_time( 'timestamp' ) );
$date_created = gmdate( 'Y-m-d\TH:i:s', current_time( 'timestamp' ) );
$this->assertEquals( 'DUMMY SKU', $data['sku'] );
$this->assertEquals( 10, $data['regular_price'] );
@ -455,6 +456,55 @@ class WC_Tests_API_Product extends WC_REST_Unit_Test_Case {
$this->assertEquals( 3, count( $products ) );
}
/**
* Test to update complex metadata.
*/
public function test_update_complex_meta_27282() {
wp_set_current_user( $this->user );
$product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product();
$product->save();
$url = '/wc/v3/products/' . $product->get_id();
$this->assert_update_complex_meta( $url );
}
/**
* Test to update datetime property.
*/
public function test_update_date_time() {
wp_set_current_user( $this->user );
$product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product();
$product->save();
$date_from_sale = '2020-01-01T01:01:01';
$request = new WP_REST_Request( 'PUT', '/wc/v3/products/' . $product->get_id() );
$request->set_body_params( array( 'date_on_sale_from' => $date_from_sale ) );
$response = $this->server->dispatch( $request );
$this->assertEquals( 200, $response->get_status() );
$data = $response->get_data();
$this->assertEquals( $date_from_sale, $data['date_on_sale_from'] );
// Empty string should delete.
$request->set_body_params( array( 'date_on_sale_from' => '' ) );
$response = $this->server->dispatch( $request );
$this->assertEquals( 200, $response->get_status() );
$data = $response->get_data();
$this->assertEquals( null, $data['date_on_sale_from'] );
$request->set_body_params( array( 'date_on_sale_from' => $date_from_sale ) );
$response = $this->server->dispatch( $request );
$this->assertEquals( 200, $response->get_status() );
$data = $response->get_data();
$this->assertEquals( $date_from_sale, $data['date_on_sale_from'] );
// Null does not delete.
$request->set_body_params( array( 'date_on_sale_from' => null ) );
$response = $this->server->dispatch( $request );
$this->assertEquals( 200, $response->get_status() );
$data = $response->get_data();
$this->assertEquals( $date_from_sale, $data['date_on_sale_from'] );
}
/**
* Test creating a single product without permission.
*

View File

@ -88,7 +88,15 @@ class WC_Tests_Totals extends WC_Unit_Test_Case {
WC()->cart->add_to_cart( $product2->get_id(), 2 );
$variations = $product3->get_available_variations();
$variation = array_shift( $variations );
WC()->cart->add_to_cart( $product3->get_id(), 1, $variation['variation_id'], array( 'Size' => ucfirst( $variation['attributes']['attribute_pa_size'] ) ) );
WC()->cart->add_to_cart(
$product3->get_id(),
1,
$variation['variation_id'],
array(
'attribute_pa_colour' => 'red', // Set a value since this is an 'any' attribute.
'attribute_pa_number' => '2', // Set a value since this is an 'any' attribute.
)
);
WC()->cart->add_discount( $coupon->get_code() );

View File

@ -123,7 +123,15 @@ class WC_Tests_Validation extends WC_Unit_Test_Case {
array( false, WC_Validation::is_postcode( '7850', 'BA' ) ),
);
return array_merge( $it, $gb, $us, $ch, $br, $ca, $nl, $si, $ba );
$jp = array(
array( true, WC_Validation::is_postcode( '1340088', 'JP' ) ),
array( true, WC_Validation::is_postcode( '134-0088', 'JP' ) ),
array( false, WC_Validation::is_postcode( '1340-088', 'JP' ) ),
array( false, WC_Validation::is_postcode( '12345', 'JP' ) ),
array( false, WC_Validation::is_postcode( '0123', 'JP' ) ),
);
return array_merge( $it, $gb, $us, $ch, $br, $ca, $nl, $si, $ba, $jp );
}
/**

View File

@ -0,0 +1,102 @@
<?php
/**
* Unit tests for the WC_Cart_Test class.
*
* @package WooCommerce\Tests\Cart.
*/
/**
* Class WC_Cart_Test
*/
class WC_Cart_Test extends \WC_Unit_Test_Case {
/**
* tearDown.
*/
public function tearDown() {
parent::tearDown();
WC()->cart->empty_cart();
WC()->customer->set_is_vat_exempt( false );
WC()->session->set( 'wc_notices', null );
}
/**
* @testdox should throw a notice to the cart if an "any" attribute is empty.
*/
public function test_add_variation_to_the_cart_with_empty_attributes() {
WC()->cart->empty_cart();
WC()->session->set( 'wc_notices', null );
$product = WC_Helper_Product::create_variation_product();
$variations = $product->get_available_variations();
// Get a variation with small pa_size and any pa_colour and pa_number.
$variation = $variations[0];
// Add variation using parent id.
WC()->cart->add_to_cart(
$variation['variation_id'],
1,
0,
array(
'attribute_pa_colour' => '',
'attribute_pa_number' => '',
)
);
$notices = WC()->session->get( 'wc_notices', array() );
// Check that the second add to cart call increases the quantity of the existing cart-item.
$this->assertCount( 0, WC()->cart->get_cart_contents() );
$this->assertEquals( 0, WC()->cart->get_cart_contents_count() );
// Check that the notices contain an error message about invalid colour and number.
$this->assertArrayHasKey( 'error', $notices );
$this->assertCount( 1, $notices['error'] );
$this->assertEquals( 'colour and number are required fields', $notices['error'][0]['notice'] );
// Reset cart.
WC()->cart->empty_cart();
WC()->customer->set_is_vat_exempt( false );
$product->delete( true );
}
/**
* Test show shipping.
*/
public function test_show_shipping() {
// Test with an empty cart.
$this->assertFalse( WC()->cart->show_shipping() );
// Add a product to the cart.
$product = WC_Helper_Product::create_simple_product();
WC()->cart->add_to_cart( $product->get_id(), 1 );
// Test with "woocommerce_ship_to_countries" disabled.
$default_ship_to_countries = get_option( 'woocommerce_ship_to_countries', '' );
update_option( 'woocommerce_ship_to_countries', 'disabled' );
$this->assertFalse( WC()->cart->show_shipping() );
// Test with default "woocommerce_ship_to_countries" and "woocommerce_shipping_cost_requires_address".
update_option( 'woocommerce_ship_to_countries', $default_ship_to_countries );
$this->assertTrue( WC()->cart->show_shipping() );
// Test with "woocommerce_shipping_cost_requires_address" enabled.
$default_shipping_cost_requires_address = get_option( 'woocommerce_shipping_cost_requires_address', 'no' );
update_option( 'woocommerce_shipping_cost_requires_address', 'yes' );
$this->assertFalse( WC()->cart->show_shipping() );
// Set address for shipping calculation required for "woocommerce_shipping_cost_requires_address".
WC()->cart->get_customer()->set_shipping_country( 'US' );
WC()->cart->get_customer()->set_shipping_state( 'NY' );
WC()->cart->get_customer()->set_shipping_postcode( '12345' );
$this->assertTrue( WC()->cart->show_shipping() );
// Reset.
update_option( 'woocommerce_shipping_cost_requires_address', $default_shipping_cost_requires_address );
$product->delete( true );
WC()->cart->get_customer()->set_shipping_country( 'GB' );
WC()->cart->get_customer()->set_shipping_state( '' );
WC()->cart->get_customer()->set_shipping_postcode( '' );
}
}

View File

@ -0,0 +1,41 @@
<?php
/**
* Class WC_Product_Data_Store_CPT_Test
*/
class WC_Product_Data_Store_CPT_Test extends WC_Unit_Test_Case {
/**
* @testdox Variations should appear when searching for parent product's SKU.
*/
public function test_variation_searches_parent_sku() {
$parent = new WC_Product_Variable();
$parent->set_name( 'Blue widget' );
$parent->set_sku( 'blue-widget-1' );
$parent->save();
$variation = new WC_Product_Variation();
$variation->set_parent_id( $parent->get_id() );
$variation->set_sku( '' );
$variation->save();
$data_store = WC_Data_Store::load( 'product' );
// No variations should be found searching for just the parent.
$results = $data_store->search_products( 'blue-widget-1', '', false, true );
$this->assertContains( $parent->get_id(), $results );
$this->assertNotContains( $variation->get_id(), $results );
// Variation should be found when searching for it.
$results = $data_store->search_products( 'blue-widget-1', '', true, true );
$this->assertContains( $parent->get_id(), $results );
$this->assertContains( $variation->get_id(), $results );
$variation->set_sku( 'test-widget' );
$variation->save();
// Variations should be found when searching for their specific SKU.
$results = $data_store->search_products( 'test-widget', '', true, true );
$this->assertContains( $variation->get_id(), $results );
}
}

View File

@ -7,11 +7,13 @@ namespace Automattic\WooCommerce\Tests\Internal\DependencyManagement;
use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;
use Automattic\WooCommerce\Internal\DependencyManagement\ContainerException;
use Automattic\WooCommerce\Internal\DependencyManagement\Definition;
use Automattic\WooCommerce\Internal\DependencyManagement\ExtendedContainer;
use Automattic\WooCommerce\Tests\Internal\DependencyManagement\ExampleClasses\ClassWithConstructorArgumentWithoutTypeHint;
use Automattic\WooCommerce\Tests\Internal\DependencyManagement\ExampleClasses\ClassWithInjectionMethodArgumentWithoutTypeHint;
use Automattic\WooCommerce\Tests\Internal\DependencyManagement\ExampleClasses\ClassWithDependencies;
use Automattic\WooCommerce\Tests\Internal\DependencyManagement\ExampleClasses\ClassWithPrivateConstructor;
use Automattic\WooCommerce\Tests\Internal\DependencyManagement\ExampleClasses\ClassWithScalarConstructorArgument;
use Automattic\WooCommerce\Tests\Internal\DependencyManagement\ExampleClasses\ClassWithNonFinalInjectionMethod;
use Automattic\WooCommerce\Tests\Internal\DependencyManagement\ExampleClasses\ClassWithPrivateInjectionMethod;
use Automattic\WooCommerce\Tests\Internal\DependencyManagement\ExampleClasses\ClassWithScalarInjectionMethodArgument;
use Automattic\WooCommerce\Tests\Internal\DependencyManagement\ExampleClasses\DependencyClass;
use League\Container\Definition\DefinitionInterface;
@ -82,49 +84,69 @@ class AbstractServiceProviderTest extends \WC_Unit_Test_Case {
*/
public function test_add_with_auto_arguments_throws_on_non_class_passed_as_class_name() {
$this->expectException( ContainerException::class );
$this->expectExceptionMessage( "AbstractServiceProvider::add_with_auto_arguments: error when reflecting class 'foobar': Class foobar does not exist" );
$this->expectExceptionMessage( "You cannot add 'foobar', only classes in the Automattic\WooCommerce\ namespace are allowed." );
$this->sut->add_with_auto_arguments( 'foobar' );
}
/**
* @testdox 'add_with_auto_arguments' should throw an exception if the passed class has a private constructor.
* @testdox 'add_with_auto_arguments' should throw an exception if the passed class has a private injection method.
*/
public function test_add_with_auto_arguments_throws_on_class_private_constructor() {
public function test_add_with_auto_arguments_throws_on_class_private_method_injection() {
$this->expectException( ContainerException::class );
$this->expectExceptionMessage( "AbstractServiceProvider::add_with_auto_arguments: constructor of class '" . ClassWithPrivateConstructor::class . "' isn't public, instances can't be created." );
$this->expectExceptionMessage( "Method '" . Definition::INJECTION_METHOD . "' of class '" . ClassWithPrivateInjectionMethod::class . "' isn't 'public', instances can't be created." );
$this->sut->add_with_auto_arguments( ClassWithPrivateConstructor::class );
$this->sut->add_with_auto_arguments( ClassWithPrivateInjectionMethod::class );
}
/**
* @testdox 'add_with_auto_arguments' should throw an exception if the passed concrete is a class with a private constructor.
* @testdox 'add_with_auto_arguments' should throw an exception if the passed class has a non-final injection method.
*/
public function test_add_with_auto_arguments_throws_on_concrete_private_constructor() {
public function test_add_with_auto_arguments_throws_on_class_non_final_method_injection() {
$this->expectException( ContainerException::class );
$this->expectExceptionMessage( "AbstractServiceProvider::add_with_auto_arguments: constructor of class '" . ClassWithPrivateConstructor::class . "' isn't public, instances can't be created." );
$this->expectExceptionMessage( "Method '" . Definition::INJECTION_METHOD . "' of class '" . ClassWithNonFinalInjectionMethod::class . "' isn't 'final', instances can't be created." );
$this->sut->add_with_auto_arguments( ClassWithDependencies::class, ClassWithPrivateConstructor::class );
$this->sut->add_with_auto_arguments( ClassWithNonFinalInjectionMethod::class );
}
/**
* @testdox 'add_with_auto_arguments' should throw an exception if the passed class has a constructor argument without type hint.
* @testdox 'add_with_auto_arguments' should throw an exception if the passed concrete is a class with a private injection method.
*/
public function test_add_with_auto_arguments_throws_on_constructor_argument_without_type_hint() {
public function test_add_with_auto_arguments_throws_on_concrete_private_method_injection() {
$this->expectException( ContainerException::class );
$this->expectExceptionMessage( "AbstractServiceProvider::add_with_auto_arguments: constructor argument 'argument_without_type_hint' of class '" . ClassWithConstructorArgumentWithoutTypeHint::class . "' doesn't have a type hint or has one that doesn't specify a class." );
$this->expectExceptionMessage( "Method '" . Definition::INJECTION_METHOD . "' of class '" . ClassWithPrivateInjectionMethod::class . "' isn't 'public', instances can't be created." );
$this->sut->add_with_auto_arguments( ClassWithConstructorArgumentWithoutTypeHint::class );
$this->sut->add_with_auto_arguments( ClassWithDependencies::class, ClassWithPrivateInjectionMethod::class );
}
/**
* @testdox 'add_with_auto_arguments' should throw an exception if the passed class has a constructor argument with a scalar type hint.
* @testdox 'add_with_auto_arguments' should throw an exception if the passed concrete is a class with a non-final injection method.
*/
public function test_add_with_auto_arguments_throws_on_constructor_argument_with_scalar_type_hint() {
public function test_add_with_auto_arguments_throws_on_concrete_non_final_method_injection() {
$this->expectException( ContainerException::class );
$this->expectExceptionMessage( "AbstractServiceProvider::add_with_auto_arguments: constructor argument 'scalar_argument_without_default_value' of class '" . ClassWithScalarConstructorArgument::class . "' doesn't have a type hint or has one that doesn't specify a class." );
$this->expectExceptionMessage( "Method '" . Definition::INJECTION_METHOD . "' of class '" . ClassWithNonFinalInjectionMethod::class . "' isn't 'final', instances can't be created." );
$this->sut->add_with_auto_arguments( ClassWithScalarConstructorArgument::class );
$this->sut->add_with_auto_arguments( ClassWithDependencies::class, ClassWithNonFinalInjectionMethod::class );
}
/**
* @testdox 'add_with_auto_arguments' should throw an exception if the passed class has a method argument without type hint.
*/
public function test_add_with_auto_arguments_throws_on_method_argument_without_type_hint() {
$this->expectException( ContainerException::class );
$this->expectExceptionMessage( "Argument 'argument_without_type_hint' of class '" . ClassWithInjectionMethodArgumentWithoutTypeHint::class . "' doesn't have a type hint or has one that doesn't specify a class." );
$this->sut->add_with_auto_arguments( ClassWithInjectionMethodArgumentWithoutTypeHint::class );
}
/**
* @testdox 'add_with_auto_arguments' should throw an exception if the passed class has a method argument with a scalar type hint.
*/
public function test_add_with_auto_arguments_throws_on_method_argument_with_scalar_type_hint() {
$this->expectException( ContainerException::class );
$this->expectExceptionMessage( "Argument 'scalar_argument_without_default_value' of class '" . ClassWithScalarInjectionMethodArgument::class . "' doesn't have a type hint or has one that doesn't specify a class." );
$this->sut->add_with_auto_arguments( ClassWithScalarInjectionMethodArgument::class );
}
/**
@ -151,7 +173,7 @@ class AbstractServiceProviderTest extends \WC_Unit_Test_Case {
// Arguments with default values are honored.
$this->assertEquals( ClassWithDependencies::SOME_NUMBER, $resolved->some_number );
// Constructor arguments are filled as expected.
// Method arguments are filled as expected.
$this->assertSame( $this->container->get( DependencyClass::class ), $resolved->dependency_class );
}

View File

@ -1,20 +0,0 @@
<?php
/**
* ClassWithConstructorArgumentWithoutTypeHint class file.
*/
namespace Automattic\WooCommerce\Tests\Internal\DependencyManagement\ExampleClasses;
/**
* An example class that has a constructor argument without type hint.
*/
class ClassWithConstructorArgumentWithoutTypeHint {
/**
* Class constructor.
*
* @param mixed $argument_without_type_hint Anything, really.
*/
public function __construct( $argument_without_type_hint ) {
}
}

View File

@ -37,14 +37,16 @@ class ClassWithDependencies {
public $dependency_class = null;
/**
* Class constructor.
* Initialize the class instance.
*
* @internal
*
* @param DependencyClass $dependency_class A class we depend on.
* @param int $some_number Some number we need for some reason.
*/
public function __construct( DependencyClass $dependency_class, int $some_number = self::SOME_NUMBER ) {
final public function init( DependencyClass $dependency_class, int $some_number = self::SOME_NUMBER ) {
self::$instances_count++;
$this->dependency_class = $dependency_class;
$this->some_number = $some_number;
$this->some_number = self::SOME_NUMBER;
}
}

View File

@ -0,0 +1,24 @@
<?php
/**
* ClassWithInjectionMethodArgumentWithoutTypeHint class file.
*
* @package Automattic\WooCommerce\Tests\Internal\DependencyManagement\ExampleClasses
*/
namespace Automattic\WooCommerce\Tests\Internal\DependencyManagement\ExampleClasses;
/**
* An example class that has a injector method argument without type hint.
*/
class ClassWithInjectionMethodArgumentWithoutTypeHint {
/**
* Initialize the class instance.
*
* @internal
*
* @param mixed $argument_without_type_hint Anything, really.
*/
final public function init( $argument_without_type_hint ) {
}
}

View File

@ -0,0 +1,24 @@
<?php
/**
* ClassWithNonFinalInjectionMethod class file.
*
* @package Automattic\WooCommerce\Tests\Internal\DependencyManagement\ExampleClasses
*/
namespace Automattic\WooCommerce\Tests\Internal\DependencyManagement\ExampleClasses;
/**
* An example of a class with a private injection method.
*/
class ClassWithNonFinalInjectionMethod {
// phpcs:disable WooCommerce.Functions.InternalInjectionMethod.MissingFinal
/**
* Initialize the class instance.
*
* @internal
*/
public function init() {
}
}

View File

@ -1,18 +0,0 @@
<?php
/**
* ClassWithPrivateConstructor class file.
*/
namespace Automattic\WooCommerce\Tests\Internal\DependencyManagement\ExampleClasses;
/**
* An example of a class with a private constructor.
*/
class ClassWithPrivateConstructor {
/**
* Class constructor.
*/
private function __construct() {
}
}

View File

@ -0,0 +1,24 @@
<?php
/**
* ClassWithPrivateInjectionMethod class file.
*
* @package Automattic\WooCommerce\Tests\Internal\DependencyManagement\ExampleClasses
*/
namespace Automattic\WooCommerce\Tests\Internal\DependencyManagement\ExampleClasses;
/**
* An example of a class with a private injection method.
*/
class ClassWithPrivateInjectionMethod {
// phpcs:disable WooCommerce.Functions.InternalInjectionMethod.MissingPublic
/**
* Initialize the class instance.
*
* @internal
*/
final private function init() {
}
}

View File

@ -1,22 +0,0 @@
<?php
/**
* ClassWithConstructorArgumentWithoutTypeHint class file.
*/
namespace Automattic\WooCommerce\Tests\Internal\DependencyManagement\ExampleClasses;
/**
* An example class that has a constructor argument with a scalar type but without a default value.
*/
class ClassWithScalarConstructorArgument {
// phpcs:disable Squiz.Commenting.FunctionComment.InvalidTypeHint
/**
* Class constructor.
*
* @param mixed $scalar_argument_without_default_value Anything, really.
*/
public function __construct( int $scalar_argument_without_default_value ) {
}
}

View File

@ -0,0 +1,26 @@
<?php
/**
* ClassWithScalarInjectionMethodArgument class file.
*
* @package Automattic\WooCommerce\Tests\Internal\DependencyManagement\ExampleClasses
*/
namespace Automattic\WooCommerce\Tests\Internal\DependencyManagement\ExampleClasses;
/**
* An example class that has an injector method argument with a scalar type but without a default value.
*/
class ClassWithScalarInjectionMethodArgument {
// phpcs:disable Squiz.Commenting.FunctionComment.InvalidTypeHint
/**
* Initialize the class instance.
*
* @internal
*
* @param mixed $scalar_argument_without_default_value Anything, really.
*/
final public function init( int $scalar_argument_without_default_value ) {
}
}

View File

@ -36,11 +36,23 @@ class ExtendedContainerTest extends \WC_Unit_Test_Case {
$external_class = \League\Container\Container::class;
$this->expectException( ContainerException::class );
$this->expectExceptionMessage( "Can't use the container to register '" . $external_class . "', only objects in the Automattic\WooCommerce namespace are allowed for registration." );
$this->expectExceptionMessage( "You cannot add '$external_class', only classes in the Automattic\WooCommerce\ namespace are allowed." );
$this->sut->add( $external_class );
}
/**
* @testdox 'add' should throw an exception when trying to register a concrete class not in the WooCommerce root namespace.
*/
public function test_add_throws_when_trying_to_register_concrete_class_in_forbidden_namespace() {
$external_class = \League\Container\Container::class;
$this->expectException( ContainerException::class );
$this->expectExceptionMessage( "You cannot add concrete '$external_class', only classes in the Automattic\WooCommerce\ namespace are allowed." );
$this->sut->add( DependencyClass::class, $external_class );
}
/**
* @testdox 'add' should allow registering classes in the WooCommerce root namespace.
*/
@ -57,11 +69,26 @@ class ExtendedContainerTest extends \WC_Unit_Test_Case {
*/
public function test_replace_throws_if_class_has_not_been_registered() {
$this->expectException( ContainerException::class );
$this->expectExceptionMessage( "ExtendedContainer::replace: The container doesn't have '" . DependencyClass::class . "' registered, please use 'add' instead of 'replace'." );
$this->expectExceptionMessage( "The container doesn't have '" . DependencyClass::class . "' registered, please use 'add' instead of 'replace'." );
$this->sut->replace( DependencyClass::class, null );
}
/**
* @testdox 'replace'
*/
public function test_replace_throws_if_concrete_not_in_woocommerce_root_namespace() {
$instance = new DependencyClass();
$this->sut->add( DependencyClass::class, $instance, true );
$external_class = \League\Container\Container::class;
$this->expectException( ContainerException::class );
$this->expectExceptionMessage( "You cannot use concrete '$external_class', only classes in the Automattic\WooCommerce\ namespace are allowed." );
$this->sut->replace( DependencyClass::class, $external_class );
}
/**
* @testdox 'replace' should allow to replace existing registrations.
*/

View File

@ -8,9 +8,10 @@ namespace Automattic\WooCommerce\Tests\Proxies\ExampleClasses;
use Automattic\WooCommerce\Proxies\LegacyProxy;
/**
* An example class that uses the legacy proxy both from an constructor injected proxy and from the helper methods in the WooCommerce class.
* An example class that uses the legacy proxy both from a dependency injected proxy and from the helper methods in the WooCommerce class.
*/
class ClassThatDependsOnLegacyCode {
/**
* The injected LegacyProxy.
*
@ -19,11 +20,13 @@ class ClassThatDependsOnLegacyCode {
private $legacy_proxy;
/**
* Class constructor.
* Initialize the class instance.
*
* @internal
*
* @param LegacyProxy $legacy_proxy The instance of LegacyProxy to use.
*/
public function __construct( LegacyProxy $legacy_proxy ) {
final public function init( LegacyProxy $legacy_proxy ) {
$this->legacy_proxy = $legacy_proxy;
}

View File

@ -5,6 +5,7 @@
namespace Automattic\WooCommerce\Tests\Proxies;
use Automattic\WooCommerce\Internal\DependencyManagement\Definition;
use Automattic\WooCommerce\Proxies\LegacyProxy;
use Automattic\WooCommerce\Tests\Internal\DependencyManagement\ExampleClasses\DependencyClass;
@ -31,7 +32,7 @@ class LegacyProxyTest extends \WC_Unit_Test_Case {
*/
public function test_get_instance_of_throws_when_trying_to_get_a_namespaced_class() {
$this->expectException( \Exception::class );
$this->expectExceptionMessage( 'The LegacyProxy class is not intended for getting instances of classes in the src directory, please use constructor injection or the instance of \Psr\Container\ContainerInterface for that.' );
$this->expectExceptionMessage( 'The LegacyProxy class is not intended for getting instances of classes in the src directory, please use ' . Definition::INJECTION_METHOD . ' method injection or the instance of Psr\Container\ContainerInterface for that.' );
$this->sut->get_instance_of( DependencyClass::class );
}

View File

@ -0,0 +1,51 @@
<?php
namespace Automattic\WooCommerce\Tests\Utilities;
use Automattic\WooCommerce\Utilities\StringUtil;
/**
* A collection of tests for the string utility class.
*/
class StringUtilTest extends \WC_Unit_Test_Case {
/**
* @testdox `starts_with` should check whether one string starts with another.
*/
public function test_starts_with() {
$this->assertTrue( StringUtil::starts_with( 'test', 'te' ) );
$this->assertTrue( StringUtil::starts_with( ' foo bar', ' foo' ) );
$this->assertFalse( StringUtil::starts_with( 'test', 'st' ) );
$this->assertFalse( StringUtil::starts_with( ' foo bar', ' bar' ) );
$this->assertTrue( StringUtil::starts_with( 'TEST', 'te', false ) );
$this->assertTrue( StringUtil::starts_with( ' FOO BAR', ' foo', false ) );
$this->assertFalse( StringUtil::starts_with( 'TEST', 'st', false ) );
$this->assertFalse( StringUtil::starts_with( ' FOO BAR', ' bar', false ) );
$this->assertTrue( StringUtil::starts_with( 'test', 'TE', false ) );
$this->assertTrue( StringUtil::starts_with( ' foo bar', ' FOO', false ) );
$this->assertFalse( StringUtil::starts_with( 'test', 'ST', false ) );
$this->assertFalse( StringUtil::starts_with( ' foo bar', ' BAR', false ) );
}
/**
* @testdox `ends_with` should check whether one string ends with another.
*/
public function test_ends_with() {
$this->assertFalse( StringUtil::ends_with( 'test', 'te' ) );
$this->assertFalse( StringUtil::ends_with( ' foo bar', ' foo' ) );
$this->assertTrue( StringUtil::ends_with( 'test', 'st' ) );
$this->assertTrue( StringUtil::ends_with( ' foo bar', ' bar' ) );
$this->assertFalse( StringUtil::ends_with( 'TEST', 'te', false ) );
$this->assertFalse( StringUtil::ends_with( ' FOO BAR', ' foo', false ) );
$this->assertTrue( StringUtil::ends_with( 'TEST', 'st', false ) );
$this->assertTrue( StringUtil::ends_with( ' FOO BAR', ' bar', false ) );
$this->assertFalse( StringUtil::ends_with( 'test', 'TE', false ) );
$this->assertFalse( StringUtil::ends_with( ' foo bar', ' FOO', false ) );
$this->assertTrue( StringUtil::ends_with( 'test', 'ST', false ) );
$this->assertTrue( StringUtil::ends_with( ' foo bar', ' BAR', false ) );
}
}