diff --git a/.distignore b/.distignore index eb33c4ac00f..0eb76a6e10c 100644 --- a/.distignore +++ b/.distignore @@ -8,8 +8,9 @@ /node_modules/ /tests/ babel.config.js -CHANGELOG.txt +changelog.txt composer.* +tsconfig.* contributors.html docker-compose.yaml Dockerfile diff --git a/.gitattributes b/.gitattributes index 43d37bff513..782ca411dd7 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,13 +1,20 @@ -/.* export-ignore -bin export-ignore +# Set the default behavior, in case people don't have `core.autocrlf` set. +* text=auto + +# Declare files that will always have LF line endings on checkout. +*.php text eol=lf + +# Remove files for archives generated using `git archive`. +/.* export-ignore +bin export-ignore CODE_OF_CONDUCT.md export-ignore -CHANGELOG.txt export-ignore -composer.* export-ignore -Gruntfile.js export-ignore -package.json export-ignore -package-lock.json export-ignore -phpcs.xml export-ignore -phpunit.* export-ignore -README.md export-ignore -tests export-ignore -renovate.json export-ignore +changelog.txt export-ignore +composer.* export-ignore +Gruntfile.js export-ignore +package.json export-ignore +package-lock.json export-ignore +phpcs.xml export-ignore +phpunit.* export-ignore +README.md export-ignore +renovate.json export-ignore +tests export-ignore diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 3cfefb166e2..e5f93efa41c 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -11,9 +11,7 @@ jobs: uses: actions/checkout@v2 - name: Build id: build - uses: woocommerce/action-build@master - with: - generate-zip: true + uses: woocommerce/action-build@v2 - name: Upload release asset uses: actions/upload-release-asset@v1 env: diff --git a/.github/workflows/nightly-builds.yml b/.github/workflows/nightly-builds.yml index c5bf5a3c3e5..1c60108c3e9 100644 --- a/.github/workflows/nightly-builds.yml +++ b/.github/workflows/nightly-builds.yml @@ -17,9 +17,7 @@ jobs: ref: ${{ matrix.build }} - name: Build id: build - uses: woocommerce/action-build@master - with: - generate-zip: true + uses: woocommerce/action-build@v2 - name: Deploy nightly build uses: WebFreak001/deploy-nightly@v1.0.3 env: diff --git a/.gitignore b/.gitignore index 4906d0e7c9e..d9923d9613d 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/.travis.yml b/.travis.yml index a81ebb563af..b813f766610 100644 --- a/.travis.yml +++ b/.travis.yml @@ -33,8 +33,7 @@ jobs: php: 7.4 env: WP_VERSION=latest WP_MULTISITE=0 RUN_E2E=1 script: - - composer require wp-cli/i18n-command - - npm run build + - npm run build:assets - npm run build:packages - npm install jest --global - npm run docker:up diff --git a/.wordpress-org/banner-1544x500.png b/.wordpress-org/banner-1544x500.png index 42c75cecd43..003b2658acb 100644 Binary files a/.wordpress-org/banner-1544x500.png and b/.wordpress-org/banner-1544x500.png differ diff --git a/.wordpress-org/banner-772x250.png b/.wordpress-org/banner-772x250.png index e4187e3a9d6..98e431539a2 100644 Binary files a/.wordpress-org/banner-772x250.png and b/.wordpress-org/banner-772x250.png differ diff --git a/.wordpress-org/icon-128x128.png b/.wordpress-org/icon-128x128.png index 08df15c0748..8c6878b8e22 100644 Binary files a/.wordpress-org/icon-128x128.png and b/.wordpress-org/icon-128x128.png differ diff --git a/.wordpress-org/icon-256x256.png b/.wordpress-org/icon-256x256.png index a2508f0b84c..c2a550f6ac7 100644 Binary files a/.wordpress-org/icon-256x256.png and b/.wordpress-org/icon-256x256.png differ diff --git a/.wordpress-org/screenshot-1.jpg b/.wordpress-org/screenshot-1.jpg index d5383232d5b..2d02e6d15d6 100644 Binary files a/.wordpress-org/screenshot-1.jpg and b/.wordpress-org/screenshot-1.jpg differ diff --git a/.wordpress-org/screenshot-2.jpg b/.wordpress-org/screenshot-2.jpg index 0fef945a86e..5bf160d8740 100644 Binary files a/.wordpress-org/screenshot-2.jpg and b/.wordpress-org/screenshot-2.jpg differ diff --git a/.wordpress-org/screenshot-3.jpg b/.wordpress-org/screenshot-3.jpg index 2468c849b03..fd6331120cd 100644 Binary files a/.wordpress-org/screenshot-3.jpg and b/.wordpress-org/screenshot-3.jpg differ diff --git a/.wordpress-org/screenshot-4.jpg b/.wordpress-org/screenshot-4.jpg index a671aef0770..a29d971daed 100644 Binary files a/.wordpress-org/screenshot-4.jpg and b/.wordpress-org/screenshot-4.jpg differ diff --git a/.wordpress-org/screenshot-5.jpg b/.wordpress-org/screenshot-5.jpg index c3049936a43..5e923df74d4 100644 Binary files a/.wordpress-org/screenshot-5.jpg and b/.wordpress-org/screenshot-5.jpg differ diff --git a/.wordpress-org/screenshot-6.jpg b/.wordpress-org/screenshot-6.jpg index 8d00d366518..0f461eee709 100644 Binary files a/.wordpress-org/screenshot-6.jpg and b/.wordpress-org/screenshot-6.jpg differ diff --git a/assets/css/admin.scss b/assets/css/admin.scss index 4e6cc55ab3d..c855575e7cf 100644 --- a/assets/css/admin.scss +++ b/assets/css/admin.scss @@ -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 */ diff --git a/assets/js/admin/meta-boxes-order.js b/assets/js/admin/meta-boxes-order.js index 74eb0bd9e60..750558aa44b 100644 --- a/assets/js/admin/meta-boxes-order.js +++ b/assets/js/admin/meta-boxes-order.js @@ -1315,6 +1315,7 @@ jQuery( function ( $ ) { }; $.post( woocommerce_admin_meta_boxes.ajax_url, data, function( response ) { + $( 'ul.order_notes .no-items' ).remove(); $( 'ul.order_notes' ).prepend( response ); $( '#woocommerce-order-notes' ).unblock(); $( '#add_order_note' ).val( '' ); diff --git a/assets/js/frontend/address-i18n.js b/assets/js/frontend/address-i18n.js index 6ab1bfbeda2..7b851445cd0 100644 --- a/assets/js/frontend/address-i18n.js +++ b/assets/js/frontend/address-i18n.js @@ -103,8 +103,10 @@ jQuery( function( $ ) { } // Class changes. - field.removeClass( 'form-row-first form-row-last form-row-wide' ); - field.addClass( fieldLocale.class.join( ' ' ) ); + if ( Array.isArray( fieldLocale.class ) ) { + field.removeClass( 'form-row-first form-row-last form-row-wide' ); + field.addClass( fieldLocale.class.join( ' ' ) ); + } }); var fieldsets = $( diff --git a/bin/build-zip.sh b/bin/build-zip.sh index a80eaa9c928..79cf3b74442 100755 --- a/bin/build-zip.sh +++ b/bin/build-zip.sh @@ -13,20 +13,19 @@ echo "Installing PHP and JS dependencies..." npm install composer install || exit "$?" echo "Running JS Build..." -npm run build || exit "$?" +npm run build:core || exit "$?" echo "Cleaning up PHP dependencies..." composer install --no-dev || exit "$?" echo "Syncing files..." rsync -rc --exclude-from="$PROJECT_PATH/.distignore" "$PROJECT_PATH/" "$DEST_PATH/" --delete --delete-excluded -echo "Restoring PHP dependencies..." -composer install || exit "$?" -npm run build || exit "$?" - echo "Generating zip file..." cd "$BUILD_PATH" || exit zip -q -r "${PLUGIN_SLUG}.zip" "$PLUGIN_SLUG/" -echo "$BUILD_PATH/${PLUGIN_SLUG}.zip file generated!" + +cd "$PROJECT_PATH" || exit +mv "$BUILD_PATH/${PLUGIN_SLUG}.zip" "$PROJECT_PATH" +echo "${PLUGIN_SLUG}.zip file generated!" echo "Build done!" diff --git a/CHANGELOG.txt b/changelog.txt similarity index 98% rename from CHANGELOG.txt rename to changelog.txt index 23bcd8812c6..74a02f43c58 100644 --- a/CHANGELOG.txt +++ b/changelog.txt @@ -1,5 +1,49 @@ == Changelog == += 4.5.0 - 2020-09-08 = + +**WooCommerce** +* Localization - Added postcode validation for Bosnia and Herzegovina. #27048 +* Localization - Added the postcode validation for Liechtenstein. #27059 +* Localization - Add i18n locale information for Liechtenstein, Switzerland and Austria. #27193 +* Tweak - Increase priority of `admin_body_class` filter to avoid comflict with plugins that incorrectly remove all body classes from WP. #27426 +* Tweak - Rename built-in PayPal payment method to PayPal Standard. #27468 +* Fix - Remove whitespace within a link. #26897 +* Fix - `get_review_count_for_product` return all comments count not only 'review' types #26928 +* Fix - Hidden field type is now supported by `woocommerce_form_field`. #27023 +* Fix - Remove state for country liechtenstein. #27057 +* Fix - Fixed validation of variation attributes while adding products to the cart. #27115 +* Fix - Coupon code inconsistent between admins and shop owners. #27140 +* Fix - Fixed the logic behind "Hide shipping costs until an address is entered". #27143 +* Fix - Searches for variations now will fallback to parent SKU if one is not entered. #27171 +* Fix - Release coupon holds for cancelled orders previously in pending status. #27179 +* Fix - Fixes Japan zip code format issue (dash is now optional). #27244 +* Fix - Restore backward compatibility with WC 4.x and forward compatibility with WC 5.5. #27318 +* Fix - Switch to site locale before translating refund reason. #27323 +* Fix - Declare `WC_Post_Types::updated_term_messages` as a static method to remove PHP deprecation warning. #27436 +* Fix - Allow HTML to be entered in product title for formatting purposes. #27465 +* Dev - Added additional stock-based cart filters including `woocommerce_cart_product_cannot_add_another_message`, `woocommerce_cart_product_out_of_stock_message`, and `woocommerce_cart_product_not_enough_stock_message`. #26439 +* Dev - Changed text domain to `woocommerce` for REST API files. #27248 +* Dev - Added file path to the `woocommerce_file_download_method` filter. #27152 +* Dev - Merge API Package into core. #27100 + +**WooCommerce Admin 1.5.0** +* Enhancement - Add eWAY to Payment Setup for AU/NZ Stores. #4947 +* Fix - Use clipRule and fillRule props. #4889, part of #4864 +* Dev - New notification: Don't forget to test your checkout. #4805 +* Dev - Enable tax calculation before redirecting to standard tax rates page. #4878 +* Dev - Added event recording to Orders, Stock, and Reviews panels. #4861 +* Dev - Added personalization to purchase extension task. #4849 +* Dev - Display modal with more info about the new homescreen. #4890 +* Dev - Task list - add a shortcut back to store setup. #4853 +* Dev - Update the colors of the illustrations in the welcome modal. #4945 + += 4.4.1 - 2020-08-19 = + +**WooCommerce** +* Fix - Add protection to run adjust methods only if product query. #27396 +* Dev - Stripped the internals of the DI Container to address plugin dependency conflicts it caused. #27395 + = 4.4.0 - 2020-08-18 = **WooCommerce** @@ -58,8 +102,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 +131,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 +157,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 @@ -130,6 +186,11 @@ * Fix - 'Product Summary' in All Products block is not pulling in the short description of the product. #2913 * Dev - Add query filter when searching for a table. #2886 += 4.3.3 - 2020-08-14 = + +**WooCommerce REST API 1.0.10-pl-2** +* Fix - Fix regression and restore backward compatibility for date-time and mixed datatypes. #239 + = 4.3.2 - 2020-08-10 = **WooCommerce** @@ -811,7 +872,7 @@ * Tweak - Remove the left and right margin from the logo in emails. #23360 * Tweak - Use the high res version of the WP spinner in the coupon Block UI. #23364 * Tweak - Improve user registration validation messages. #23468 -* Tweak - Auto generate a new username when a username is blacklisted by WordPress. #23672 +* Tweak - Auto generate a new username when a username is blocked by WordPress. #23672 * Tweak - Guest cart sessions now gets deleted when a user logs in, preventing duplicate cart sessions. #23687 * Tweak - Include the store's base postcode and city when calculating order taxes. #23695 * Tweak - Update the generate username setting description label to reflect how the username is actually generated. #23911 @@ -2973,7 +3034,7 @@ * Fix - Make save button clickable in tax rate table after using autocomplete field. * Fix - Fix passed image_size variable in email templates. * Fix - Don't show purchase note to admin in emails. -* Fix - Fix 'hide empty' setting in category widget . +* Fix - Fix 'hide empty' setting in category widget. * Fix - Prevent notice in get_allowed_countries. * Fix - Prevent add-to-cart querystring in pagination links. * Tweak - Allow propagation in variation script. @@ -3005,7 +3066,7 @@ * Fix - Show the taxable country rather than base country in "estimated for" text during checkout. * Fix - Prevent select2 gaining focus on IOS7 scroll. * Fix - API - Fix indexes on decimal and thousand values. -* Tweak - Clear cron jobs on uninstall . +* Tweak - Clear cron jobs on uninstall. * Tweak - Don't disable place order button on checkout if a weak password is used. * Tweak - Added password strength meter in lost password and edit accout pages. * Tweak - Pass $args to woocommerce_dropdown_variation_attribute_options_html hook. @@ -3075,10 +3136,10 @@ = 2.4.11 - 2015-12-7 = * Fix - WordPress 4.4 support. -* Fix - Removes Switzerland from EU VAT definition . +* Fix - Removes Switzerland from EU VAT definition. * Fix - Fix auth endpoint urls. * Fix - To allow backslash in SKUs. -* Fix - Sanity check for min/max quantity . +* Fix - Sanity check for min/max quantity. * Fix - 4.4 - Shipping class menu display. * Fix - 4.4 - Admin menu icons and styling. * Fix - API - Variable product backorders editing. @@ -3115,9 +3176,9 @@ * Fix - Shipping priority for methods with colons in the name. * Fix - Saving of passwords with '&' inside. * Fix - Remove double escaping of coupon descriptions. -* Fix - Settings API default value should not apply if value of option is 0 . +* Fix - Settings API default value should not apply if value of option is 0. * Fix - Avoid potential PHP Fatals by avoiding premature script enqueues. -* Fix - Pass mimes when checking file type . +* Fix - Pass mimes when checking file type. * Fix - Reset shipping totals before calculation to prevent totals being used incorrectly. * Fix - API - Corrected how attributes terms saves non-latin characters. * Fix - API - Variations price sync. @@ -3147,7 +3208,7 @@ * Fix - Network activated plugins not showing up in system status report. * Fix - Tax fields showing on bulk/quick edit when disabled the tax system. * Fix - Tax status and tax class values within bulk edit. -* Tweak - Allow bulk edit price to 0 . +* Tweak - Allow bulk edit price to 0. * Tweak - Add filters to control "shipped via" text. * Tweak - Allow line breaks in non-variation attributes. * Tweak - Renamed wc_var_prices transient to allow them to flush on product save. @@ -3907,7 +3968,7 @@ * Fix - Fix bulk editing variation sale price. * Fix - Remove comment exclusion in order notes meta box. * Fix - Sync min and max prices for regular and sale prices so prices are displayed correctly when sale price is lower than a regular price of another variation. -* Fix - Expanding line item_meta causes conflicts if attributes are named with things like 'name', 'type' or 'qty'. Added blacklist to exclude unsafe values. +* Fix - Expanding line item_meta causes conflicts if attributes are named with things like 'name', 'type' or 'qty'. Added blocklist to exclude unsafe values. * Fix - Added support for clearing report transients when using object caching. * Fix - encoding issues with attribute values. * Fix - Escape the contents of the changelog when displayed. diff --git a/composer.json b/composer.json index b870e932ddc..d90ac304aa6 100644 --- a/composer.json +++ b/composer.json @@ -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", + "psr/container": "^1.0", "woocommerce/action-scheduler": "3.1.6", - "woocommerce/woocommerce-admin": "1.4.0-beta.3", + "woocommerce/woocommerce-admin": "1.5.0-rc.2", "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": { diff --git a/composer.lock b/composer.lock index 9c95657f4c1..8783ff5ecc7 100644 --- a/composer.lock +++ b/composer.lock @@ -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": "6f5bef4c75b0d62b2f3d9bc2458eff03", "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", @@ -195,78 +195,6 @@ ], "time": "2019-08-12T15:00:31+00:00" }, - { - "name": "league/container", - "version": "3.3.1", - "source": { - "type": "git", - "url": "https://github.com/thephpleague/container.git", - "reference": "93238f74ff5964aee27a78508cdfbdba1cd338f6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/thephpleague/container/zipball/93238f74ff5964aee27a78508cdfbdba1cd338f6", - "reference": "93238f74ff5964aee27a78508cdfbdba1cd338f6", - "shasum": "" - }, - "require": { - "php": "^7.0", - "psr/container": "^1.0" - }, - "provide": { - "psr/container-implementation": "^1.0" - }, - "replace": { - "orno/di": "~2.0" - }, - "require-dev": { - "phpunit/phpunit": "^6.0", - "squizlabs/php_codesniffer": "^3.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-3.x": "3.x-dev", - "dev-2.x": "2.x-dev", - "dev-1.x": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "League\\Container\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Phil Bennett", - "email": "philipobenito@gmail.com", - "homepage": "http://www.philipobenito.com", - "role": "Developer" - } - ], - "description": "A fast and intuitive dependency injection container.", - "homepage": "https://github.com/thephpleague/container", - "keywords": [ - "container", - "dependency", - "di", - "injection", - "league", - "provider", - "service" - ], - "funding": [ - { - "url": "https://github.com/philipobenito", - "type": "github" - } - ], - "time": "2020-05-18T08:20:23+00:00" - }, { "name": "maxmind-db/reader", "version": "v1.6.0", @@ -501,20 +429,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 +468,16 @@ }, { "name": "woocommerce/woocommerce-admin", - "version": "v1.4.0-beta.3", + "version": "1.5.0-rc.2", "source": { "type": "git", "url": "https://github.com/woocommerce/woocommerce-admin.git", - "reference": "df2af46a8552cdee15df0030fccbe4cd5a6d270d" + "reference": "bb2fbb0e105e419478b09a15dc4b43c8f1426381" }, "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/bb2fbb0e105e419478b09a15dc4b43c8f1426381", + "reference": "bb2fbb0e105e419478b09a15dc4b43c8f1426381", "shasum": "" }, "require": { @@ -597,7 +511,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-20T23:35:02+00:00" }, { "name": "woocommerce/woocommerce-blocks", @@ -650,22 +564,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 +626,7 @@ "stylecheck", "tests" ], - "time": "2020-01-29T20:22:20+00:00" + "time": "2020-06-25T14:57:39+00:00" }, { "name": "doctrine/instantiator", @@ -768,20 +682,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 +944,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" }, { @@ -1775,6 +1669,7 @@ "keywords": [ "tokenizer" ], + "abandoned": true, "time": "2019-09-17T06:23:10+00:00" }, { @@ -2478,16 +2373,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 +2420,7 @@ "phpcs", "standards" ], - "time": "2020-04-17T01:09:41+00:00" + "time": "2020-08-10T04:50:15+00:00" }, { "name": "symfony/finder", @@ -2574,20 +2469,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 +2624,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 +2660,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 +2881,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 +2900,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 +2923,7 @@ "standards", "wordpress" ], - "time": "2020-02-04T02:52:06+00:00" + "time": "2020-05-13T23:57:56+00:00" } ], "aliases": [], @@ -3055,6 +2937,5 @@ "platform-dev": [], "platform-overrides": { "php": "7.1" - }, - "plugin-api-version": "1.1.0" + } } diff --git a/includes/admin/class-wc-admin-customize.php b/includes/admin/class-wc-admin-customize.php index cba3f2b4e68..317f6eb3f37 100644 --- a/includes/admin/class-wc-admin-customize.php +++ b/includes/admin/class-wc-admin-customize.php @@ -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' ) ) { diff --git a/includes/admin/class-wc-admin-pointers.php b/includes/admin/class-wc-admin-pointers.php index 23242fe3f8d..1ca73026713 100644 --- a/includes/admin/class-wc-admin-pointers.php +++ b/includes/admin/class-wc-admin-pointers.php @@ -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 ) ); diff --git a/includes/admin/class-wc-admin.php b/includes/admin/class-wc-admin.php index e864cd80e1f..80db7148ab2 100644 --- a/includes/admin/class-wc-admin.php +++ b/includes/admin/class-wc-admin.php @@ -35,7 +35,7 @@ class WC_Admin { add_filter( 'action_scheduler_post_type_args', array( $this, 'disable_webhook_post_export' ) ); // Add body class for WP 5.3+ compatibility. - add_filter( 'admin_body_class', array( $this, 'include_admin_body_class' ) ); + add_filter( 'admin_body_class', array( $this, 'include_admin_body_class' ), 9999 ); } /** @@ -137,7 +137,7 @@ class WC_Admin { } // phpcs:disable WordPress.Security.NonceVerification.Recommended - // Nonced plugin install redirects (whitelisted). + // Nonced plugin install redirects. if ( ! empty( $_GET['wc-install-plugin-redirect'] ) ) { $plugin_slug = wc_clean( wp_unslash( $_GET['wc-install-plugin-redirect'] ) ); @@ -318,7 +318,7 @@ class WC_Admin { * @return string */ public function include_admin_body_class( $classes ) { - if ( false !== strpos( $classes, 'wc-wp-version-gte-53' ) ) { + if ( in_array( array( 'wc-wp-version-gte-53', 'wc-wp-version-gte-55' ), explode( ' ', $classes ), true ) ) { return $classes; } diff --git a/includes/admin/meta-boxes/class-wc-meta-box-coupon-data.php b/includes/admin/meta-boxes/class-wc-meta-box-coupon-data.php index 5a855e33cec..f6489123a4d 100644 --- a/includes/admin/meta-boxes/class-wc-meta-box-coupon-data.php +++ b/includes/admin/meta-boxes/class-wc-meta-box-coupon-data.php @@ -267,7 +267,7 @@ class WC_Meta_Box_Coupon_Data { 'id' => 'customer_email', 'label' => __( 'Allowed emails', 'woocommerce' ), 'placeholder' => __( 'No restrictions', 'woocommerce' ), - 'description' => __( 'Whitelist of billing emails to check against when an order is placed. Separate email addresses with commas. You can also use an asterisk (*) to match parts of an email. For example "*@gmail.com" would match all gmail addresses.', 'woocommerce' ), + 'description' => __( 'List of allowed billing emails to check against when an order is placed. Separate email addresses with commas. You can also use an asterisk (*) to match parts of an email. For example "*@gmail.com" would match all gmail addresses.', 'woocommerce' ), 'value' => implode( ', ', (array) $coupon->get_email_restrictions( 'edit' ) ), 'desc_tip' => true, 'type' => 'email', diff --git a/includes/admin/meta-boxes/views/html-order-notes.php b/includes/admin/meta-boxes/views/html-order-notes.php index 50db87bcea2..906c3d700c9 100644 --- a/includes/admin/meta-boxes/views/html-order-notes.php +++ b/includes/admin/meta-boxes/views/html-order-notes.php @@ -41,7 +41,7 @@ defined( 'ABSPATH' ) || exit; } } else { ?> -
+ diff --git a/includes/class-wc-cart.php b/includes/class-wc-cart.php index 42f9ac630ac..f316f8817e2 100644 --- a/includes/class-wc-cart.php +++ b/includes/class-wc-cart.php @@ -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 ); } diff --git a/includes/class-wc-comments.php b/includes/class-wc-comments.php index 2900e97d5e2..7c92fb46d24 100644 --- a/includes/class-wc-comments.php +++ b/includes/class-wc-comments.php @@ -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() ) diff --git a/includes/class-wc-download-handler.php b/includes/class-wc-download-handler.php index a2826527f43..fa279c8c6fd 100644 --- a/includes/class-wc-download-handler.php +++ b/includes/class-wc-download-handler.php @@ -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. diff --git a/includes/class-wc-form-handler.php b/includes/class-wc-form-handler.php index e64a0b3fcb6..cf2a7b809e7 100644 --- a/includes/class-wc-form-handler.php +++ b/includes/class-wc-form-handler.php @@ -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…', '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 diff --git a/includes/class-wc-install.php b/includes/class-wc-install.php index 6a45e7b726a..84e42a1c083 100644 --- a/includes/class-wc-install.php +++ b/includes/class-wc-install.php @@ -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` diff --git a/includes/class-wc-post-types.php b/includes/class-wc-post-types.php index 79c65823ccc..0ffb4f989d2 100644 --- a/includes/class-wc-post-types.php +++ b/includes/class-wc-post-types.php @@ -489,7 +489,7 @@ class WC_Post_Types { * @since 4.4.0 * @return bool */ - public function updated_term_messages( $messages ) { + public static function updated_term_messages( $messages ) { $messages['product_cat'] = array( 0 => '', 1 => __( 'Category added.', 'woocommerce' ), diff --git a/includes/class-wc-query.php b/includes/class-wc-query.php index ba787df3b99..60ce791c63b 100644 --- a/includes/class-wc-query.php +++ b/includes/class-wc-query.php @@ -44,8 +44,6 @@ class WC_Query { add_filter( 'query_vars', array( $this, 'add_query_vars' ), 0 ); add_action( 'parse_request', array( $this, 'parse_request' ), 0 ); add_action( 'pre_get_posts', array( $this, 'pre_get_posts' ) ); - add_filter( 'the_posts', array( $this, 'handle_get_posts' ) ); - add_filter( 'found_posts', array( $this, 'adjust_posts_count' ), 10, 2 ); add_filter( 'get_pagenum_link', array( $this, 'remove_add_to_cart_pagination' ), 10, 1 ); } $this->init_query_vars(); @@ -355,11 +353,15 @@ class WC_Query { /** * Handler for the 'the_posts' WP filter. * - * @param array $posts Posts from WP Query. + * @param array $posts Posts from WP Query. + * @param WP_Query $query Current query. * * @return array */ - public function handle_get_posts( $posts ) { + public function handle_get_posts( $posts, $query ) { + if ( 'product_query' !== $query->get( 'wc_query' ) ) { + return $posts; + } $this->adjust_total_pages(); $this->remove_product_query_filters( $posts ); return $posts; @@ -511,7 +513,8 @@ class WC_Query { // Additonal hooks to change WP Query. add_filter( 'posts_clauses', array( $this, 'price_filter_post_clauses' ), 10, 2 ); - + add_filter( 'the_posts', array( $this, 'handle_get_posts' ), 10, 2 ); + add_filter( 'found_posts', array( $this, 'adjust_posts_count' ), 10, 2 ); do_action( 'woocommerce_product_query', $q, $this ); } diff --git a/includes/class-wc-regenerate-images.php b/includes/class-wc-regenerate-images.php index 023deabfd83..6e6ed20b027 100644 --- a/includes/class-wc-regenerate-images.php +++ b/includes/class-wc-regenerate-images.php @@ -203,7 +203,7 @@ class WC_Regenerate_Images { return $image; } - // Use a whitelist of sizes we want to resize. Ignore others. + // List of sizes we want to resize. Ignore others. if ( ! $image || ! in_array( $size, apply_filters( 'woocommerce_image_sizes_to_resize', array( 'woocommerce_thumbnail', 'woocommerce_gallery_thumbnail', 'woocommerce_single', 'shop_thumbnail', 'shop_catalog', 'shop_single' ) ), true ) ) { return $image; } diff --git a/includes/class-wc-validation.php b/includes/class-wc-validation.php index 5b0248e2e5d..d28428aa6b2 100644 --- a/includes/class-wc-validation.php +++ b/includes/class-wc-validation.php @@ -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; diff --git a/includes/class-woocommerce.php b/includes/class-woocommerce.php index 2bad0bf07b0..4fab93f0e02 100644 --- a/includes/class-woocommerce.php +++ b/includes/class-woocommerce.php @@ -912,60 +912,4 @@ final class WooCommerce { public function is_wc_admin_active() { return function_exists( 'wc_admin_url' ); } - - /** - * Call a user function. This should be used to execute any non-idempotent function, especially - * those in the `includes` directory or provided by WordPress. - * - * This method can be useful for unit tests, since functions called using this method - * can be easily mocked by using WC_Unit_Test_Case::register_legacy_proxy_function_mocks. - * - * @param string $function_name The function to execute. - * @param mixed ...$parameters The parameters to pass to the function. - * - * @return mixed The result from the function. - * - * @since 4.4 - */ - public function call_function( $function_name, ...$parameters ) { - return wc_get_container()->get( LegacyProxy::class )->call_function( $function_name, ...$parameters ); - } - - /** - * Call a static method in a class. This should be used to execute any non-idempotent method in classes - * from the `includes` directory. - * - * This method can be useful for unit tests, since methods called using this method - * can be easily mocked by using WC_Unit_Test_Case::register_legacy_proxy_static_mocks. - * - * @param string $class_name The name of the class containing the method. - * @param string $method_name The name of the method. - * @param mixed ...$parameters The parameters to pass to the method. - * - * @return mixed The result from the method. - * - * @since 4.4 - */ - public function call_static( $class_name, $method_name, ...$parameters ) { - return wc_get_container()->get( LegacyProxy::class )->call_static( $class_name, $method_name, ...$parameters ); - } - - /** - * Gets an instance of a given legacy class. - * This must not be used to get instances of classes in the `src` directory. - * - * This method can be useful for unit tests, since objects obtained using this method - * can be easily mocked by using WC_Unit_Test_Case::register_legacy_proxy_class_mocks. - * - * @param string $class_name The name of the class to get an instance for. - * @param mixed ...$args Parameters to be passed to the class constructor or to the appropriate internal 'get_instance_of_' method. - * - * @return object The instance of the class. - * @throws \Exception The requested class belongs to the `src` directory, or there was an error creating an instance of the class. - * - * @since 4.4 - */ - public function get_instance_of( string $class_name, ...$args ) { - return wc_get_container()->get( LegacyProxy::class )->get_instance_of( $class_name, ...$args ); - } } diff --git a/includes/data-stores/class-wc-product-data-store-cpt.php b/includes/data-stores/class-wc-product-data-store-cpt.php index 9e17fb52869..6158a4cad86 100644 --- a/includes/data-stores/class-wc-product-data-store-cpt.php +++ b/includes/data-stores/class-wc-product-data-store-cpt.php @@ -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 @@ -1692,7 +1708,7 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da $product_type = 'variation'; } elseif ( 'product' === $post_type ) { $terms = get_the_terms( $product_id, 'product_type' ); - $product_type = ! empty( $terms ) ? sanitize_title( current( $terms )->name ) : 'simple'; + $product_type = ! empty( $terms ) && ! is_wp_error( $terms ) ? sanitize_title( current( $terms )->name ) : 'simple'; } else { $product_type = false; } diff --git a/includes/gateways/paypal/class-wc-gateway-paypal.php b/includes/gateways/paypal/class-wc-gateway-paypal.php index 6cd1a03dd83..2094717896b 100644 --- a/includes/gateways/paypal/class-wc-gateway-paypal.php +++ b/includes/gateways/paypal/class-wc-gateway-paypal.php @@ -42,7 +42,7 @@ class WC_Gateway_Paypal extends WC_Payment_Gateway { $this->id = 'paypal'; $this->has_fields = false; $this->order_button_text = __( 'Proceed to PayPal', 'woocommerce' ); - $this->method_title = __( 'PayPal', 'woocommerce' ); + $this->method_title = __( 'PayPal Standard', 'woocommerce' ); /* translators: %s: Link to WC system status page */ $this->method_description = __( 'PayPal Standard redirects customers to PayPal to enter their payment information.', 'woocommerce' ); $this->supports = array( @@ -283,7 +283,7 @@ class WC_Gateway_Paypal extends WC_Payment_Gateway { ?>- : + :
'; + + $this->assertEquals( $expected_html, $actual_html ); + + } } diff --git a/tests/legacy/unit-tests/totals/totals.php b/tests/legacy/unit-tests/totals/totals.php index 2edd6d00fd2..71cca3b3279 100644 --- a/tests/legacy/unit-tests/totals/totals.php +++ b/tests/legacy/unit-tests/totals/totals.php @@ -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() ); diff --git a/tests/legacy/unit-tests/util/class-wc-tests-core-functions.php b/tests/legacy/unit-tests/util/class-wc-tests-core-functions.php index 5db7b9dfec4..3cfd34aad6e 100644 --- a/tests/legacy/unit-tests/util/class-wc-tests-core-functions.php +++ b/tests/legacy/unit-tests/util/class-wc-tests-core-functions.php @@ -405,7 +405,7 @@ class WC_Tests_Core_Functions extends WC_Unit_Test_Case { ); // This filter will sequentially remove handlers, allowing us to test as though our - // functions were accumulatively blacklisted, adding one on each call. + // functions were accumulatively blocked, adding one on each call. add_filter( 'woocommerce_print_r_alternatives', array( $this, 'filter_wc_print_r_alternatives' ) ); $this->expectOutputString( diff --git a/tests/legacy/unit-tests/util/validation.php b/tests/legacy/unit-tests/util/validation.php index 1c08b029582..21827b43d35 100644 --- a/tests/legacy/unit-tests/util/validation.php +++ b/tests/legacy/unit-tests/util/validation.php @@ -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 ); } /** diff --git a/tests/php/includes/class-wc-cart-test.php b/tests/php/includes/class-wc-cart-test.php new file mode 100644 index 00000000000..093cbf2a4cb --- /dev/null +++ b/tests/php/includes/class-wc-cart-test.php @@ -0,0 +1,102 @@ +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( '' ); + } +} diff --git a/tests/php/includes/data-stores/class-wc-product-data-store-cpt-test.php b/tests/php/includes/data-stores/class-wc-product-data-store-cpt-test.php new file mode 100644 index 00000000000..a2519bab3d6 --- /dev/null +++ b/tests/php/includes/data-stores/class-wc-product-data-store-cpt-test.php @@ -0,0 +1,41 @@ +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 ); + } +} diff --git a/tests/php/src/Internal/DependencyManagement/AbstractServiceProviderTest.php b/tests/php/src/Internal/DependencyManagement/AbstractServiceProviderTest.php deleted file mode 100644 index cc241e61301..00000000000 --- a/tests/php/src/Internal/DependencyManagement/AbstractServiceProviderTest.php +++ /dev/null @@ -1,211 +0,0 @@ -container = new ExtendedContainer(); - - $this->sut = new class() extends AbstractServiceProvider { - // phpcs:disable - - /** - * Public version of add_with_auto_arguments, which is usually protected. - */ - public function add_with_auto_arguments( string $class_name, $concrete = null, bool $shared = false ) : DefinitionInterface { - return parent::add_with_auto_arguments( $class_name, $concrete, $shared ); - } - - /** - * The mandatory 'register' method (defined in the base class as abstract). - * Not implemented because this class is tested on its own, not as a service provider actually registered on a container. - */ - public function register() {} - - // phpcs:enable - }; - - $this->sut->setContainer( $this->container ); - } - - /** - * Runs before all the tests of the class. - */ - public static function setUpBeforeClass() { - /** - * Return a new instance of ClassWithDependencies. - * - * @param DependencyClass $dependency The dependency to inject. - * @return ClassWithDependencies The new instance. - */ - function get_new_dependency_class( DependencyClass $dependency ) { - return new ClassWithDependencies( $dependency ); - }; - } - - /** - * @testdox 'add_with_auto_arguments' should throw an exception if an invalid class name is passed as class name. - */ - 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->sut->add_with_auto_arguments( 'foobar' ); - } - - /** - * @testdox 'add_with_auto_arguments' should throw an exception if the passed class has a private constructor. - */ - public function test_add_with_auto_arguments_throws_on_class_private_constructor() { - $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->sut->add_with_auto_arguments( ClassWithPrivateConstructor::class ); - } - - /** - * @testdox 'add_with_auto_arguments' should throw an exception if the passed concrete is a class with a private constructor. - */ - public function test_add_with_auto_arguments_throws_on_concrete_private_constructor() { - $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->sut->add_with_auto_arguments( ClassWithDependencies::class, ClassWithPrivateConstructor::class ); - } - - /** - * @testdox 'add_with_auto_arguments' should throw an exception if the passed class has a constructor argument without type hint. - */ - public function test_add_with_auto_arguments_throws_on_constructor_argument_without_type_hint() { - $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->sut->add_with_auto_arguments( ClassWithConstructorArgumentWithoutTypeHint::class ); - } - - /** - * @testdox 'add_with_auto_arguments' should throw an exception if the passed class has a constructor argument with a scalar type hint. - */ - public function test_add_with_auto_arguments_throws_on_constructor_argument_with_scalar_type_hint() { - $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->sut->add_with_auto_arguments( ClassWithScalarConstructorArgument::class ); - } - - /** - * @testdox 'add_with_auto_arguments' should properly register the supplied class when no concrete is passed. - * - * @testWith [true, 1] - * [false, 2] - * - * @param bool $shared Whether to register the test class as shared or not. - * @param int $expected_constructions_count Expected number of times that the test class will have been instantiated. - */ - public function test_add_with_auto_arguments_works_as_expected_with_no_concrete( bool $shared, int $expected_constructions_count ) { - ClassWithDependencies::$instances_count = 0; - - $this->container->share( DependencyClass::class ); - $this->sut->add_with_auto_arguments( ClassWithDependencies::class, null, $shared ); - - $this->container->get( ClassWithDependencies::class ); - $resolved = $this->container->get( ClassWithDependencies::class ); - - // A new instance is created for each resolution or not, depending on $shared. - $this->assertEquals( $expected_constructions_count, ClassWithDependencies::$instances_count ); - - // Arguments with default values are honored. - $this->assertEquals( ClassWithDependencies::SOME_NUMBER, $resolved->some_number ); - - // Constructor arguments are filled as expected. - $this->assertSame( $this->container->get( DependencyClass::class ), $resolved->dependency_class ); - } - - /** - * @testdox 'add_with_auto_arguments' should properly register the supplied class when a concrete representing a class name is passed. - */ - public function test_add_with_auto_arguments_works_as_expected_when_concrete_is_class_name() { - $this->sut->add_with_auto_arguments( ClassWithDependencies::class, DependencyClass::class ); - - $resolved = $this->container->get( ClassWithDependencies::class ); - - $this->assertInstanceOf( DependencyClass::class, $resolved ); - } - - /** - * @testdox 'add_with_auto_arguments' should properly register the supplied class when a concrete that is an object is passed. - */ - public function test_add_with_auto_arguments_works_as_expected_when_concrete_is_object() { - $object = new DependencyClass(); - - $this->sut->add_with_auto_arguments( ClassWithDependencies::class, $object ); - - $resolved = $this->container->get( ClassWithDependencies::class ); - - $this->assertSame( $object, $resolved ); - } - - /** - * @testdox 'add_with_auto_arguments' should properly register the supplied class when a concrete that is a closure is passed. - */ - public function test_add_with_auto_arguments_works_as_expected_when_concrete_is_a_closure() { - $this->container->share( DependencyClass::class ); - $callable = function( DependencyClass $dependency ) { - return new ClassWithDependencies( $dependency ); - }; - - $this->sut->add_with_auto_arguments( ClassWithDependencies::class, $callable ); - - $resolved = $this->container->get( ClassWithDependencies::class ); - - $this->assertInstanceOf( ClassWithDependencies::class, $resolved ); - } - - /** - * @testdox 'add_with_auto_arguments' should properly register the supplied class when a concrete that is a function name is passed. - */ - public function test_add_with_auto_arguments_works_as_expected_when_concrete_is_a_function_name() { - $this->container->share( DependencyClass::class ); - - $this->sut->add_with_auto_arguments( ClassWithDependencies::class, __NAMESPACE__ . '\get_new_dependency_class' ); - - $resolved = $this->container->get( ClassWithDependencies::class ); - - $this->assertInstanceOf( ClassWithDependencies::class, $resolved ); - } -} - diff --git a/tests/php/src/Internal/DependencyManagement/ExampleClasses/ClassWithConstructorArgumentWithoutTypeHint.php b/tests/php/src/Internal/DependencyManagement/ExampleClasses/ClassWithConstructorArgumentWithoutTypeHint.php deleted file mode 100644 index 6f62858af3a..00000000000 --- a/tests/php/src/Internal/DependencyManagement/ExampleClasses/ClassWithConstructorArgumentWithoutTypeHint.php +++ /dev/null @@ -1,20 +0,0 @@ -dependency_class = $dependency_class; - $this->some_number = $some_number; - } -} diff --git a/tests/php/src/Internal/DependencyManagement/ExampleClasses/ClassWithPrivateConstructor.php b/tests/php/src/Internal/DependencyManagement/ExampleClasses/ClassWithPrivateConstructor.php deleted file mode 100644 index 4e432a93a1e..00000000000 --- a/tests/php/src/Internal/DependencyManagement/ExampleClasses/ClassWithPrivateConstructor.php +++ /dev/null @@ -1,18 +0,0 @@ -sut = new ExtendedContainer(); - } - - /** - * @testdox 'add' should throw an exception when trying to register a class not in the WooCommerce root namespace. - */ - public function test_add_throws_when_trying_to_register_class_in_forbidden_namespace() { - $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->sut->add( $external_class ); - } - - /** - * @testdox 'add' should allow registering classes in the WooCommerce root namespace. - */ - public function test_add_allows_registering_classes_in_woocommerce_root_namespace() { - $instance = new DependencyClass(); - $this->sut->add( DependencyClass::class, $instance, true ); - $resolved = $this->sut->get( DependencyClass::class ); - - $this->assertSame( $instance, $resolved ); - } - - /** - * @testdox 'replace' should throw an exception when trying to replace a class that has not been previously registered. - */ - 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->sut->replace( DependencyClass::class, null ); - } - - /** - * @testdox 'replace' should allow to replace existing registrations. - */ - public function test_replace_allows_replacing_existing_registrations() { - $instance_1 = new DependencyClass(); - $instance_2 = new DependencyClass(); - - $this->sut->add( DependencyClass::class, $instance_1, true ); - $this->assertSame( $instance_1, $this->sut->get( DependencyClass::class ) ); - - $this->sut->replace( DependencyClass::class, $instance_2, true ); - $this->assertSame( $instance_2, $this->sut->get( DependencyClass::class ) ); - } - - /** - * @testdox 'reset_all_resolved' should discard cached resolutions for classes registered as 'shared'. - */ - public function test_reset_all_resolved_discards_cached_shared_resolutions() { - $this->sut->add( DependencyClass::class ); - $this->sut->add( ClassWithDependencies::class, null, true )->addArgument( DependencyClass::class ); - ClassWithDependencies::$instances_count = 0; - - $this->sut->get( ClassWithDependencies::class ); - $this->assertEquals( 1, ClassWithDependencies::$instances_count ); - $this->sut->get( ClassWithDependencies::class ); - $this->assertEquals( 1, ClassWithDependencies::$instances_count ); - - $this->sut->reset_all_resolved(); - - $this->sut->get( ClassWithDependencies::class ); - $this->assertEquals( 2, ClassWithDependencies::$instances_count ); - $this->sut->get( ClassWithDependencies::class ); - $this->assertEquals( 2, ClassWithDependencies::$instances_count ); - } -} diff --git a/tests/php/src/Proxies/ClassThatDependsOnLegacyCodeTest.php b/tests/php/src/Proxies/ClassThatDependsOnLegacyCodeTest.php deleted file mode 100644 index 2f1985c095d..00000000000 --- a/tests/php/src/Proxies/ClassThatDependsOnLegacyCodeTest.php +++ /dev/null @@ -1,126 +0,0 @@ -add( ClassThatDependsOnLegacyCode::class )->addArgument( LegacyProxy::class ); - $this->sut = $container->get( ClassThatDependsOnLegacyCode::class ); - } - - /** - * Legacy proxy's 'call_function' can be used from both an injected LegacyProxy and from 'WC()->call_function' - * - * @param string $method_to_use Method in the tested class to use. - * - * @testWith ["call_legacy_function_using_injected_proxy"] - * ["call_legacy_function_using_woocommerce_class"] - */ - public function test_call_function_can_be_invoked_via_injected_legacy_proxy_and_woocommerce_object( $method_to_use ) { - $this->assertEquals( 255, $this->sut->$method_to_use( 'hexdec', 'FF' ) ); - } - - /** - * Function mocks can be used from both an injected LegacyProxy and from 'WC()->call_function' - * - * @param string $method_to_use Method in the tested class to use. - * - * @testWith ["call_legacy_function_using_injected_proxy"] - * ["call_legacy_function_using_woocommerce_class"] - */ - public function test_function_mocks_can_be_used_via_injected_legacy_proxy_and_woocommerce_object( $method_to_use ) { - $this->register_legacy_proxy_function_mocks( - array( - 'hexdec' => function( $hex_string ) { - return "Mocked hexdec for $hex_string"; - }, - ) - ); - $this->assertEquals( 'Mocked hexdec for FF', $this->sut->$method_to_use( 'hexdec', 'FF' ) ); - } - - /** - * Legacy proxy's 'call_static' can be used from both an injected LegacyProxy and from 'WC()->call_function' - * - * @param string $method_to_use Method in the tested class to use. - * - * @testWith ["call_static_method_using_injected_proxy"] - * ["call_static_method_using_woocommerce_class"] - */ - public function test_call_static_can_be_invoked_via_injected_legacy_proxy_and_woocommerce_object( $method_to_use ) { - $result = $this->sut->$method_to_use( DependencyClass::class, 'concat', 'foo', 'bar', 'fizz' ); - $this->assertEquals( 'Parts: foo, bar, fizz', $result ); - } - - /** - * Static method mocks can be used from both an injected LegacyProxy and from 'WC()->call_function' - * - * @param string $method_to_use Method in the tested class to use. - * - * @testWith ["call_static_method_using_injected_proxy"] - * ["call_static_method_using_woocommerce_class"] - */ - public function test_static_mocks_can_be_used_via_injected_legacy_proxy_and_woocommerce_object( $method_to_use ) { - $this->register_legacy_proxy_static_mocks( - array( - DependencyClass::class => array( - 'concat' => function( ...$parts ) { - return "I'm returning concat of these parts: " . join( ' ', $parts ); - }, - ), - ) - ); - - $expected = "I'm returning concat of these parts: foo bar fizz"; - $result = $this->sut->$method_to_use( DependencyClass::class, 'concat', 'foo', 'bar', 'fizz' ); - $this->assertEquals( $expected, $result ); - } - - /** - * Legacy proxy's 'get_instance_of' can be used from both an injected LegacyProxy and from 'WC()->call_function' - * - * @param string $method_to_use Method in the tested class to use. - * - * @testWith ["get_instance_of_using_injected_proxy"] - * ["get_instance_of_using_woocommerce_class"] - */ - public function test_get_instance_of_can_be_used_via_injected_legacy_proxy_and_woocommerce_object( $method_to_use ) { - $instance = $this->sut->$method_to_use( \WC_Queue_Interface::class, 34 ); - $this->assertInstanceOf( \WC_Action_Queue::class, $instance ); - } - - /** - * Legacy object mocks can be used from both an injected LegacyProxy and from 'WC()->call_function' - * - * @param string $method_to_use Method in the tested class to use. - * - * @testWith ["get_instance_of_using_injected_proxy"] - * ["get_instance_of_using_woocommerce_class"] - */ - public function test_class_mocks_can_be_used_via_injected_legacy_proxy_and_woocommerce_object( $method_to_use ) { - $mock = new \stdClass(); - $this->register_legacy_proxy_class_mocks( array( \WC_Query::class => $mock ) ); - $this->assertSame( $mock, $this->sut->$method_to_use( \WC_Query::class ) ); - } -} diff --git a/tests/php/src/Proxies/ExampleClasses/ClassThatDependsOnLegacyCode.php b/tests/php/src/Proxies/ExampleClasses/ClassThatDependsOnLegacyCode.php deleted file mode 100644 index d9c18a65d6b..00000000000 --- a/tests/php/src/Proxies/ExampleClasses/ClassThatDependsOnLegacyCode.php +++ /dev/null @@ -1,103 +0,0 @@ -legacy_proxy = $legacy_proxy; - } - - /** - * Use proxy's 'call_function' from the injected proxy. - * - * @param string $function Function to call. - * @param mixed ...$parameters Parameters to pass to the function. - * - * @return mixed The result from the function. - */ - public function call_legacy_function_using_injected_proxy( $function, ...$parameters ) { - return $this->legacy_proxy->call_function( $function, ...$parameters ); - } - - /** - * Use proxy's 'call_function' using 'WC()->call_function'. - * - * @param string $function Function to call. - * @param mixed ...$parameters Parameters to pass to the function. - * - * @return mixed The result from the function. - */ - public function call_legacy_function_using_woocommerce_class( $function, ...$parameters ) { - return WC()->call_function( $function, ...$parameters ); - } - - /** - * Use proxy's 'call_static' from the injected proxy. - * - * @param string $class_name Class containing the static method to call. - * @param string $method_name Static method to call. - * @param mixed ...$parameters Parameters to pass to the method. - * - * @return mixed The result from the method. - */ - public function call_static_method_using_injected_proxy( $class_name, $method_name, ...$parameters ) { - return $this->legacy_proxy->call_static( $class_name, $method_name, ...$parameters ); - } - - /** - * Use proxy's 'call_static' using 'WC()->call_function'. - * - * @param string $class_name Class containing the static method to call. - * @param string $method_name Static method to call. - * @param mixed ...$parameters Parameters to pass to the method. - * - * @return mixed The result from the method. - */ - public function call_static_method_using_woocommerce_class( $class_name, $method_name, ...$parameters ) { - return WC()->call_static( $class_name, $method_name, ...$parameters ); - } - - /** - * Use proxy's 'get_instance_of' from the injected proxy. - * - * @param string $class_name The name of the class to get an instance of. - * @param mixed ...$args Extra arguments for 'get_instance_of'. - * - * @return object The instance obtained. - */ - public function get_instance_of_using_injected_proxy( string $class_name, ...$args ) { - return $this->legacy_proxy->get_instance_of( $class_name, ...$args ); - } - - /** - * Use proxy's 'get_instance_of' using 'WC()->call_function'. - * - * @param string $class_name The name of the class to get an instance of. - * @param mixed ...$args Extra arguments for 'get_instance_of'. - * - * @return object The instance obtained. - */ - public function get_instance_of_using_woocommerce_class( string $class_name, ...$args ) { - return WC()->get_instance_of( $class_name, ...$args ); - } -} diff --git a/tests/php/src/Proxies/LegacyProxyTest.php b/tests/php/src/Proxies/LegacyProxyTest.php deleted file mode 100644 index bc90f4f7fc2..00000000000 --- a/tests/php/src/Proxies/LegacyProxyTest.php +++ /dev/null @@ -1,85 +0,0 @@ -sut = new LegacyProxy(); - } - - /** - * @testdox 'get_instance_of' throws an exception when trying to use it to get an instance of a namespaced class. - */ - 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->sut->get_instance_of( DependencyClass::class ); - } - - /** - * @testdox 'get_instance_of' can be used to get an instance of a class by using its constructor and passing constructor arguments. - */ - public function test_get_instance_of_can_be_used_to_get_a_non_namespaced_class_with_constructor_parameters() { - $instance = $this->sut->get_instance_of( \WC_Data_Exception::class, 1234, 'Error!', 432 ); - $this->assertInstanceOf( \WC_Data_Exception::class, $instance ); - $this->assertEquals( 1234, $instance->getErrorCode() ); - $this->assertEquals( 'Error!', $instance->getMessage() ); - $this->assertEquals( 432, $instance->getCode() ); - } - - /** - * @testdox 'get_instance_of' uses the 'instance' static method in classes that implement it, passing the supplied arguments. - */ - public function test_get_instance_of_class_with_instance_method_gets_an_instance_of_the_appropriate_class() { - // ClassWithSingleton is in the root namespace and thus can't be autoloaded. - require_once dirname( __DIR__ ) . '/Internal/DependencyManagement/ExampleClasses/ClassWithSingleton.php'; - - $instance = $this->sut->get_instance_of( \ClassWithSingleton::class, 'foo', 'bar' ); - $this->assertSame( \ClassWithSingleton::$instance, $instance ); - $this->assertEquals( array( 'foo', 'bar' ), \ClassWithSingleton::$instance_args ); - } - - /** - * @testdox 'get_instance_of' can be used to get an instance of a class implementing WC_Queue_Interface. - */ - public function test_get_instance_of_wc_queue_interface_gets_an_instance_of_the_appropriate_class() { - $instance = $this->sut->get_instance_of( \WC_Queue_Interface::class, 34 ); - $this->assertInstanceOf( \WC_Action_Queue::class, $instance ); - } - - /** - * @testdox 'call_function' can be used to invoke any standalone function. - */ - public function test_call_function_can_be_used_to_invoke_functions() { - $result = $this->sut->call_function( 'substr', 'foo bar fizz', 4, 3 ); - $this->assertEquals( 'bar', $result ); - } - - /** - * @testdox 'call_static' can be used to invoke any public static class method. - */ - public function test_call_static_can_be_used_to_invoke_public_static_methods() { - $result = $this->sut->call_static( DependencyClass::class, 'concat', 'foo', 'bar', 'fizz' ); - $this->assertEquals( 'Parts: foo, bar, fizz', $result ); - } -} diff --git a/tests/php/src/Proxies/MockableLegacyProxyTest.php b/tests/php/src/Proxies/MockableLegacyProxyTest.php deleted file mode 100644 index 579f514a7bc..00000000000 --- a/tests/php/src/Proxies/MockableLegacyProxyTest.php +++ /dev/null @@ -1,226 +0,0 @@ -sut = new MockableLegacyProxy(); - } - - /** - * @testdox 'get_instance_of' works as in LegacyProxy if no class mocks are registered. - */ - public function test_get_instance_of_works_as_regular_legacy_proxy_if_no_mock_registered() { - $instance = $this->sut->get_instance_of( \WC_Data_Exception::class, 1234, 'Error!', 432 ); - $this->assertInstanceOf( \WC_Data_Exception::class, $instance ); - $this->assertEquals( 1234, $instance->getErrorCode() ); - $this->assertEquals( 'Error!', $instance->getMessage() ); - $this->assertEquals( 432, $instance->getCode() ); - } - - /** - * The data provider for test_register_class_mocks_throws_if_invalid_parameters_supplied. - * - * @return array[] - */ - public function data_provider_for_test_register_class_mocks_throws_if_invalid_parameters_supplied() { - return array( - array( 1234, new \stdClass() ), - array( 'SomeClassName', 1234 ), - ); - } - - /** - * @testdox 'register_class_mocks' throws an exception if an invalid parameter is supplied (not an array of class name => object or factory callback). - * - * @dataProvider data_provider_for_test_register_class_mocks_throws_if_invalid_parameters_supplied - * - * @param string $class_name The name of the class to mock. - * @param object $mock The mock. - */ - public function test_register_class_mocks_throws_if_invalid_parameters_supplied( $class_name, $mock ) { - $this->expectException( \Exception::class ); - $this->expectExceptionMessage( 'MockableLegacyProxy::register_class_mocks: $mocks must be an associative array of class_name => object or factory callback.' ); - - $this->sut->register_class_mocks( array( $class_name => $mock ) ); - } - - /** - * @testdox 'register_class_mocks' can be used to return class mocks by passing fixed mock instances. - */ - public function test_register_class_mocks_can_be_used_so_that_get_instance_of_returns_a_fixed_instance_mock() { - $mock = new \stdClass(); - $this->sut->register_class_mocks( array( \WC_Query::class => $mock ) ); - $this->assertSame( $mock, $this->sut->get_instance_of( \WC_Query::class ) ); - } - - /** - * @testdox 'register_class_mocks' can be used to return class mocks by passing mock factory callbacks. - */ - public function test_register_class_mocks_can_be_used_so_that_get_instance_of_uses_a_factory_function_to_return_the_instance() { - $mock_factory = function( $code, $message, $http_status_code = 400, $data = array() ) { - return "$code, $message, $http_status_code"; - }; - $this->sut->register_class_mocks( array( \WC_Data_Exception::class => $mock_factory ) ); - $this->assertEquals( '1234, Error!, 432', $this->sut->get_instance_of( \WC_Data_Exception::class, 1234, 'Error!', 432 ) ); - } - - /** - * @testdox 'call_function' works as in LegacyProxy if no function mocks are registered. - */ - public function test_call_function_works_as_regular_legacy_proxy_if_no_mocks_registered() { - $result = $this->sut->call_function( 'substr', 'foo bar fizz', 4, 3 ); - $this->assertEquals( 'bar', $result ); - } - - /** - * The data provider for test_register_function_mocks_throws_if_invalid_parameters_supplied. - * - * @return array[] - */ - public function data_provider_for_test_register_function_mocks_throws_if_invalid_parameters_supplied() { - return array( - array( 1234, function() {} ), - array( 'SomeClassName', 1234 ), - ); - } - - /** - * @testdox 'register_function_mocks' throws an exception if an invalid parameter is supplied (not an array of function name => mock function). - * - * @dataProvider data_provider_for_test_register_function_mocks_throws_if_invalid_parameters_supplied - * - * @param string $function_name The name of the function to mock. - * @param callable $mock The mock. - */ - public function test_register_function_mocks_throws_if_invalid_parameters_supplied( $function_name, $mock ) { - $this->expectException( \Exception::class ); - $this->expectExceptionMessage( 'MockableLegacyProxy::register_function_mocks: The supplied mocks array must have function names as keys and function replacement callbacks as values.' ); - - $this->sut->register_function_mocks( array( $function_name => $mock ) ); - } - - /** - * @testdox 'register_function_mocks' can be used to register mocks for any function. - */ - public function test_register_function_mocks_can_be_used_so_that_call_function_calls_mock_functions() { - $this->sut->register_function_mocks( - array( - 'substr' => function( $string, $start, $length ) { - return "I'm returning substr of '$string' from $start with length $length"; - }, - ) - ); - - $expected = "I'm returning substr of 'foo bar fizz' from 4 with length 3"; - $result = $this->sut->call_function( 'substr', 'foo bar fizz', 4, 3 ); - $this->assertEquals( $expected, $result ); - } - - /** - * @testdox 'call_static' works as in LegacyProxy if no static method mocks are registered. - */ - public function test_call_static_works_as_regular_legacy_proxy_if_no_mocks_registered() { - $result = $this->sut->call_static( DependencyClass::class, 'concat', 'foo', 'bar', 'fizz' ); - $this->assertEquals( 'Parts: foo, bar, fizz', $result ); - } - - /** - * The data provider for test_register_static_mocks_throws_if_invalid_parameters_supplied. - * - * @return array[] - */ - public function data_provider_for_test_register_static_mocks_throws_if_invalid_parameters_supplied() { - return array( - array( 1234, array( 'some_method' => function(){} ) ), - array( 'SomeClassName', 1234 ), - array( 'SomeClassName', array( 1234 => function(){} ) ), - array( 'SomeClassName', array( 'the_method' => 1234 ) ), - ); - } - - /** - * @testdox - * - * @dataProvider data_provider_for_test_register_function_mocks_throws_if_invalid_parameters_supplied - * - * @param string $class_name The name of the class whose static methods we want to mock. - * @param array $mocks The mocks. - */ - public function test_register_static_mocks_throws_if_invalid_parameters_supplied( $class_name, $mocks ) { - $this->expectException( \Exception::class ); - $this->expectExceptionMessage( 'MockableLegacyProxy::register_static_mocks: $mocks must be an associative array of class name => associative array of method name => callable.' ); - - $this->sut->register_static_mocks( array( $class_name => $mocks ) ); - } - - /** - * @testdox 'register_static_mocks' can be used to register mocks for any static method. - */ - public function test_register_static_mocks_can_be_used_so_that_call_function_calls_mock_functions() { - $this->sut->register_static_mocks( - array( - DependencyClass::class => array( - 'concat' => function( ...$parts ) { - return "I'm returning concat of these parts: " . join( ' ', $parts ); - }, - ), - ) - ); - - $expected = "I'm returning concat of these parts: foo bar fizz"; - $result = $this->sut->call_static( DependencyClass::class, 'concat', 'foo', 'bar', 'fizz' ); - $this->assertEquals( $expected, $result ); - } - - /** - * @testdox 'reset' can be used to revert the instance to its original state, in which nothing is mocked. - */ - public function test_reset_can_be_used_to_unregister_all_mocks() { - $this->sut->register_class_mocks( array( \WC_Query::class => new \stdClass() ) ); - - $this->sut->register_function_mocks( - array( - 'substr' => function( $string, $start, $length ) { - return null; - }, - ) - ); - - $this->sut->register_static_mocks( - array( - DependencyClass::class => array( - 'concat' => function( ...$parts ) { - return null; - }, - ), - ) - ); - - $this->sut->reset(); - - $this->test_call_function_works_as_regular_legacy_proxy_if_no_mocks_registered(); - $this->test_call_static_works_as_regular_legacy_proxy_if_no_mocks_registered(); - $this->test_call_static_works_as_regular_legacy_proxy_if_no_mocks_registered(); - } -} diff --git a/tests/php/src/Utilities/StringUtilTest.php b/tests/php/src/Utilities/StringUtilTest.php new file mode 100644 index 00000000000..255c371c247 --- /dev/null +++ b/tests/php/src/Utilities/StringUtilTest.php @@ -0,0 +1,51 @@ +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 ) ); + } +} diff --git a/woocommerce.php b/woocommerce.php index 9ece15531c7..480e8d5f69a 100644 --- a/woocommerce.php +++ b/woocommerce.php @@ -3,7 +3,7 @@ * Plugin Name: WooCommerce * Plugin URI: https://woocommerce.com/ * Description: An eCommerce toolkit that helps you sell anything. Beautifully. - * Version: 4.5.0-dev + * Version: 4.5.0-rc.1 * Author: Automattic * Author URI: https://woocommerce.com * Text Domain: woocommerce