diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 68e4ec5132a..d3f1e2a4f3c 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,143 @@ == Changelog == += 4.4.0 - 2020-08-18 = + +**WooCommerce** +* Accessibility: Adds alt attribute to photoswipe gallery images. #26945 +* Enhancement - Remove the privacy page dropdown from the Accounts & Privacy page. #26809 +* Enhancement - Added automatic language pack updates for WooCommerce.com extensions. #26750 +* Enhancement - Improvements for the Hungarian address format. #26697 +* Enhancement - Dropdown arrow width was made smaller. #26202 +* Enhancement - Add a "No change" option to the "Stock status" selector in quick edit, preselect it when the product being edited is a variable product. #26174 +* Enhancement - Don't request language packs for empty locales list. #27148 +* Localization - Added 14 Namibia regions. #26894 +* Localization - Change default Greek states names to English. #26719 +* Localization - Improved Puerto Rico addresses and improve address formatting. #26698 +* Localization - Wrapped price and currency inside a BDI tag, in order to prevent the bidirectional algorithm to produce confusing results. #26462 +* Localization - Added Algerian provinces. #25687 +* Tweak - Added "order_total" to the wcadmin_orders_edit_status_change tracker event. #26935 +* Tweak - Fixed WooCommerce menu for users that can only manage orders on WooCommerce. #26877 +* Tweak - Limit nocache headers to googleweblight by default. #26858 +* Tweak - Preserve quantity input value when changing variations. #26805 +* Tweak - Confirm before running any tool from the WooCommerce Status settings. #26660 +* Tweak - Limit stock changes for order items to status methods for consistency. #26642 +* Tweak - Custom vendor taxonomy update messages. #26634 +* Tweak - Remove HTML tags from plain text email template for Customer new account. #26613 +* Tweak - Conditionally change the text in My account to reflect if shipping is disabled. #26325 +* Tweak - Show CSV file name in result message when product import is complete. #25240 +* Tweak - Improve order details UI to highlight "Paid" and "Net Payment" sections. #27142 +* Fix - Remove the dot after the generated password in new account emails. #27073 +* Fix - Delayed the execution of all webhooks until after the request has completed. #27067 +* Fix - [Importer/Exporter] Fixed the value display of "Published" for children of draft variable products. #27046 +* Fix - Removed the extra id parameter added to CLI commands that shouldn't have one. #27017 +* Fix - Added the missing instance_id to the REST CLI command so that shipping zone method commands will work. #27017 +* Fix - Add rating_count to order by rating clause. #26964 +* Fix - Don't show premium support forum link if the store is not connected to WooCommerce.com. #26932 +* Fix - Incorrect capability used on add order note while creating an user note. #26920 +* Fix - Preserve HTML entities from product names in the cart page. #26885 +* Fix - Display warning hen leaving settings page without saving first. #26880 +* Fix - Remove wc_round_tax_total from shipping tax because shipping prices never include tax so rounding down is not needed. #26850 +* Fix - Make the "Please log in" message displayed to users with an existing account a hyperlink. #26837 +* Fix - Typo in composer.json for makepot. #26829 +* Fix - Layout issue on the checkout page when switching countries. #26697 +* Fix - Missing closing select tag to the product exporter category select. #26680 +* Fix - Possible PHP undefined index notice before WooCommerce has been configured. #26658 +* Fix - A deferred product sync is now scheduled when a product having a parent (e.g. a variation product) is deleted, not only when it's saved. #26629 +* Fix - Stock status of variable products that handle stock at the main product level is now appropriately updated when the product is saved. #26611 +* Fix - Discounted prices are no longer underlined in Twenty Twenty. #26609 +* Fix - Email link color clash. #26591 +* Fix - Remove HTML from error message. #26589 +* Fix - Fixed Tooltip flashing. #26558 +* Fix - Correctly displays the instructional option as default in the select box for picking a Country / Region on the checkout page. #26554 +* Fix - Default option "Select a country..." will now display accurately on Country select box in Cart shipping calculator. #26541 +* Fix - Fixed user capability required to view the order count indicator. #26338 +* Fix - The filtering widget now works as expected with variable products, displaying those products for which visible variations are available. #26260 +* Fix - Added a z-index to the remove button (x) to set the z-order of the element. #26202 +* Fix - Don't change the stock status of variations when bulk editing a variable product and leaving the "Stock status" selector as "No change". #26174 +* Fix - Remove new WP 5.5 meta box arrows from "Order data" and "Order items" meta boxes. #27173 +* 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 +* 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 +* Dev - Allow extend BACS accounts filter with order ID. #26961 +* Dev - Add npm run build:packages to npm run build. #26906 +* Dev - Add woocommerce_order_note_added action. #26846 +* Dev - Add tests for template cache. #26840 +* Dev - Add filter to allow disabling nocache headers. #26802 +* Dev - Introduce a dependency injection framework for the code in the src directory. #26731 +* Dev - Normalized parameters of woocommerce_product_importer_parsed_data filter. #26669 +* Dev - Introduced new WC_Product_CSV_Importer::get_formatting_callback() fixing a typo in the method name. #26668 +* Dev - Allow set "date_created" while creating orders via CRUD. #26567 +* Dev - Allow set a custom as order key using wc_generate_order_key(). #26566 +* Dev - Allow set order_key while creating an order via CRUD. #26565 +* Dev - Introduced woocommerce_product_cross_sells_products_heading filter. #26545 +* Dev - Added the removed_coupon_in_checkout event that is triggered on the Checkout page after a coupon is removed using .woocommerce-remove-coupon button. #26536 +* Dev - Remove no longer used styles from TwentyTwenty. #26516 +* Dev - Fix error message in wc_get_template() function. #26515 +* Dev - Add npm publish script for @woocommerce/e2e-environment. #26432 +* Dev - Make WC_Cart::display_prices_including_tax aware of tax display changes. #26400 +* Dev - Deprecated WC_Legacy_Cart::tax_display_cart in favor of WC_Cart:: get_tax_price_display_mode(). #26400 +* Dev - Add an optional $render_variations argument to in WC_Product_Variable::get_available_variation() in order to allow plugins to avoid performance bottlenecks. #26303 +* 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 + +**REST API 1.0.11** +* 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 +* 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 + +**WooCommerce Admin 1.4.0** +* Enhancement - Move the WooCommerce > Coupons dashboard menu item to Marketing > Coupons. #4786 +* Fix - Installation of child theme zip files from the store setup wizard. #4852 +* Fix - Center the skip link on the theme selection step. #4847 +* Fix - Removed item "profiler" from the menu. #4851 +* Fix - PHP notices when hosts block certain WP scripts. #4856 +* Fix - Remove new WP 5.5 meta box arrows in the shipping banner. #4914 +* Fix - Allow revisiting of the payments task. #4918 +* Fix - Use of Jetpack autoloader. #4920 +* Fix - Only show WCPay task in US based stores. #4899 +* 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 - Change account required text on biz step in onboarding wizard. #4909 +* Dev - Add the experimental resolver to WCA data package. #4862 +* Dev - Fix linter errors. #4904 + +**WooCommerce Blocks 3.0.0** +* Build - Updated the automattic/jetpack-autoloader package to the 2.0 branch. #2847 +* Enhancement - Add support for the Bank Transfer (BACS) payment method in the Checkout block. #2821 +* Enhancement - Several improvements to make Credit Card input fields display more consistent across different themes and viewport sizes. #2869 +* Enhancement - Cart and Checkout blocks show a notification for products on backorder. #2833 +* Enhancement - Chip styles of the Filter Products by Attribute and Active Filters have been updated to give a more consistent experience. #2765 +* Enhancement - Add protection for rogue filters on order queries when executing cleanup draft orders logic. #2874 +* Enhancement - Extend payment gateway extension API so gateways (payment methods) can dynamically disable (hide), based on checkout or order data (such as cart items or shipping method). For example, Cash on Delivery can limit availability to specific shipping methods only. #2840 [DN] +* Enhancement - Support Cash on Delivery core payment gateway in the Checkout block. #2831 +* Performance - Don't load shortcode Cart and Checkout scripts when using the blocks. #2842 +* Performance - Scripts only relevant to the frontend side of blocks are no longer loaded in the editor. #2788 +* Performance - Lazy Loading Atomic Components. #2777 +* Pefactor - Remove dashicon classes. #2848 + +**WooCommerce Blocks 3.1.0** +* Fix - Missing permissions_callback arg in StoreApi route definitions. #2926 +* 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.1 - 2020-07-21 = + +**WooCommerce Admin 1.3.1** +* Fix - PHP Fatal errors when columns are missing from the Notes table. #4831 + +**WooCommerce Blocks 2.7.2** +* Enhancement - Move Draft order logic behind feature flag. #2874 + = 4.3.0 - 2020-07-08 = **WooCommerce** diff --git a/composer.json b/composer.json index 49987d76e3f..05fa883b432 100644 --- a/composer.json +++ b/composer.json @@ -8,16 +8,15 @@ "minimum-stability": "dev", "require": { "php": ">=7.0", - "automattic/jetpack-autoloader": "^2.0.2", - "automattic/jetpack-constants": "^1.1", + "automattic/jetpack-autoloader": "2.0.2", + "automattic/jetpack-constants": "1.4.0", "composer/installers": "1.7.0", - "league/container": "^3.3", + "league/container": "3.3.1", "maxmind-db/reader": "1.6.0", - "pelago/emogrifier": "^3.1", + "pelago/emogrifier": "3.1.0", "woocommerce/action-scheduler": "3.1.6", - "woocommerce/woocommerce-admin": "1.4.0-beta.2", - "woocommerce/woocommerce-blocks": "3.1.0", - "woocommerce/woocommerce-rest-api": "1.0.11" + "woocommerce/woocommerce-admin": "1.4.0-beta.3", + "woocommerce/woocommerce-blocks": "3.1.0" }, "require-dev": { "phpunit/phpunit": "7.5.20", @@ -40,6 +39,9 @@ "includes/legacy", "includes/libraries" ], + "classmap": [ + "includes/rest-api" + ], "psr-4": { "Automattic\\WooCommerce\\": "src/" } @@ -48,7 +50,10 @@ "psr-4": { "Automattic\\WooCommerce\\Tests\\": "tests/php/src/", "Automattic\\WooCommerce\\Testing\\Tools\\": "tests/Tools/" - } + }, + "classmap": [ + "tests/legacy/unit-tests/rest-api/Helpers" + ] }, "scripts": { "post-install-cmd": [ diff --git a/composer.lock b/composer.lock index 36692446ad1..9c95657f4c1 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": "26aef05efb985ef742260c80f4b7bca0", + "content-hash": "ae4abaa8d39e860cc6c379cb5f6a0c2f", "packages": [ { "name": "automattic/jetpack-autoloader", - "version": "v2.1.0", + "version": "v2.0.2", "source": { "type": "git", "url": "https://github.com/Automattic/jetpack-autoloader.git", - "reference": "802517b3ff3010de89141d9f7c4d56aec1d21527" + "reference": "4502da4b2443fc1b61389cacc94c34876aca2b3d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Automattic/jetpack-autoloader/zipball/802517b3ff3010de89141d9f7c4d56aec1d21527", - "reference": "802517b3ff3010de89141d9f7c4d56aec1d21527", + "url": "https://api.github.com/repos/Automattic/jetpack-autoloader/zipball/4502da4b2443fc1b61389cacc94c34876aca2b3d", + "reference": "4502da4b2443fc1b61389cacc94c34876aca2b3d", "shasum": "" }, "require": { @@ -40,7 +40,7 @@ "GPL-2.0-or-later" ], "description": "Creates a custom autoloader for a plugin or theme.", - "time": "2020-07-27T20:37:00+00:00" + "time": "2020-07-09T13:18:38+00:00" }, { "name": "automattic/jetpack-constants", @@ -554,16 +554,16 @@ }, { "name": "woocommerce/woocommerce-admin", - "version": "v1.4.0-beta.2", + "version": "v1.4.0-beta.3", "source": { "type": "git", "url": "https://github.com/woocommerce/woocommerce-admin.git", - "reference": "d56ac35bbb62bda2c981443932e7f90b0f6dbe99" + "reference": "df2af46a8552cdee15df0030fccbe4cd5a6d270d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/woocommerce/woocommerce-admin/zipball/d56ac35bbb62bda2c981443932e7f90b0f6dbe99", - "reference": "d56ac35bbb62bda2c981443932e7f90b0f6dbe99", + "url": "https://api.github.com/repos/woocommerce/woocommerce-admin/zipball/df2af46a8552cdee15df0030fccbe4cd5a6d270d", + "reference": "df2af46a8552cdee15df0030fccbe4cd5a6d270d", "shasum": "" }, "require": { @@ -597,7 +597,7 @@ ], "description": "A modern, javascript-driven WooCommerce Admin experience.", "homepage": "https://github.com/woocommerce/woocommerce-admin", - "time": "2020-07-28T00:28:40+00:00" + "time": "2020-08-04T02:21:47+00:00" }, { "name": "woocommerce/woocommerce-blocks", @@ -645,67 +645,27 @@ "woocommerce" ], "time": "2020-07-29T15:45:19+00:00" - }, - { - "name": "woocommerce/woocommerce-rest-api", - "version": "1.0.11", - "source": { - "type": "git", - "url": "https://github.com/woocommerce/woocommerce-rest-api.git", - "reference": "304bb95cb4b95f182f09d56153d5ac254d5fe60a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/woocommerce/woocommerce-rest-api/zipball/304bb95cb4b95f182f09d56153d5ac254d5fe60a", - "reference": "304bb95cb4b95f182f09d56153d5ac254d5fe60a", - "shasum": "" - }, - "require": { - "automattic/jetpack-autoloader": "^2.0.0" - }, - "require-dev": { - "phpunit/phpunit": "6.5.14", - "woocommerce/woocommerce-sniffs": "0.0.9" - }, - "type": "wordpress-plugin", - "autoload": { - "classmap": [ - "src/Controllers/Version1", - "src/Controllers/Version2", - "src/Controllers/Version3" - ], - "psr-4": { - "Automattic\\WooCommerce\\RestApi\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "GPL-3.0-or-later" - ], - "description": "The WooCommerce core REST API.", - "homepage": "https://github.com/woocommerce/woocommerce-rest-api", - "time": "2020-07-24T13:38:16+00:00" } ], "packages-dev": [ { "name": "dealerdirect/phpcodesniffer-composer-installer", - "version": "v0.7.0", + "version": "v0.6.2", "source": { "type": "git", "url": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer.git", - "reference": "e8d808670b8f882188368faaf1144448c169c0b7" + "reference": "8001af8eb107fbfcedc31a8b51e20b07d85b457a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Dealerdirect/phpcodesniffer-composer-installer/zipball/e8d808670b8f882188368faaf1144448c169c0b7", - "reference": "e8d808670b8f882188368faaf1144448c169c0b7", + "url": "https://api.github.com/repos/Dealerdirect/phpcodesniffer-composer-installer/zipball/8001af8eb107fbfcedc31a8b51e20b07d85b457a", + "reference": "8001af8eb107fbfcedc31a8b51e20b07d85b457a", "shasum": "" }, "require": { - "composer-plugin-api": "^1.0 || ^2.0", - "php": ">=5.3", - "squizlabs/php_codesniffer": "^2 || ^3 || 4.0.x-dev" + "composer-plugin-api": "^1.0", + "php": "^5.3|^7", + "squizlabs/php_codesniffer": "^2|^3" }, "require-dev": { "composer/composer": "*", @@ -752,7 +712,7 @@ "stylecheck", "tests" ], - "time": "2020-06-25T14:57:39+00:00" + "time": "2020-01-29T20:22:20+00:00" }, { "name": "doctrine/instantiator", @@ -2690,20 +2650,6 @@ "polyfill", "portable" ], - "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-07-14T12:35:20+00:00" }, { @@ -2797,23 +2743,23 @@ }, { "name": "woocommerce/woocommerce-sniffs", - "version": "0.1.0", + "version": "0.0.10", "source": { "type": "git", "url": "https://github.com/woocommerce/woocommerce-sniffs.git", - "reference": "b72b7dd2e70aa6aed16f80cdae5b1e6cce2e4c79" + "reference": "b0e3d69a53b3ffdbb97a0371bd1b43aa17092d65" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/woocommerce/woocommerce-sniffs/zipball/b72b7dd2e70aa6aed16f80cdae5b1e6cce2e4c79", - "reference": "b72b7dd2e70aa6aed16f80cdae5b1e6cce2e4c79", + "url": "https://api.github.com/repos/woocommerce/woocommerce-sniffs/zipball/b0e3d69a53b3ffdbb97a0371bd1b43aa17092d65", + "reference": "b0e3d69a53b3ffdbb97a0371bd1b43aa17092d65", "shasum": "" }, "require": { - "dealerdirect/phpcodesniffer-composer-installer": "0.7.0", + "dealerdirect/phpcodesniffer-composer-installer": "0.6.2", "php": ">=7.0", "phpcompatibility/phpcompatibility-wp": "2.1.0", - "wp-coding-standards/wpcs": "2.3.0" + "wp-coding-standards/wpcs": "2.2.1" }, "type": "phpcodesniffer-standard", "notification-url": "https://packagist.org/downloads/", @@ -2833,7 +2779,7 @@ "woocommerce", "wordpress" ], - "time": "2020-08-06T18:23:45+00:00" + "time": "2020-04-07T20:25:44+00:00" }, { "name": "wp-cli/i18n-command", @@ -3054,16 +3000,16 @@ }, { "name": "wp-coding-standards/wpcs", - "version": "2.3.0", + "version": "2.2.1", "source": { "type": "git", "url": "https://github.com/WordPress/WordPress-Coding-Standards.git", - "reference": "7da1894633f168fe244afc6de00d141f27517b62" + "reference": "b5a453203114cc2284b1a614c4953456fbe4f546" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/7da1894633f168fe244afc6de00d141f27517b62", - "reference": "7da1894633f168fe244afc6de00d141f27517b62", + "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/b5a453203114cc2284b1a614c4953456fbe4f546", + "reference": "b5a453203114cc2284b1a614c4953456fbe4f546", "shasum": "" }, "require": { @@ -3073,7 +3019,6 @@ "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": { @@ -3096,7 +3041,7 @@ "standards", "wordpress" ], - "time": "2020-05-13T23:57:56+00:00" + "time": "2020-02-04T02:52:06+00:00" } ], "aliases": [], diff --git a/i18n/continents.php b/i18n/continents.php index da519983b17..13f743c9c68 100644 --- a/i18n/continents.php +++ b/i18n/continents.php @@ -4,7 +4,7 @@ * * Returns an array of continents. * - * @package WooCommerce/i18n + * @package WooCommerce\i18n * @version 2.5.0 */ diff --git a/i18n/countries.php b/i18n/countries.php index 9d2d517fb7f..dda20ec5880 100644 --- a/i18n/countries.php +++ b/i18n/countries.php @@ -13,7 +13,7 @@ defined( 'ABSPATH' ) || exit; return array( 'AF' => __( 'Afghanistan', 'woocommerce' ), - 'AX' => __( 'Åland Islands', 'woocommerce' ), + 'AX' => __( 'Åland Islands', 'woocommerce' ), 'AL' => __( 'Albania', 'woocommerce' ), 'DZ' => __( 'Algeria', 'woocommerce' ), 'AS' => __( 'American Samoa', 'woocommerce' ), diff --git a/i18n/locale-info.php b/i18n/locale-info.php index 08866ca44b1..458b17f95d8 100644 --- a/i18n/locale-info.php +++ b/i18n/locale-info.php @@ -2,13 +2,22 @@ /** * Locales information * - * @package WooCommerce/i18n + * @package WooCommerce\i18n * @version 3.5.0 */ defined( 'ABSPATH' ) || exit; return array( + 'AT' => array( + 'currency_code' => 'EUR', + 'currency_pos' => 'left', + 'thousand_sep' => '.', + 'decimal_sep' => ',', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + ), 'AU' => array( 'currency_code' => 'AUD', 'currency_pos' => 'left', @@ -54,6 +63,15 @@ return array( 'weight_unit' => 'kg', 'dimension_unit' => 'cm', ), + 'CH' => array( + 'currency_code' => 'CHF', + 'currency_pos' => 'left_space', + 'thousand_sep' => "'", + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + ), 'DE' => array( 'currency_code' => 'EUR', 'currency_pos' => 'left', @@ -135,6 +153,15 @@ return array( 'weight_unit' => 'kg', 'dimension_unit' => 'cm', ), + 'LI' => array( + 'currency_code' => 'CHF', + 'currency_pos' => 'left_space', + 'thousand_sep' => "'", + 'decimal_sep' => '.', + 'num_decimals' => 2, + 'weight_unit' => 'kg', + 'dimension_unit' => 'cm', + ), 'MD' => array( 'currency_code' => 'MDL', 'currency_pos' => 'right_space', diff --git a/i18n/phone.php b/i18n/phone.php index 3f118c971ea..4856e7421b7 100644 --- a/i18n/phone.php +++ b/i18n/phone.php @@ -4,7 +4,7 @@ * * Returns an array of calling codes. * - * @package WooCommerce/i18n + * @package WooCommerce\i18n */ defined( 'ABSPATH' ) || exit; diff --git a/i18n/states.php b/i18n/states.php index a4a0ca5ccfc..78441bd3dfb 100644 --- a/i18n/states.php +++ b/i18n/states.php @@ -36,22 +36,22 @@ return array( 'ZAI' => __( 'Zaire', 'woocommerce' ), ), 'AR' => array( // Argentinian provinces. - 'C' => __( 'Ciudad Autónoma de Buenos Aires', 'woocommerce' ), + 'C' => __( 'Ciudad Autónoma de Buenos Aires', 'woocommerce' ), 'B' => __( 'Buenos Aires', 'woocommerce' ), 'K' => __( 'Catamarca', 'woocommerce' ), 'H' => __( 'Chaco', 'woocommerce' ), 'U' => __( 'Chubut', 'woocommerce' ), - 'X' => __( 'Córdoba', 'woocommerce' ), + 'X' => __( 'Córdoba', 'woocommerce' ), 'W' => __( 'Corrientes', 'woocommerce' ), - 'E' => __( 'Entre Ríos', 'woocommerce' ), + 'E' => __( 'Entre Ríos', 'woocommerce' ), 'P' => __( 'Formosa', 'woocommerce' ), 'Y' => __( 'Jujuy', 'woocommerce' ), 'L' => __( 'La Pampa', 'woocommerce' ), 'F' => __( 'La Rioja', 'woocommerce' ), 'M' => __( 'Mendoza', 'woocommerce' ), 'N' => __( 'Misiones', 'woocommerce' ), - 'Q' => __( 'Neuquén', 'woocommerce' ), - 'R' => __( 'Río Negro', 'woocommerce' ), + 'Q' => __( 'Neuquén', 'woocommerce' ), + 'R' => __( 'Río Negro', 'woocommerce' ), 'A' => __( 'Salta', 'woocommerce' ), 'J' => __( 'San Juan', 'woocommerce' ), 'D' => __( 'San Luis', 'woocommerce' ), @@ -59,7 +59,7 @@ return array( 'S' => __( 'Santa Fe', 'woocommerce' ), 'G' => __( 'Santiago del Estero', 'woocommerce' ), 'V' => __( 'Tierra del Fuego', 'woocommerce' ), - 'T' => __( 'Tucumán', 'woocommerce' ), + 'T' => __( 'Tucumán', 'woocommerce' ), ), 'AT' => array(), 'AU' => array( // Australian states. @@ -186,29 +186,29 @@ return array( 'BR' => array( // Brazillian states. 'AC' => __( 'Acre', 'woocommerce' ), 'AL' => __( 'Alagoas', 'woocommerce' ), - 'AP' => __( 'Amapá', 'woocommerce' ), + 'AP' => __( 'Amapá', 'woocommerce' ), 'AM' => __( 'Amazonas', 'woocommerce' ), 'BA' => __( 'Bahia', 'woocommerce' ), - 'CE' => __( 'Ceará', 'woocommerce' ), + 'CE' => __( 'Ceará', 'woocommerce' ), 'DF' => __( 'Distrito Federal', 'woocommerce' ), - 'ES' => __( 'Espírito Santo', 'woocommerce' ), - 'GO' => __( 'Goiás', 'woocommerce' ), - 'MA' => __( 'Maranhão', 'woocommerce' ), + 'ES' => __( 'Espírito Santo', 'woocommerce' ), + 'GO' => __( 'Goiás', 'woocommerce' ), + 'MA' => __( 'Maranhão', 'woocommerce' ), 'MT' => __( 'Mato Grosso', 'woocommerce' ), 'MS' => __( 'Mato Grosso do Sul', 'woocommerce' ), 'MG' => __( 'Minas Gerais', 'woocommerce' ), - 'PA' => __( 'Pará', 'woocommerce' ), - 'PB' => __( 'Paraíba', 'woocommerce' ), - 'PR' => __( 'Paraná', 'woocommerce' ), + 'PA' => __( 'Pará', 'woocommerce' ), + 'PB' => __( 'Paraíba', 'woocommerce' ), + 'PR' => __( 'Paraná', 'woocommerce' ), 'PE' => __( 'Pernambuco', 'woocommerce' ), - 'PI' => __( 'Piauí', 'woocommerce' ), + 'PI' => __( 'Piauí', 'woocommerce' ), 'RJ' => __( 'Rio de Janeiro', 'woocommerce' ), 'RN' => __( 'Rio Grande do Norte', 'woocommerce' ), 'RS' => __( 'Rio Grande do Sul', 'woocommerce' ), - 'RO' => __( 'Rondônia', 'woocommerce' ), + 'RO' => __( 'Rondônia', 'woocommerce' ), 'RR' => __( 'Roraima', 'woocommerce' ), 'SC' => __( 'Santa Catarina', 'woocommerce' ), - 'SP' => __( 'São Paulo', 'woocommerce' ), + 'SP' => __( 'São Paulo', 'woocommerce' ), 'SE' => __( 'Sergipe', 'woocommerce' ), 'TO' => __( 'Tocantins', 'woocommerce' ), ), @@ -237,10 +237,10 @@ return array( 'FR' => __( 'Fribourg', 'woocommerce' ), 'GE' => __( 'Geneva', 'woocommerce' ), 'GL' => __( 'Glarus', 'woocommerce' ), - 'GR' => __( 'Graubünden', 'woocommerce' ), + 'GR' => __( 'Graubünden', 'woocommerce' ), 'JU' => __( 'Jura', 'woocommerce' ), 'LU' => __( 'Luzern', 'woocommerce' ), - 'NE' => __( 'Neuchâtel', 'woocommerce' ), + 'NE' => __( 'Neuchâtel', 'woocommerce' ), 'NW' => __( 'Nidwalden', 'woocommerce' ), 'OW' => __( 'Obwalden', 'woocommerce' ), 'SH' => __( 'Schaffhausen', 'woocommerce' ), @@ -253,41 +253,41 @@ return array( 'VS' => __( 'Valais', 'woocommerce' ), 'VD' => __( 'Vaud', 'woocommerce' ), 'ZG' => __( 'Zug', 'woocommerce' ), - 'ZH' => __( 'Zürich', 'woocommerce' ), + 'ZH' => __( 'Zürich', 'woocommerce' ), ), 'CN' => array( // Chinese states. - 'CN1' => __( 'Yunnan / 云南', 'woocommerce' ), - 'CN2' => __( 'Beijing / 北京', 'woocommerce' ), - 'CN3' => __( 'Tianjin / 天津', 'woocommerce' ), - 'CN4' => __( 'Hebei / 河北', 'woocommerce' ), - 'CN5' => __( 'Shanxi / 山西', 'woocommerce' ), - 'CN6' => __( 'Inner Mongolia / 內蒙古', 'woocommerce' ), - 'CN7' => __( 'Liaoning / 辽宁', 'woocommerce' ), - 'CN8' => __( 'Jilin / 吉林', 'woocommerce' ), - 'CN9' => __( 'Heilongjiang / 黑龙江', 'woocommerce' ), - 'CN10' => __( 'Shanghai / 上海', 'woocommerce' ), - 'CN11' => __( 'Jiangsu / 江苏', 'woocommerce' ), - 'CN12' => __( 'Zhejiang / 浙江', 'woocommerce' ), - 'CN13' => __( 'Anhui / 安徽', 'woocommerce' ), - 'CN14' => __( 'Fujian / 福建', 'woocommerce' ), - 'CN15' => __( 'Jiangxi / 江西', 'woocommerce' ), - 'CN16' => __( 'Shandong / 山东', 'woocommerce' ), - 'CN17' => __( 'Henan / 河南', 'woocommerce' ), - 'CN18' => __( 'Hubei / 湖北', 'woocommerce' ), - 'CN19' => __( 'Hunan / 湖南', 'woocommerce' ), - 'CN20' => __( 'Guangdong / 广东', 'woocommerce' ), - 'CN21' => __( 'Guangxi Zhuang / 广西壮族', 'woocommerce' ), - 'CN22' => __( 'Hainan / 海南', 'woocommerce' ), - 'CN23' => __( 'Chongqing / 重庆', 'woocommerce' ), - 'CN24' => __( 'Sichuan / 四川', 'woocommerce' ), - 'CN25' => __( 'Guizhou / 贵州', 'woocommerce' ), - 'CN26' => __( 'Shaanxi / 陕西', 'woocommerce' ), - 'CN27' => __( 'Gansu / 甘肃', 'woocommerce' ), - 'CN28' => __( 'Qinghai / 青海', 'woocommerce' ), - 'CN29' => __( 'Ningxia Hui / 宁夏', 'woocommerce' ), - 'CN30' => __( 'Macao / 澳门', 'woocommerce' ), - 'CN31' => __( 'Tibet / 西藏', 'woocommerce' ), - 'CN32' => __( 'Xinjiang / 新疆', 'woocommerce' ), + 'CN1' => __( 'Yunnan / 云南', 'woocommerce' ), + 'CN2' => __( 'Beijing / 北京', 'woocommerce' ), + 'CN3' => __( 'Tianjin / 天津', 'woocommerce' ), + 'CN4' => __( 'Hebei / 河北', 'woocommerce' ), + 'CN5' => __( 'Shanxi / 山西', 'woocommerce' ), + 'CN6' => __( 'Inner Mongolia / 內蒙古', 'woocommerce' ), + 'CN7' => __( 'Liaoning / 辽宁', 'woocommerce' ), + 'CN8' => __( 'Jilin / 吉林', 'woocommerce' ), + 'CN9' => __( 'Heilongjiang / 黑龙江', 'woocommerce' ), + 'CN10' => __( 'Shanghai / 上海', 'woocommerce' ), + 'CN11' => __( 'Jiangsu / 江苏', 'woocommerce' ), + 'CN12' => __( 'Zhejiang / 浙江', 'woocommerce' ), + 'CN13' => __( 'Anhui / 安徽', 'woocommerce' ), + 'CN14' => __( 'Fujian / 福建', 'woocommerce' ), + 'CN15' => __( 'Jiangxi / 江西', 'woocommerce' ), + 'CN16' => __( 'Shandong / 山东', 'woocommerce' ), + 'CN17' => __( 'Henan / 河南', 'woocommerce' ), + 'CN18' => __( 'Hubei / 湖北', 'woocommerce' ), + 'CN19' => __( 'Hunan / 湖南', 'woocommerce' ), + 'CN20' => __( 'Guangdong / 广东', 'woocommerce' ), + 'CN21' => __( 'Guangxi Zhuang / 广西壮族', 'woocommerce' ), + 'CN22' => __( 'Hainan / 海南', 'woocommerce' ), + 'CN23' => __( 'Chongqing / 重庆', 'woocommerce' ), + 'CN24' => __( 'Sichuan / 四川', 'woocommerce' ), + 'CN25' => __( 'Guizhou / 贵州', 'woocommerce' ), + 'CN26' => __( 'Shaanxi / 陕西', 'woocommerce' ), + 'CN27' => __( 'Gansu / 甘肃', 'woocommerce' ), + 'CN28' => __( 'Qinghai / 青海', 'woocommerce' ), + 'CN29' => __( 'Ningxia Hui / 宁夏', 'woocommerce' ), + 'CN30' => __( 'Macao / 澳门', 'woocommerce' ), + 'CN31' => __( 'Tibet / 西藏', 'woocommerce' ), + 'CN32' => __( 'Xinjiang / 新疆', 'woocommerce' ), ), 'CZ' => array(), 'DE' => array(), @@ -298,36 +298,36 @@ return array( 'DZ-03' => __( 'Laghouat', 'woocommerce' ), 'DZ-04' => __( 'Oum El Bouaghi', 'woocommerce' ), 'DZ-05' => __( 'Batna', 'woocommerce' ), - 'DZ-06' => __( 'Béjaïa', 'woocommerce' ), + 'DZ-06' => __( 'Béjaïa', 'woocommerce' ), 'DZ-07' => __( 'Biskra', 'woocommerce' ), - 'DZ-08' => __( 'Béchar', 'woocommerce' ), + 'DZ-08' => __( 'Béchar', 'woocommerce' ), 'DZ-09' => __( 'Blida', 'woocommerce' ), 'DZ-10' => __( 'Bouira', 'woocommerce' ), 'DZ-11' => __( 'Tamanghasset', 'woocommerce' ), - 'DZ-12' => __( 'Tébessa', 'woocommerce' ), + 'DZ-12' => __( 'Tébessa', 'woocommerce' ), 'DZ-13' => __( 'Tlemcen', 'woocommerce' ), 'DZ-14' => __( 'Tiaret', 'woocommerce' ), 'DZ-15' => __( 'Tizi Ouzou', 'woocommerce' ), 'DZ-16' => __( 'Algiers', 'woocommerce' ), 'DZ-17' => __( 'Djelfa', 'woocommerce' ), 'DZ-18' => __( 'Jijel', 'woocommerce' ), - 'DZ-19' => __( 'Sétif', 'woocommerce' ), - 'DZ-20' => __( 'Saïda', 'woocommerce' ), + 'DZ-19' => __( 'Sétif', 'woocommerce' ), + 'DZ-20' => __( 'Saïda', 'woocommerce' ), 'DZ-21' => __( 'Skikda', 'woocommerce' ), - 'DZ-22' => __( 'Sidi Bel Abbès', 'woocommerce' ), + 'DZ-22' => __( 'Sidi Bel Abbès', 'woocommerce' ), 'DZ-23' => __( 'Annaba', 'woocommerce' ), 'DZ-24' => __( 'Guelma', 'woocommerce' ), 'DZ-25' => __( 'Constantine', 'woocommerce' ), - 'DZ-26' => __( 'Médéa', 'woocommerce' ), + 'DZ-26' => __( 'Médéa', 'woocommerce' ), 'DZ-27' => __( 'Mostaganem', 'woocommerce' ), - 'DZ-28' => __( 'M’Sila', 'woocommerce' ), + 'DZ-28' => __( 'M’Sila', 'woocommerce' ), 'DZ-29' => __( 'Mascara', 'woocommerce' ), 'DZ-30' => __( 'Ouargla', 'woocommerce' ), 'DZ-31' => __( 'Oran', 'woocommerce' ), 'DZ-32' => __( 'El Bayadh', 'woocommerce' ), 'DZ-33' => __( 'Illizi', 'woocommerce' ), - 'DZ-34' => __( 'Bordj Bou Arréridj', 'woocommerce' ), - 'DZ-35' => __( 'Boumerdès', 'woocommerce' ), + 'DZ-34' => __( 'Bordj Bou Arréridj', 'woocommerce' ), + 'DZ-35' => __( 'Boumerdès', 'woocommerce' ), 'DZ-36' => __( 'El Tarf', 'woocommerce' ), 'DZ-37' => __( 'Tindouf', 'woocommerce' ), 'DZ-38' => __( 'Tissemsilt', 'woocommerce' ), @@ -336,32 +336,32 @@ return array( 'DZ-41' => __( 'Souk Ahras', 'woocommerce' ), 'DZ-42' => __( 'Tipasa', 'woocommerce' ), 'DZ-43' => __( 'Mila', 'woocommerce' ), - 'DZ-44' => __( 'Aïn Defla', 'woocommerce' ), + 'DZ-44' => __( 'Aïn Defla', 'woocommerce' ), 'DZ-45' => __( 'Naama', 'woocommerce' ), - 'DZ-46' => __( 'Aïn Témouchent', 'woocommerce' ), - 'DZ-47' => __( 'Ghardaïa', 'woocommerce' ), + 'DZ-46' => __( 'Aïn Témouchent', 'woocommerce' ), + 'DZ-47' => __( 'Ghardaïa', 'woocommerce' ), 'DZ-48' => __( 'Relizane', 'woocommerce' ), ), 'EE' => array(), 'ES' => array( // Spanish states. - 'C' => __( 'A Coruña', 'woocommerce' ), - 'VI' => __( 'Araba/Álava', 'woocommerce' ), + 'C' => __( 'A Coruña', 'woocommerce' ), + 'VI' => __( 'Araba/Álava', 'woocommerce' ), 'AB' => __( 'Albacete', 'woocommerce' ), 'A' => __( 'Alicante', 'woocommerce' ), - 'AL' => __( 'Almería', 'woocommerce' ), + 'AL' => __( 'Almería', 'woocommerce' ), 'O' => __( 'Asturias', 'woocommerce' ), - 'AV' => __( 'Ávila', 'woocommerce' ), + 'AV' => __( 'Ávila', 'woocommerce' ), 'BA' => __( 'Badajoz', 'woocommerce' ), 'PM' => __( 'Baleares', 'woocommerce' ), 'B' => __( 'Barcelona', 'woocommerce' ), 'BU' => __( 'Burgos', 'woocommerce' ), - 'CC' => __( 'Cáceres', 'woocommerce' ), - 'CA' => __( 'Cádiz', 'woocommerce' ), + 'CC' => __( 'Cáceres', 'woocommerce' ), + 'CA' => __( 'Cádiz', 'woocommerce' ), 'S' => __( 'Cantabria', 'woocommerce' ), - 'CS' => __( 'Castellón', 'woocommerce' ), + 'CS' => __( 'Castellón', 'woocommerce' ), 'CE' => __( 'Ceuta', 'woocommerce' ), 'CR' => __( 'Ciudad Real', 'woocommerce' ), - 'CO' => __( 'Córdoba', 'woocommerce' ), + 'CO' => __( 'Córdoba', 'woocommerce' ), 'CU' => __( 'Cuenca', 'woocommerce' ), 'GI' => __( 'Girona', 'woocommerce' ), 'GR' => __( 'Granada', 'woocommerce' ), @@ -369,14 +369,14 @@ return array( 'SS' => __( 'Gipuzkoa', 'woocommerce' ), 'H' => __( 'Huelva', 'woocommerce' ), 'HU' => __( 'Huesca', 'woocommerce' ), - 'J' => __( 'Jaén', 'woocommerce' ), + 'J' => __( 'Jaén', 'woocommerce' ), 'LO' => __( 'La Rioja', 'woocommerce' ), 'GC' => __( 'Las Palmas', 'woocommerce' ), - 'LE' => __( 'León', 'woocommerce' ), + 'LE' => __( 'León', 'woocommerce' ), 'L' => __( 'Lleida', 'woocommerce' ), 'LU' => __( 'Lugo', 'woocommerce' ), 'M' => __( 'Madrid', 'woocommerce' ), - 'MA' => __( 'Málaga', 'woocommerce' ), + 'MA' => __( 'Málaga', 'woocommerce' ), 'ML' => __( 'Melilla', 'woocommerce' ), 'MU' => __( 'Murcia', 'woocommerce' ), 'NA' => __( 'Navarra', 'woocommerce' ), @@ -856,48 +856,48 @@ return array( ), 'LU' => array(), 'MD' => array( // Moldova states. - 'C' => __( 'Chișinău', 'woocommerce' ), - 'BL' => __( 'Bălți', 'woocommerce' ), + 'C' => __( 'Chișinău', 'woocommerce' ), + 'BL' => __( 'Bălți', 'woocommerce' ), 'AN' => __( 'Anenii Noi', 'woocommerce' ), 'BS' => __( 'Basarabeasca', 'woocommerce' ), 'BR' => __( 'Briceni', 'woocommerce' ), 'CH' => __( 'Cahul', 'woocommerce' ), 'CT' => __( 'Cantemir', 'woocommerce' ), - 'CL' => __( 'Călărași', 'woocommerce' ), - 'CS' => __( 'Căușeni', 'woocommerce' ), - 'CM' => __( 'Cimișlia', 'woocommerce' ), + 'CL' => __( 'Călărași', 'woocommerce' ), + 'CS' => __( 'Căușeni', 'woocommerce' ), + 'CM' => __( 'Cimișlia', 'woocommerce' ), 'CR' => __( 'Criuleni', 'woocommerce' ), - 'DN' => __( 'Dondușeni', 'woocommerce' ), + 'DN' => __( 'Dondușeni', 'woocommerce' ), 'DR' => __( 'Drochia', 'woocommerce' ), - 'DB' => __( 'Dubăsari', 'woocommerce' ), - 'ED' => __( 'Edineț', 'woocommerce' ), - 'FL' => __( 'Fălești', 'woocommerce' ), - 'FR' => __( 'Florești', 'woocommerce' ), - 'GE' => __( 'UTA Găgăuzia', 'woocommerce' ), + 'DB' => __( 'Dubăsari', 'woocommerce' ), + 'ED' => __( 'Edineț', 'woocommerce' ), + 'FL' => __( 'Fălești', 'woocommerce' ), + 'FR' => __( 'Florești', 'woocommerce' ), + 'GE' => __( 'UTA Găgăuzia', 'woocommerce' ), 'GL' => __( 'Glodeni', 'woocommerce' ), - 'HN' => __( 'Hîncești', 'woocommerce' ), + 'HN' => __( 'Hîncești', 'woocommerce' ), 'IL' => __( 'Ialoveni', 'woocommerce' ), 'LV' => __( 'Leova', 'woocommerce' ), 'NS' => __( 'Nisporeni', 'woocommerce' ), - 'OC' => __( 'Ocnița', 'woocommerce' ), + 'OC' => __( 'Ocnița', 'woocommerce' ), 'OR' => __( 'Orhei', 'woocommerce' ), 'RZ' => __( 'Rezina', 'woocommerce' ), - 'RS' => __( 'Rîșcani', 'woocommerce' ), - 'SG' => __( 'Sîngerei', 'woocommerce' ), + 'RS' => __( 'Rîșcani', 'woocommerce' ), + 'SG' => __( 'Sîngerei', 'woocommerce' ), 'SR' => __( 'Soroca', 'woocommerce' ), - 'ST' => __( 'Strășeni', 'woocommerce' ), - 'SD' => __( 'Șoldănești', 'woocommerce' ), - 'SV' => __( 'Ștefan Vodă', 'woocommerce' ), + 'ST' => __( 'Strășeni', 'woocommerce' ), + 'SD' => __( 'Șoldănești', 'woocommerce' ), + 'SV' => __( 'Ștefan Vodă', 'woocommerce' ), 'TR' => __( 'Taraclia', 'woocommerce' ), - 'TL' => __( 'Telenești', 'woocommerce' ), + 'TL' => __( 'Telenești', 'woocommerce' ), 'UN' => __( 'Ungheni', 'woocommerce' ), ), 'MQ' => array(), 'MT' => array(), 'MX' => array( // Mexico States. - 'DF' => __( 'Ciudad de México', 'woocommerce' ), + 'DF' => __( 'Ciudad de México', 'woocommerce' ), 'JA' => __( 'Jalisco', 'woocommerce' ), - 'NL' => __( 'Nuevo León', 'woocommerce' ), + 'NL' => __( 'Nuevo León', 'woocommerce' ), 'AG' => __( 'Aguascalientes', 'woocommerce' ), 'BC' => __( 'Baja California', 'woocommerce' ), 'BS' => __( 'Baja California Sur', 'woocommerce' ), @@ -910,22 +910,22 @@ return array( 'GT' => __( 'Guanajuato', 'woocommerce' ), 'GR' => __( 'Guerrero', 'woocommerce' ), 'HG' => __( 'Hidalgo', 'woocommerce' ), - 'MX' => __( 'Estado de México', 'woocommerce' ), - 'MI' => __( 'Michoacán', 'woocommerce' ), + 'MX' => __( 'Estado de México', 'woocommerce' ), + 'MI' => __( 'Michoacán', 'woocommerce' ), 'MO' => __( 'Morelos', 'woocommerce' ), 'NA' => __( 'Nayarit', 'woocommerce' ), 'OA' => __( 'Oaxaca', 'woocommerce' ), 'PU' => __( 'Puebla', 'woocommerce' ), - 'QT' => __( 'Querétaro', 'woocommerce' ), + 'QT' => __( 'Querétaro', 'woocommerce' ), 'QR' => __( 'Quintana Roo', 'woocommerce' ), - 'SL' => __( 'San Luis Potosí', 'woocommerce' ), + 'SL' => __( 'San Luis Potosí', 'woocommerce' ), 'SI' => __( 'Sinaloa', 'woocommerce' ), 'SO' => __( 'Sonora', 'woocommerce' ), 'TB' => __( 'Tabasco', 'woocommerce' ), 'TM' => __( 'Tamaulipas', 'woocommerce' ), 'TL' => __( 'Tlaxcala', 'woocommerce' ), 'VE' => __( 'Veracruz', 'woocommerce' ), - 'YU' => __( 'Yucatán', 'woocommerce' ), + 'YU' => __( 'Yucatán', 'woocommerce' ), 'ZA' => __( 'Zacatecas', 'woocommerce' ), ), 'MY' => array( // Malaysian states. @@ -1039,7 +1039,7 @@ return array( 'BP' => __( 'Bay of Plenty', 'woocommerce' ), 'TK' => __( 'Taranaki', 'woocommerce' ), 'GI' => __( 'Gisborne', 'woocommerce' ), - 'HB' => __( 'Hawke’s Bay', 'woocommerce' ), + 'HB' => __( 'Hawke’s Bay', 'woocommerce' ), 'MW' => __( 'Manawatu-Wanganui', 'woocommerce' ), 'WE' => __( 'Wellington', 'woocommerce' ), 'NS' => __( 'Nelson', 'woocommerce' ), @@ -1055,15 +1055,15 @@ return array( 'LMA' => __( 'Municipalidad Metropolitana de Lima', 'woocommerce' ), 'AMA' => __( 'Amazonas', 'woocommerce' ), 'ANC' => __( 'Ancash', 'woocommerce' ), - 'APU' => __( 'Apurímac', 'woocommerce' ), + 'APU' => __( 'Apurímac', 'woocommerce' ), 'ARE' => __( 'Arequipa', 'woocommerce' ), 'AYA' => __( 'Ayacucho', 'woocommerce' ), 'CAJ' => __( 'Cajamarca', 'woocommerce' ), 'CUS' => __( 'Cusco', 'woocommerce' ), 'HUV' => __( 'Huancavelica', 'woocommerce' ), - 'HUC' => __( 'Huánuco', 'woocommerce' ), + 'HUC' => __( 'Huánuco', 'woocommerce' ), 'ICA' => __( 'Ica', 'woocommerce' ), - 'JUN' => __( 'Junín', 'woocommerce' ), + 'JUN' => __( 'Junín', 'woocommerce' ), 'LAL' => __( 'La Libertad', 'woocommerce' ), 'LAM' => __( 'Lambayeque', 'woocommerce' ), 'LIM' => __( 'Lima', 'woocommerce' ), @@ -1073,7 +1073,7 @@ return array( 'PAS' => __( 'Pasco', 'woocommerce' ), 'PIU' => __( 'Piura', 'woocommerce' ), 'PUN' => __( 'Puno', 'woocommerce' ), - 'SAM' => __( 'San Martín', 'woocommerce' ), + 'SAM' => __( 'San Martín', 'woocommerce' ), 'TAC' => __( 'Tacna', 'woocommerce' ), 'TUM' => __( 'Tumbes', 'woocommerce' ), 'UCA' => __( 'Ucayali', 'woocommerce' ), @@ -1179,67 +1179,67 @@ return array( 'PL' => array(), 'PT' => array(), 'PY' => array( // Paraguay states. - 'PY-ASU' => __( 'Asunción', 'woocommerce' ), - 'PY-1' => __( 'Concepción', 'woocommerce' ), + 'PY-ASU' => __( 'Asunción', 'woocommerce' ), + 'PY-1' => __( 'Concepción', 'woocommerce' ), 'PY-2' => __( 'San Pedro', 'woocommerce' ), 'PY-3' => __( 'Cordillera', 'woocommerce' ), - 'PY-4' => __( 'Guairá', 'woocommerce' ), - 'PY-5' => __( 'Caaguazú', 'woocommerce' ), - 'PY-6' => __( 'Caazapá', 'woocommerce' ), - 'PY-7' => __( 'Itapúa', 'woocommerce' ), + 'PY-4' => __( 'Guairá', 'woocommerce' ), + 'PY-5' => __( 'Caaguazú', 'woocommerce' ), + 'PY-6' => __( 'Caazapá', 'woocommerce' ), + 'PY-7' => __( 'Itapúa', 'woocommerce' ), 'PY-8' => __( 'Misiones', 'woocommerce' ), - 'PY-9' => __( 'Paraguarí', 'woocommerce' ), - 'PY-10' => __( 'Alto Paraná', 'woocommerce' ), + 'PY-9' => __( 'Paraguarí', 'woocommerce' ), + 'PY-10' => __( 'Alto Paraná', 'woocommerce' ), 'PY-11' => __( 'Central', 'woocommerce' ), - 'PY-12' => __( 'Ñeembucú', 'woocommerce' ), + 'PY-12' => __( 'Ñeembucú', 'woocommerce' ), 'PY-13' => __( 'Amambay', 'woocommerce' ), - 'PY-14' => __( 'Canindeyú', 'woocommerce' ), + 'PY-14' => __( 'Canindeyú', 'woocommerce' ), 'PY-15' => __( 'Presidente Hayes', 'woocommerce' ), 'PY-16' => __( 'Alto Paraguay', 'woocommerce' ), - 'PY-17' => __( 'Boquerón', 'woocommerce' ), + 'PY-17' => __( 'Boquerón', 'woocommerce' ), ), 'RE' => array(), 'RO' => array( // Romania states. 'AB' => __( 'Alba', 'woocommerce' ), 'AR' => __( 'Arad', 'woocommerce' ), - 'AG' => __( 'Argeș', 'woocommerce' ), - 'BC' => __( 'Bacău', 'woocommerce' ), + 'AG' => __( 'Argeș', 'woocommerce' ), + 'BC' => __( 'Bacău', 'woocommerce' ), 'BH' => __( 'Bihor', 'woocommerce' ), - 'BN' => __( 'Bistrița-Năsăud', 'woocommerce' ), - 'BT' => __( 'Botoșani', 'woocommerce' ), - 'BR' => __( 'Brăila', 'woocommerce' ), - 'BV' => __( 'Brașov', 'woocommerce' ), - 'B' => __( 'București', 'woocommerce' ), - 'BZ' => __( 'Buzău', 'woocommerce' ), - 'CL' => __( 'Călărași', 'woocommerce' ), - 'CS' => __( 'Caraș-Severin', 'woocommerce' ), + 'BN' => __( 'Bistrița-Năsăud', 'woocommerce' ), + 'BT' => __( 'Botoșani', 'woocommerce' ), + 'BR' => __( 'Brăila', 'woocommerce' ), + 'BV' => __( 'Brașov', 'woocommerce' ), + 'B' => __( 'București', 'woocommerce' ), + 'BZ' => __( 'Buzău', 'woocommerce' ), + 'CL' => __( 'Călărași', 'woocommerce' ), + 'CS' => __( 'Caraș-Severin', 'woocommerce' ), 'CJ' => __( 'Cluj', 'woocommerce' ), - 'CT' => __( 'Constanța', 'woocommerce' ), + 'CT' => __( 'Constanța', 'woocommerce' ), 'CV' => __( 'Covasna', 'woocommerce' ), - 'DB' => __( 'Dâmbovița', 'woocommerce' ), + 'DB' => __( 'Dâmbovița', 'woocommerce' ), 'DJ' => __( 'Dolj', 'woocommerce' ), - 'GL' => __( 'Galați', 'woocommerce' ), + 'GL' => __( 'Galați', 'woocommerce' ), 'GR' => __( 'Giurgiu', 'woocommerce' ), 'GJ' => __( 'Gorj', 'woocommerce' ), 'HR' => __( 'Harghita', 'woocommerce' ), 'HD' => __( 'Hunedoara', 'woocommerce' ), - 'IL' => __( 'Ialomița', 'woocommerce' ), - 'IS' => __( 'Iași', 'woocommerce' ), + 'IL' => __( 'Ialomița', 'woocommerce' ), + 'IS' => __( 'Iași', 'woocommerce' ), 'IF' => __( 'Ilfov', 'woocommerce' ), - 'MM' => __( 'Maramureș', 'woocommerce' ), - 'MH' => __( 'Mehedinți', 'woocommerce' ), - 'MS' => __( 'Mureș', 'woocommerce' ), - 'NT' => __( 'Neamț', 'woocommerce' ), + 'MM' => __( 'Maramureș', 'woocommerce' ), + 'MH' => __( 'Mehedinți', 'woocommerce' ), + 'MS' => __( 'Mureș', 'woocommerce' ), + 'NT' => __( 'Neamț', 'woocommerce' ), 'OT' => __( 'Olt', 'woocommerce' ), 'PH' => __( 'Prahova', 'woocommerce' ), - 'SJ' => __( 'Sălaj', 'woocommerce' ), + 'SJ' => __( 'Sălaj', 'woocommerce' ), 'SM' => __( 'Satu Mare', 'woocommerce' ), 'SB' => __( 'Sibiu', 'woocommerce' ), 'SV' => __( 'Suceava', 'woocommerce' ), 'TR' => __( 'Teleorman', 'woocommerce' ), - 'TM' => __( 'Timiș', 'woocommerce' ), + 'TM' => __( 'Timiș', 'woocommerce' ), 'TL' => __( 'Tulcea', 'woocommerce' ), - 'VL' => __( 'Vâlcea', 'woocommerce' ), + 'VL' => __( 'Vâlcea', 'woocommerce' ), 'VS' => __( 'Vaslui', 'woocommerce' ), 'VN' => __( 'Vrancea', 'woocommerce' ), ), @@ -1328,56 +1328,56 @@ return array( ), 'TR' => array( // Turkey States. 'TR01' => __( 'Adana', 'woocommerce' ), - 'TR02' => __( 'Adıyaman', 'woocommerce' ), + 'TR02' => __( 'Adıyaman', 'woocommerce' ), 'TR03' => __( 'Afyon', 'woocommerce' ), - 'TR04' => __( 'Ağrı', 'woocommerce' ), + 'TR04' => __( 'Ağrı', 'woocommerce' ), 'TR05' => __( 'Amasya', 'woocommerce' ), 'TR06' => __( 'Ankara', 'woocommerce' ), 'TR07' => __( 'Antalya', 'woocommerce' ), 'TR08' => __( 'Artvin', 'woocommerce' ), - 'TR09' => __( 'Aydın', 'woocommerce' ), - 'TR10' => __( 'Balıkesir', 'woocommerce' ), + 'TR09' => __( 'Aydın', 'woocommerce' ), + 'TR10' => __( 'Balıkesir', 'woocommerce' ), 'TR11' => __( 'Bilecik', 'woocommerce' ), - 'TR12' => __( 'Bingöl', 'woocommerce' ), + 'TR12' => __( 'Bingöl', 'woocommerce' ), 'TR13' => __( 'Bitlis', 'woocommerce' ), 'TR14' => __( 'Bolu', 'woocommerce' ), 'TR15' => __( 'Burdur', 'woocommerce' ), 'TR16' => __( 'Bursa', 'woocommerce' ), - 'TR17' => __( 'Çanakkale', 'woocommerce' ), - 'TR18' => __( 'Çankırı', 'woocommerce' ), - 'TR19' => __( 'Çorum', 'woocommerce' ), + 'TR17' => __( 'Çanakkale', 'woocommerce' ), + 'TR18' => __( 'Çankırı', 'woocommerce' ), + 'TR19' => __( 'Çorum', 'woocommerce' ), 'TR20' => __( 'Denizli', 'woocommerce' ), - 'TR21' => __( 'Diyarbakır', 'woocommerce' ), + 'TR21' => __( 'Diyarbakır', 'woocommerce' ), 'TR22' => __( 'Edirne', 'woocommerce' ), - 'TR23' => __( 'Elazığ', 'woocommerce' ), + 'TR23' => __( 'Elazığ', 'woocommerce' ), 'TR24' => __( 'Erzincan', 'woocommerce' ), 'TR25' => __( 'Erzurum', 'woocommerce' ), - 'TR26' => __( 'Eskişehir', 'woocommerce' ), + 'TR26' => __( 'Eskişehir', 'woocommerce' ), 'TR27' => __( 'Gaziantep', 'woocommerce' ), 'TR28' => __( 'Giresun', 'woocommerce' ), - 'TR29' => __( 'Gümüşhane', 'woocommerce' ), + 'TR29' => __( 'Gümüşhane', 'woocommerce' ), 'TR30' => __( 'Hakkari', 'woocommerce' ), 'TR31' => __( 'Hatay', 'woocommerce' ), 'TR32' => __( 'Isparta', 'woocommerce' ), - 'TR33' => __( 'İçel', 'woocommerce' ), - 'TR34' => __( 'İstanbul', 'woocommerce' ), - 'TR35' => __( 'İzmir', 'woocommerce' ), + 'TR33' => __( 'İçel', 'woocommerce' ), + 'TR34' => __( 'İstanbul', 'woocommerce' ), + 'TR35' => __( 'İzmir', 'woocommerce' ), 'TR36' => __( 'Kars', 'woocommerce' ), 'TR37' => __( 'Kastamonu', 'woocommerce' ), 'TR38' => __( 'Kayseri', 'woocommerce' ), - 'TR39' => __( 'Kırklareli', 'woocommerce' ), - 'TR40' => __( 'Kırşehir', 'woocommerce' ), + 'TR39' => __( 'Kırklareli', 'woocommerce' ), + 'TR40' => __( 'Kırşehir', 'woocommerce' ), 'TR41' => __( 'Kocaeli', 'woocommerce' ), 'TR42' => __( 'Konya', 'woocommerce' ), - 'TR43' => __( 'Kütahya', 'woocommerce' ), + 'TR43' => __( 'Kütahya', 'woocommerce' ), 'TR44' => __( 'Malatya', 'woocommerce' ), 'TR45' => __( 'Manisa', 'woocommerce' ), - 'TR46' => __( 'Kahramanmaraş', 'woocommerce' ), + 'TR46' => __( 'Kahramanmaraş', 'woocommerce' ), 'TR47' => __( 'Mardin', 'woocommerce' ), - 'TR48' => __( 'Muğla', 'woocommerce' ), - 'TR49' => __( 'Muş', 'woocommerce' ), - 'TR50' => __( 'Nevşehir', 'woocommerce' ), - 'TR51' => __( 'Niğde', 'woocommerce' ), + 'TR48' => __( 'Muğla', 'woocommerce' ), + 'TR49' => __( 'Muş', 'woocommerce' ), + 'TR50' => __( 'Nevşehir', 'woocommerce' ), + 'TR51' => __( 'Niğde', 'woocommerce' ), 'TR52' => __( 'Ordu', 'woocommerce' ), 'TR53' => __( 'Rize', 'woocommerce' ), 'TR54' => __( 'Sakarya', 'woocommerce' ), @@ -1385,29 +1385,29 @@ return array( 'TR56' => __( 'Siirt', 'woocommerce' ), 'TR57' => __( 'Sinop', 'woocommerce' ), 'TR58' => __( 'Sivas', 'woocommerce' ), - 'TR59' => __( 'Tekirdağ', 'woocommerce' ), + 'TR59' => __( 'Tekirdağ', 'woocommerce' ), 'TR60' => __( 'Tokat', 'woocommerce' ), 'TR61' => __( 'Trabzon', 'woocommerce' ), 'TR62' => __( 'Tunceli', 'woocommerce' ), - 'TR63' => __( 'Şanlıurfa', 'woocommerce' ), - 'TR64' => __( 'Uşak', 'woocommerce' ), + 'TR63' => __( 'Şanlıurfa', 'woocommerce' ), + 'TR64' => __( 'Uşak', 'woocommerce' ), 'TR65' => __( 'Van', 'woocommerce' ), 'TR66' => __( 'Yozgat', 'woocommerce' ), 'TR67' => __( 'Zonguldak', 'woocommerce' ), 'TR68' => __( 'Aksaray', 'woocommerce' ), 'TR69' => __( 'Bayburt', 'woocommerce' ), 'TR70' => __( 'Karaman', 'woocommerce' ), - 'TR71' => __( 'Kırıkkale', 'woocommerce' ), + 'TR71' => __( 'Kırıkkale', 'woocommerce' ), 'TR72' => __( 'Batman', 'woocommerce' ), - 'TR73' => __( 'Şırnak', 'woocommerce' ), - 'TR74' => __( 'Bartın', 'woocommerce' ), + 'TR73' => __( 'Şırnak', 'woocommerce' ), + 'TR74' => __( 'Bartın', 'woocommerce' ), 'TR75' => __( 'Ardahan', 'woocommerce' ), - 'TR76' => __( 'Iğdır', 'woocommerce' ), + 'TR76' => __( 'Iğdır', 'woocommerce' ), 'TR77' => __( 'Yalova', 'woocommerce' ), - 'TR78' => __( 'Karabük', 'woocommerce' ), + 'TR78' => __( 'Karabük', 'woocommerce' ), 'TR79' => __( 'Kilis', 'woocommerce' ), 'TR80' => __( 'Osmaniye', 'woocommerce' ), - 'TR81' => __( 'Düzce', 'woocommerce' ), + 'TR81' => __( 'Düzce', 'woocommerce' ), ), 'TZ' => array( // Tanzania States. 'TZ01' => __( 'Arusha', 'woocommerce' ), diff --git a/includes/abstracts/abstract-wc-data.php b/includes/abstracts/abstract-wc-data.php index ea54e7085df..383d6d19547 100644 --- a/includes/abstracts/abstract-wc-data.php +++ b/includes/abstracts/abstract-wc-data.php @@ -7,7 +7,7 @@ * * @class WC_Data * @version 3.0.0 - * @package WooCommerce/Classes + * @package WooCommerce\Classes */ if ( ! defined( 'ABSPATH' ) ) { @@ -20,7 +20,7 @@ if ( ! defined( 'ABSPATH' ) ) { * Implemented by classes using the same CRUD(s) pattern. * * @version 2.6.0 - * @package WooCommerce/Abstracts + * @package WooCommerce\Abstracts */ abstract class WC_Data { diff --git a/includes/abstracts/abstract-wc-integration.php b/includes/abstracts/abstract-wc-integration.php index 8ad46759b09..98dd28486f5 100644 --- a/includes/abstracts/abstract-wc-integration.php +++ b/includes/abstracts/abstract-wc-integration.php @@ -7,7 +7,7 @@ * * @class WC_Settings_API * @version 2.6.0 - * @package WooCommerce/Abstracts + * @package WooCommerce\Abstracts */ if ( ! defined( 'ABSPATH' ) ) { @@ -22,7 +22,7 @@ if ( ! defined( 'ABSPATH' ) ) { * @class WC_Integration * @extends WC_Settings_API * @version 2.6.0 - * @package WooCommerce/Abstracts + * @package WooCommerce\Abstracts */ abstract class WC_Integration extends WC_Settings_API { diff --git a/includes/abstracts/abstract-wc-log-handler.php b/includes/abstracts/abstract-wc-log-handler.php index 64d6e0a6c50..4cf148d172e 100644 --- a/includes/abstracts/abstract-wc-log-handler.php +++ b/includes/abstracts/abstract-wc-log-handler.php @@ -3,7 +3,7 @@ * Log handling functionality. * * @class WC_Log_Handler - * @package WooCommerce/Abstracts + * @package WooCommerce\Abstracts */ if ( ! defined( 'ABSPATH' ) ) { @@ -14,7 +14,7 @@ if ( ! defined( 'ABSPATH' ) ) { * Abstract WC Log Handler Class * * @version 1.0.0 - * @package WooCommerce/Abstracts + * @package WooCommerce\Abstracts */ abstract class WC_Log_Handler implements WC_Log_Handler_Interface { diff --git a/includes/abstracts/abstract-wc-object-query.php b/includes/abstracts/abstract-wc-object-query.php index 7e232462a6a..0d1542056cd 100644 --- a/includes/abstracts/abstract-wc-object-query.php +++ b/includes/abstracts/abstract-wc-object-query.php @@ -2,7 +2,7 @@ /** * Query abstraction layer functionality. * - * @package WooCommerce/Abstracts + * @package WooCommerce\Abstracts */ if ( ! defined( 'ABSPATH' ) ) { @@ -15,7 +15,7 @@ if ( ! defined( 'ABSPATH' ) ) { * Extended by classes to provide a query abstraction layer for safe object searching. * * @version 3.1.0 - * @package WooCommerce/Abstracts + * @package WooCommerce\Abstracts */ abstract class WC_Object_Query { diff --git a/includes/abstracts/abstract-wc-order.php b/includes/abstracts/abstract-wc-order.php index 5efd4b0c9e6..3cca0abefb2 100644 --- a/includes/abstracts/abstract-wc-order.php +++ b/includes/abstracts/abstract-wc-order.php @@ -7,7 +7,7 @@ * * @class WC_Abstract_Order * @version 3.0.0 - * @package WooCommerce/Classes + * @package WooCommerce\Classes */ defined( 'ABSPATH' ) || exit; diff --git a/includes/abstracts/abstract-wc-payment-gateway.php b/includes/abstracts/abstract-wc-payment-gateway.php index 180c3f05adf..dcfb7614ff8 100644 --- a/includes/abstracts/abstract-wc-payment-gateway.php +++ b/includes/abstracts/abstract-wc-payment-gateway.php @@ -6,7 +6,7 @@ * * @class WC_Payment_Gateway * @version 2.1.0 - * @package WooCommerce/Abstracts + * @package WooCommerce\Abstracts */ use Automattic\Jetpack\Constants; @@ -23,7 +23,7 @@ if ( ! defined( 'ABSPATH' ) ) { * @class WC_Payment_Gateway * @extends WC_Settings_API * @version 2.1.0 - * @package WooCommerce/Abstracts + * @package WooCommerce\Abstracts */ abstract class WC_Payment_Gateway extends WC_Settings_API { diff --git a/includes/abstracts/abstract-wc-payment-token.php b/includes/abstracts/abstract-wc-payment-token.php index 60635bad6d7..ad958991981 100644 --- a/includes/abstracts/abstract-wc-payment-token.php +++ b/includes/abstracts/abstract-wc-payment-token.php @@ -5,7 +5,7 @@ * Generic payment tokens functionality which can be extended by idividual types of payment tokens. * * @class WC_Payment_Token - * @package WooCommerce/Abstracts + * @package WooCommerce\Abstracts */ if ( ! defined( 'ABSPATH' ) ) { @@ -23,7 +23,7 @@ require_once WC_ABSPATH . 'includes/legacy/abstract-wc-legacy-payment-token.php' * @class WC_Payment_Token * @version 3.0.0 * @since 2.6.0 - * @package WooCommerce/Abstracts + * @package WooCommerce\Abstracts */ abstract class WC_Payment_Token extends WC_Legacy_Payment_Token { diff --git a/includes/abstracts/abstract-wc-privacy.php b/includes/abstracts/abstract-wc-privacy.php index 5908915e6de..1f181481ee8 100644 --- a/includes/abstracts/abstract-wc-privacy.php +++ b/includes/abstracts/abstract-wc-privacy.php @@ -3,7 +3,7 @@ * WooCommerce abstract privacy class. * * @since 3.4.0 - * @package WooCommerce/Abstracts + * @package WooCommerce\Abstracts */ defined( 'ABSPATH' ) || exit; @@ -15,7 +15,7 @@ defined( 'ABSPATH' ) || exit; * privacy data to be exported and privacy data to be deleted. * * @version 3.4.0 - * @package WooCommerce/Abstracts + * @package WooCommerce\Abstracts */ abstract class WC_Abstract_Privacy { /** diff --git a/includes/abstracts/abstract-wc-product.php b/includes/abstracts/abstract-wc-product.php index 6109689ab33..f45d1449ebb 100644 --- a/includes/abstracts/abstract-wc-product.php +++ b/includes/abstracts/abstract-wc-product.php @@ -2,7 +2,7 @@ /** * WooCommerce product base class. * - * @package WooCommerce/Abstracts + * @package WooCommerce\Abstracts */ if ( ! defined( 'ABSPATH' ) ) { @@ -21,7 +21,7 @@ require_once WC_ABSPATH . 'includes/legacy/abstract-wc-legacy-product.php'; * The WooCommerce product class handles individual product data. * * @version 3.0.0 - * @package WooCommerce/Abstracts + * @package WooCommerce\Abstracts */ class WC_Product extends WC_Abstract_Legacy_Product { diff --git a/includes/abstracts/abstract-wc-session.php b/includes/abstracts/abstract-wc-session.php index fb4fc5b0e03..bec2ac223ba 100644 --- a/includes/abstracts/abstract-wc-session.php +++ b/includes/abstracts/abstract-wc-session.php @@ -4,7 +4,7 @@ * * @class WC_Session * @version 2.0.0 - * @package WooCommerce/Abstracts + * @package WooCommerce\Abstracts */ if ( ! defined( 'ABSPATH' ) ) { diff --git a/includes/abstracts/abstract-wc-settings-api.php b/includes/abstracts/abstract-wc-settings-api.php index 4fd19ca7041..f33a62365fc 100644 --- a/includes/abstracts/abstract-wc-settings-api.php +++ b/includes/abstracts/abstract-wc-settings-api.php @@ -4,7 +4,7 @@ * * Admin Settings API used by Integrations, Shipping Methods, and Payment Gateways. * - * @package WooCommerce/Abstracts + * @package WooCommerce\Abstracts */ defined( 'ABSPATH' ) || exit; diff --git a/includes/abstracts/abstract-wc-shipping-method.php b/includes/abstracts/abstract-wc-shipping-method.php index c5922a75627..c27b0ae961a 100644 --- a/includes/abstracts/abstract-wc-shipping-method.php +++ b/includes/abstracts/abstract-wc-shipping-method.php @@ -3,7 +3,7 @@ * Abstract shipping method * * @class WC_Shipping_Method - * @package WooCommerce/Abstracts + * @package WooCommerce\Abstracts */ if ( ! defined( 'ABSPATH' ) ) { @@ -17,7 +17,7 @@ if ( ! defined( 'ABSPATH' ) ) { * * @class WC_Shipping_Method * @version 3.0.0 - * @package WooCommerce/Abstracts + * @package WooCommerce\Abstracts */ abstract class WC_Shipping_Method extends WC_Settings_API { diff --git a/includes/abstracts/abstract-wc-widget.php b/includes/abstracts/abstract-wc-widget.php index c2f6f1bf298..10ac9e4cec6 100644 --- a/includes/abstracts/abstract-wc-widget.php +++ b/includes/abstracts/abstract-wc-widget.php @@ -3,7 +3,7 @@ * Abstract widget class * * @class WC_Widget - * @package WooCommerce/Abstracts + * @package WooCommerce\Abstracts */ use Automattic\Jetpack\Constants; @@ -15,7 +15,7 @@ if ( ! defined( 'ABSPATH' ) ) { /** * WC_Widget * - * @package WooCommerce/Abstracts + * @package WooCommerce\Abstracts * @version 2.5.0 * @extends WP_Widget */ diff --git a/includes/abstracts/class-wc-background-process.php b/includes/abstracts/class-wc-background-process.php index 6d9662208fc..a48fa589240 100644 --- a/includes/abstracts/class-wc-background-process.php +++ b/includes/abstracts/class-wc-background-process.php @@ -5,7 +5,7 @@ * Uses https://github.com/A5hleyRich/wp-background-processing to handle DB * updates in the background. * - * @package WooCommerce/Classes + * @package WooCommerce\Classes */ defined( 'ABSPATH' ) || exit; diff --git a/includes/admin/class-wc-admin-addons.php b/includes/admin/class-wc-admin-addons.php index 42faab0fb19..6d24bf71db4 100644 --- a/includes/admin/class-wc-admin-addons.php +++ b/includes/admin/class-wc-admin-addons.php @@ -2,7 +2,7 @@ /** * Addons Page * - * @package WooCommerce/Admin + * @package WooCommerce\Admin * @version 2.5.0 */ diff --git a/includes/admin/class-wc-admin-assets.php b/includes/admin/class-wc-admin-assets.php index a7760148a84..567e2efe7ca 100644 --- a/includes/admin/class-wc-admin-assets.php +++ b/includes/admin/class-wc-admin-assets.php @@ -2,7 +2,7 @@ /** * Load assets * - * @package WooCommerce/Admin + * @package WooCommerce\Admin * @version 3.7.0 */ diff --git a/includes/admin/class-wc-admin-attributes.php b/includes/admin/class-wc-admin-attributes.php index db152ac45ca..9dc884fe9e9 100644 --- a/includes/admin/class-wc-admin-attributes.php +++ b/includes/admin/class-wc-admin-attributes.php @@ -4,7 +4,7 @@ * * The attributes section lets users add custom attributes to assign to products - they can also be used in the "Filter Products by Attribute" widget. * - * @package WooCommerce/Admin + * @package WooCommerce\Admin * @version 2.3.0 */ diff --git a/includes/admin/class-wc-admin-customize.php b/includes/admin/class-wc-admin-customize.php index 20834846126..cba3f2b4e68 100644 --- a/includes/admin/class-wc-admin-customize.php +++ b/includes/admin/class-wc-admin-customize.php @@ -4,7 +4,7 @@ * * @author WooCommerce * @category Admin - * @package WooCommerce/Admin/Customize + * @package WooCommerce\Admin\Customize * @version 3.1.0 */ diff --git a/includes/admin/class-wc-admin-dashboard.php b/includes/admin/class-wc-admin-dashboard.php index 28d1c6b0fe2..8f1a8348405 100644 --- a/includes/admin/class-wc-admin-dashboard.php +++ b/includes/admin/class-wc-admin-dashboard.php @@ -2,7 +2,7 @@ /** * Admin Dashboard * - * @package WooCommerce/Admin + * @package WooCommerce\Admin * @version 2.1.0 */ diff --git a/includes/admin/class-wc-admin-duplicate-product.php b/includes/admin/class-wc-admin-duplicate-product.php index 4a11254e524..05c67028ac3 100644 --- a/includes/admin/class-wc-admin-duplicate-product.php +++ b/includes/admin/class-wc-admin-duplicate-product.php @@ -2,7 +2,7 @@ /** * Duplicate product functionality * - * @package WooCommerce/Admin + * @package WooCommerce\Admin * @version 3.0.0 */ diff --git a/includes/admin/class-wc-admin-exporters.php b/includes/admin/class-wc-admin-exporters.php index 011d2e1429e..c6dbc48632c 100644 --- a/includes/admin/class-wc-admin-exporters.php +++ b/includes/admin/class-wc-admin-exporters.php @@ -2,7 +2,7 @@ /** * Init WooCommerce data exporters. * - * @package WooCommerce/Admin + * @package WooCommerce\Admin * @version 3.1.0 */ diff --git a/includes/admin/class-wc-admin-help.php b/includes/admin/class-wc-admin-help.php index 334055900b0..aa5bc03a7bf 100644 --- a/includes/admin/class-wc-admin-help.php +++ b/includes/admin/class-wc-admin-help.php @@ -2,7 +2,7 @@ /** * Add some content to the help tab * - * @package WooCommerce/Admin + * @package WooCommerce\Admin * @version 2.1.0 */ diff --git a/includes/admin/class-wc-admin-importers.php b/includes/admin/class-wc-admin-importers.php index 8f85ad27b60..95f9e3bbca7 100644 --- a/includes/admin/class-wc-admin-importers.php +++ b/includes/admin/class-wc-admin-importers.php @@ -2,7 +2,7 @@ /** * Init WooCommerce data importers. * - * @package WooCommerce/Admin + * @package WooCommerce\Admin */ use Automattic\Jetpack\Constants; diff --git a/includes/admin/class-wc-admin-log-table-list.php b/includes/admin/class-wc-admin-log-table-list.php index 48e51f9bffc..2d9b9225632 100644 --- a/includes/admin/class-wc-admin-log-table-list.php +++ b/includes/admin/class-wc-admin-log-table-list.php @@ -4,7 +4,7 @@ * * @author WooThemes * @category Admin - * @package WooCommerce/Admin + * @package WooCommerce\Admin * @version 1.0.0 */ diff --git a/includes/admin/class-wc-admin-meta-boxes.php b/includes/admin/class-wc-admin-meta-boxes.php index 32e03797591..df1db141968 100644 --- a/includes/admin/class-wc-admin-meta-boxes.php +++ b/includes/admin/class-wc-admin-meta-boxes.php @@ -4,7 +4,7 @@ * * Sets up the write panels used by products and orders (custom post types). * - * @package WooCommerce/Admin/Meta Boxes + * @package WooCommerce\Admin\Meta Boxes */ use Automattic\Jetpack\Constants; diff --git a/includes/admin/class-wc-admin-permalink-settings.php b/includes/admin/class-wc-admin-permalink-settings.php index ec6534cc142..abcd784d721 100644 --- a/includes/admin/class-wc-admin-permalink-settings.php +++ b/includes/admin/class-wc-admin-permalink-settings.php @@ -5,7 +5,7 @@ * @class WC_Admin_Permalink_Settings * @author WooThemes * @category Admin - * @package WooCommerce/Admin + * @package WooCommerce\Admin * @version 2.3.0 */ diff --git a/includes/admin/class-wc-admin-pointers.php b/includes/admin/class-wc-admin-pointers.php index c4020b7f928..23242fe3f8d 100644 --- a/includes/admin/class-wc-admin-pointers.php +++ b/includes/admin/class-wc-admin-pointers.php @@ -4,7 +4,7 @@ * * @author WooThemes * @category Admin - * @package WooCommerce/Admin + * @package WooCommerce\Admin * @version 2.4.0 */ diff --git a/includes/admin/class-wc-admin-post-types.php b/includes/admin/class-wc-admin-post-types.php index 41fa6a3c4df..e2354239990 100644 --- a/includes/admin/class-wc-admin-post-types.php +++ b/includes/admin/class-wc-admin-post-types.php @@ -2,7 +2,7 @@ /** * Post Types Admin * - * @package WooCommerce/admin + * @package WooCommerce\Admin * @version 3.3.0 */ diff --git a/includes/admin/class-wc-admin-profile.php b/includes/admin/class-wc-admin-profile.php index 9aafac10e2d..94180722001 100644 --- a/includes/admin/class-wc-admin-profile.php +++ b/includes/admin/class-wc-admin-profile.php @@ -4,7 +4,7 @@ * * @author WooThemes * @category Admin - * @package WooCommerce/Admin + * @package WooCommerce\Admin * @version 2.4.0 */ diff --git a/includes/admin/class-wc-admin-reports.php b/includes/admin/class-wc-admin-reports.php index 541fe67cd1b..0d5dc4372e5 100644 --- a/includes/admin/class-wc-admin-reports.php +++ b/includes/admin/class-wc-admin-reports.php @@ -6,7 +6,7 @@ * * @author WooThemes * @category Admin - * @package WooCommerce/Admin/Reports + * @package WooCommerce\Admin\Reports * @version 2.0.0 */ diff --git a/includes/admin/class-wc-admin-settings.php b/includes/admin/class-wc-admin-settings.php index 1d1a49b8311..20d3d1c1ddc 100644 --- a/includes/admin/class-wc-admin-settings.php +++ b/includes/admin/class-wc-admin-settings.php @@ -2,7 +2,7 @@ /** * WooCommerce Admin Settings Class * - * @package WooCommerce/Admin + * @package WooCommerce\Admin * @version 3.4.0 */ diff --git a/includes/admin/class-wc-admin-setup-wizard.php b/includes/admin/class-wc-admin-setup-wizard.php index 1976596f6df..d5402a4bb12 100644 --- a/includes/admin/class-wc-admin-setup-wizard.php +++ b/includes/admin/class-wc-admin-setup-wizard.php @@ -4,7 +4,7 @@ * * Takes new users through some basic steps to setup their store. * - * @package WooCommerce/Admin + * @package WooCommerce\Admin * @version 2.6.0 */ diff --git a/includes/admin/class-wc-admin-status.php b/includes/admin/class-wc-admin-status.php index 23e670ec8d7..cb7b9387482 100644 --- a/includes/admin/class-wc-admin-status.php +++ b/includes/admin/class-wc-admin-status.php @@ -2,7 +2,7 @@ /** * Debug/Status page * - * @package WooCommerce/Admin/System Status + * @package WooCommerce\Admin\System Status * @version 2.2.0 */ diff --git a/includes/admin/class-wc-admin-taxonomies.php b/includes/admin/class-wc-admin-taxonomies.php index b5a32d7eb87..bb212d69ff3 100644 --- a/includes/admin/class-wc-admin-taxonomies.php +++ b/includes/admin/class-wc-admin-taxonomies.php @@ -4,7 +4,7 @@ * * @class WC_Admin_Taxonomies * @version 2.3.10 - * @package WooCommerce/Admin + * @package WooCommerce\Admin */ if ( ! defined( 'ABSPATH' ) ) { diff --git a/includes/admin/class-wc-admin.php b/includes/admin/class-wc-admin.php index 8a9d6e8bd43..e864cd80e1f 100644 --- a/includes/admin/class-wc-admin.php +++ b/includes/admin/class-wc-admin.php @@ -3,7 +3,7 @@ * WooCommerce Admin * * @class WC_Admin - * @package WooCommerce/Admin + * @package WooCommerce\Admin * @version 2.6.0 */ diff --git a/includes/admin/helper/class-wc-helper-api.php b/includes/admin/helper/class-wc-helper-api.php index e1baf489000..9e7c241ccde 100644 --- a/includes/admin/helper/class-wc-helper-api.php +++ b/includes/admin/helper/class-wc-helper-api.php @@ -3,7 +3,7 @@ * WooCommerce Admin * * @class WC_Helper_API - * @package WooCommerce/Admin + * @package WooCommerce\Admin */ if ( ! defined( 'ABSPATH' ) ) { diff --git a/includes/admin/helper/class-wc-helper-updater.php b/includes/admin/helper/class-wc-helper-updater.php index 62941f3f734..3feccb16ac4 100644 --- a/includes/admin/helper/class-wc-helper-updater.php +++ b/includes/admin/helper/class-wc-helper-updater.php @@ -3,7 +3,7 @@ * The update helper for WooCommerce.com plugins. * * @class WC_Helper_Updater - * @package WooCommerce/Admin. + * @package WooCommerce\Admin. */ if ( ! defined( 'ABSPATH' ) ) { diff --git a/includes/admin/helper/class-wc-helper.php b/includes/admin/helper/class-wc-helper.php index 40beb7e9493..6ee44dc8e3e 100644 --- a/includes/admin/helper/class-wc-helper.php +++ b/includes/admin/helper/class-wc-helper.php @@ -3,7 +3,7 @@ * WooCommerce Admin * * @class WC_Helper - * @package WooCommerce/Admin + * @package WooCommerce\Admin */ use Automattic\Jetpack\Constants; diff --git a/includes/admin/helper/views/html-section-nav.php b/includes/admin/helper/views/html-section-nav.php index 6b6dcf389a5..9af76fe32a0 100644 --- a/includes/admin/helper/views/html-section-nav.php +++ b/includes/admin/helper/views/html-section-nav.php @@ -2,7 +2,7 @@ /** * Helper admin navigation. * - * @package WooCommerce/Helper + * @package WooCommerce\Helper */ defined( 'ABSPATH' ) || exit(); ?> diff --git a/includes/admin/importers/class-wc-product-csv-importer-controller.php b/includes/admin/importers/class-wc-product-csv-importer-controller.php index 0d4afc75eed..0de82507fdc 100644 --- a/includes/admin/importers/class-wc-product-csv-importer-controller.php +++ b/includes/admin/importers/class-wc-product-csv-importer-controller.php @@ -16,7 +16,7 @@ if ( ! class_exists( 'WP_Importer' ) ) { /** * Product importer controller - handles file upload and forms in admin. * - * @package WooCommerce/Admin/Importers + * @package WooCommerce\Admin\Importers * @version 3.1.0 */ class WC_Product_CSV_Importer_Controller { diff --git a/includes/admin/importers/class-wc-tax-rate-importer.php b/includes/admin/importers/class-wc-tax-rate-importer.php index c0693872d8d..79f9cd10c09 100644 --- a/includes/admin/importers/class-wc-tax-rate-importer.php +++ b/includes/admin/importers/class-wc-tax-rate-importer.php @@ -3,7 +3,7 @@ * Tax importer class file * * @version 2.3.0 - * @package WooCommerce/Admin + * @package WooCommerce\Admin */ if ( ! defined( 'ABSPATH' ) ) { @@ -17,7 +17,7 @@ if ( ! class_exists( 'WP_Importer' ) ) { /** * Tax Rates importer - import tax rates and local tax rates into WooCommerce. * - * @package WooCommerce/Admin/Importers + * @package WooCommerce\Admin\Importers * @version 2.3.0 */ class WC_Tax_Rate_Importer extends WP_Importer { diff --git a/includes/admin/importers/views/html-product-csv-import-form.php b/includes/admin/importers/views/html-product-csv-import-form.php index f7e44815c11..3b96ec64f47 100644 --- a/includes/admin/importers/views/html-product-csv-import-form.php +++ b/includes/admin/importers/views/html-product-csv-import-form.php @@ -2,7 +2,7 @@ /** * Admin View: Product import form * - * @package WooCommerce/Admin + * @package WooCommerce\Admin */ if ( ! defined( 'ABSPATH' ) ) { diff --git a/includes/admin/list-tables/abstract-class-wc-admin-list-table.php b/includes/admin/list-tables/abstract-class-wc-admin-list-table.php index f985a80185b..4a0c93d6274 100644 --- a/includes/admin/list-tables/abstract-class-wc-admin-list-table.php +++ b/includes/admin/list-tables/abstract-class-wc-admin-list-table.php @@ -2,7 +2,7 @@ /** * List tables. * - * @package WooCommerce/Admin + * @package WooCommerce\Admin * @version 3.3.0 */ diff --git a/includes/admin/list-tables/class-wc-admin-list-table-coupons.php b/includes/admin/list-tables/class-wc-admin-list-table-coupons.php index dd9cab75a45..5a8ea00c78e 100644 --- a/includes/admin/list-tables/class-wc-admin-list-table-coupons.php +++ b/includes/admin/list-tables/class-wc-admin-list-table-coupons.php @@ -2,7 +2,7 @@ /** * List tables: coupons. * - * @package WooCommerce/Admin + * @package WooCommerce\Admin * @version 3.3.0 */ diff --git a/includes/admin/list-tables/class-wc-admin-list-table-orders.php b/includes/admin/list-tables/class-wc-admin-list-table-orders.php index 87a8619e45a..e76991fe440 100644 --- a/includes/admin/list-tables/class-wc-admin-list-table-orders.php +++ b/includes/admin/list-tables/class-wc-admin-list-table-orders.php @@ -2,7 +2,7 @@ /** * List tables: orders. * - * @package WooCommerce\admin + * @package WooCommerce\Admin * @version 3.3.0 */ diff --git a/includes/admin/list-tables/class-wc-admin-list-table-products.php b/includes/admin/list-tables/class-wc-admin-list-table-products.php index 6bc4ba30eed..05520130bc7 100644 --- a/includes/admin/list-tables/class-wc-admin-list-table-products.php +++ b/includes/admin/list-tables/class-wc-admin-list-table-products.php @@ -2,7 +2,7 @@ /** * List tables: products. * - * @package WooCommerce/Admin + * @package WooCommerce\Admin * @version 3.3.0 */ diff --git a/includes/admin/marketplace-suggestions/views/container.php b/includes/admin/marketplace-suggestions/views/container.php index 4799d457990..85071f3106b 100644 --- a/includes/admin/marketplace-suggestions/views/container.php +++ b/includes/admin/marketplace-suggestions/views/container.php @@ -2,7 +2,7 @@ /** * Marketplace suggestions container * - * @package WooCommerce/Templates + * @package WooCommerce\Templates * @version 3.6.0 */ 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 c8b92d10e2a..5a855e33cec 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 @@ -6,7 +6,7 @@ * * @author WooThemes * @category Admin - * @package WooCommerce/Admin/Meta Boxes + * @package WooCommerce\Admin\Meta Boxes * @version 2.1.0 */ diff --git a/includes/admin/meta-boxes/class-wc-meta-box-order-actions.php b/includes/admin/meta-boxes/class-wc-meta-box-order-actions.php index 303bdaad9d3..6e68d641740 100644 --- a/includes/admin/meta-boxes/class-wc-meta-box-order-actions.php +++ b/includes/admin/meta-boxes/class-wc-meta-box-order-actions.php @@ -6,7 +6,7 @@ * * @author WooThemes * @category Admin - * @package WooCommerce/Admin/Meta Boxes + * @package WooCommerce\Admin\Meta Boxes * @version 2.1.0 */ diff --git a/includes/admin/meta-boxes/class-wc-meta-box-order-data.php b/includes/admin/meta-boxes/class-wc-meta-box-order-data.php index f5568ddb2d1..4c30257d3dd 100644 --- a/includes/admin/meta-boxes/class-wc-meta-box-order-data.php +++ b/includes/admin/meta-boxes/class-wc-meta-box-order-data.php @@ -6,7 +6,7 @@ * * @author WooThemes * @category Admin - * @package WooCommerce/Admin/Meta Boxes + * @package WooCommerce\Admin\Meta Boxes * @version 2.2.0 */ diff --git a/includes/admin/meta-boxes/class-wc-meta-box-order-downloads.php b/includes/admin/meta-boxes/class-wc-meta-box-order-downloads.php index 897842500c0..c519dd78324 100644 --- a/includes/admin/meta-boxes/class-wc-meta-box-order-downloads.php +++ b/includes/admin/meta-boxes/class-wc-meta-box-order-downloads.php @@ -4,7 +4,7 @@ * * @author WooThemes * @category Admin - * @package WooCommerce/Admin/Meta Boxes + * @package WooCommerce\Admin\Meta Boxes * @version 2.1.0 */ diff --git a/includes/admin/meta-boxes/class-wc-meta-box-order-items.php b/includes/admin/meta-boxes/class-wc-meta-box-order-items.php index a70bea86889..0f5bf54eb74 100644 --- a/includes/admin/meta-boxes/class-wc-meta-box-order-items.php +++ b/includes/admin/meta-boxes/class-wc-meta-box-order-items.php @@ -6,7 +6,7 @@ * * @author WooThemes * @category Admin - * @package WooCommerce/Admin/Meta Boxes + * @package WooCommerce\Admin\Meta Boxes * @version 2.1.0 */ diff --git a/includes/admin/meta-boxes/class-wc-meta-box-order-notes.php b/includes/admin/meta-boxes/class-wc-meta-box-order-notes.php index ba34495607e..936a647a1e0 100644 --- a/includes/admin/meta-boxes/class-wc-meta-box-order-notes.php +++ b/includes/admin/meta-boxes/class-wc-meta-box-order-notes.php @@ -2,7 +2,7 @@ /** * Order Notes * - * @package WooCommerce/Admin/Meta Boxes + * @package WooCommerce\Admin\Meta Boxes */ if ( ! defined( 'ABSPATH' ) ) { diff --git a/includes/admin/meta-boxes/class-wc-meta-box-product-data.php b/includes/admin/meta-boxes/class-wc-meta-box-product-data.php index 7bbbab50ab2..c1275acf073 100644 --- a/includes/admin/meta-boxes/class-wc-meta-box-product-data.php +++ b/includes/admin/meta-boxes/class-wc-meta-box-product-data.php @@ -4,7 +4,7 @@ * * Displays the product data box, tabbed, with several panels covering price, stock etc. * - * @package WooCommerce/Admin/Meta Boxes + * @package WooCommerce\Admin\Meta Boxes * @version 3.0.0 */ diff --git a/includes/admin/meta-boxes/class-wc-meta-box-product-images.php b/includes/admin/meta-boxes/class-wc-meta-box-product-images.php index e2553785234..d4a0b5f04dd 100644 --- a/includes/admin/meta-boxes/class-wc-meta-box-product-images.php +++ b/includes/admin/meta-boxes/class-wc-meta-box-product-images.php @@ -6,7 +6,7 @@ * * @author WooThemes * @category Admin - * @package WooCommerce/Admin/Meta Boxes + * @package WooCommerce\Admin\Meta Boxes * @version 2.1.0 */ diff --git a/includes/admin/meta-boxes/class-wc-meta-box-product-reviews.php b/includes/admin/meta-boxes/class-wc-meta-box-product-reviews.php index 2b845692997..28e6a11435b 100644 --- a/includes/admin/meta-boxes/class-wc-meta-box-product-reviews.php +++ b/includes/admin/meta-boxes/class-wc-meta-box-product-reviews.php @@ -4,7 +4,7 @@ * * Functions for displaying product reviews data meta box. * - * @package WooCommerce/Admin/Meta Boxes + * @package WooCommerce\Admin\Meta Boxes */ defined( 'ABSPATH' ) || exit; diff --git a/includes/admin/meta-boxes/class-wc-meta-box-product-short-description.php b/includes/admin/meta-boxes/class-wc-meta-box-product-short-description.php index 736ffcb717a..ab762be2929 100644 --- a/includes/admin/meta-boxes/class-wc-meta-box-product-short-description.php +++ b/includes/admin/meta-boxes/class-wc-meta-box-product-short-description.php @@ -4,7 +4,7 @@ * * Replaces the standard excerpt box. * - * @package WooCommerce/Admin/Meta Boxes + * @package WooCommerce\Admin\Meta Boxes * @version 2.1.0 */ diff --git a/includes/admin/meta-boxes/views/html-order-item.php b/includes/admin/meta-boxes/views/html-order-item.php index 7bee65e2d05..355b9b28a1b 100644 --- a/includes/admin/meta-boxes/views/html-order-item.php +++ b/includes/admin/meta-boxes/views/html-order-item.php @@ -2,7 +2,7 @@ /** * Shows an order item * - * @package WooCommerce/Admin + * @package WooCommerce\Admin * @var object $item The item being displayed * @var int $item_id The id of the item being displayed */ diff --git a/includes/admin/meta-boxes/views/html-order-items.php b/includes/admin/meta-boxes/views/html-order-items.php index f5dd34f972e..d300e782745 100644 --- a/includes/admin/meta-boxes/views/html-order-items.php +++ b/includes/admin/meta-boxes/views/html-order-items.php @@ -2,7 +2,7 @@ /** * Order items HTML for meta box. * - * @package WooCommerce/Admin + * @package WooCommerce\Admin */ defined( 'ABSPATH' ) || exit; diff --git a/includes/admin/meta-boxes/views/html-order-notes.php b/includes/admin/meta-boxes/views/html-order-notes.php index 87015bfa3a4..50db87bcea2 100644 --- a/includes/admin/meta-boxes/views/html-order-notes.php +++ b/includes/admin/meta-boxes/views/html-order-notes.php @@ -2,7 +2,7 @@ /** * Order notes HTML for meta box. * - * @package WooCommerce/Admin + * @package WooCommerce\Admin */ defined( 'ABSPATH' ) || exit; diff --git a/includes/admin/meta-boxes/views/html-order-shipping.php b/includes/admin/meta-boxes/views/html-order-shipping.php index 38bc004b5e8..6350fb1f9f1 100644 --- a/includes/admin/meta-boxes/views/html-order-shipping.php +++ b/includes/admin/meta-boxes/views/html-order-shipping.php @@ -2,12 +2,12 @@ /** * Shows a shipping line * - * @package WooCommerce/Admin + * @package WooCommerce\Admin * * @var object $item The item being displayed * @var int $item_id The id of the item being displayed * - * @package WooCommerce/Admin/Views + * @package WooCommerce\Admin\Views */ if ( ! defined( 'ABSPATH' ) ) { diff --git a/includes/admin/meta-boxes/views/html-product-data-general.php b/includes/admin/meta-boxes/views/html-product-data-general.php index 3b2e476c538..281a3c573d9 100644 --- a/includes/admin/meta-boxes/views/html-product-data-general.php +++ b/includes/admin/meta-boxes/views/html-product-data-general.php @@ -2,7 +2,7 @@ /** * Product general data panel. * - * @package WooCommerce/Admin + * @package WooCommerce\Admin */ defined( 'ABSPATH' ) || exit; diff --git a/includes/admin/meta-boxes/views/html-product-data-linked-products.php b/includes/admin/meta-boxes/views/html-product-data-linked-products.php index 2a7a0938db9..703bce477f4 100644 --- a/includes/admin/meta-boxes/views/html-product-data-linked-products.php +++ b/includes/admin/meta-boxes/views/html-product-data-linked-products.php @@ -2,7 +2,7 @@ /** * Linked product options. * - * @package WooCommerce/admin + * @package WooCommerce\Admin */ defined( 'ABSPATH' ) || exit; diff --git a/includes/admin/meta-boxes/views/html-product-data-panel.php b/includes/admin/meta-boxes/views/html-product-data-panel.php index 5fd93719b65..5393f85d464 100644 --- a/includes/admin/meta-boxes/views/html-product-data-panel.php +++ b/includes/admin/meta-boxes/views/html-product-data-panel.php @@ -2,7 +2,7 @@ /** * Product data meta box. * - * @package WooCommerce/Admin + * @package WooCommerce\Admin */ if ( ! defined( 'ABSPATH' ) ) { diff --git a/includes/admin/plugin-updates/class-wc-plugin-updates.php b/includes/admin/plugin-updates/class-wc-plugin-updates.php index fa64bcce0b4..dd7ce5c1d5b 100644 --- a/includes/admin/plugin-updates/class-wc-plugin-updates.php +++ b/includes/admin/plugin-updates/class-wc-plugin-updates.php @@ -2,7 +2,7 @@ /** * Class for displaying plugin warning notifications and determining 3rd party plugin compatibility. * - * @package WooCommerce/Admin + * @package WooCommerce\Admin * @version 3.2.0 */ diff --git a/includes/admin/plugin-updates/class-wc-plugins-screen-updates.php b/includes/admin/plugin-updates/class-wc-plugins-screen-updates.php index a7a048db15a..ad24e66848e 100644 --- a/includes/admin/plugin-updates/class-wc-plugins-screen-updates.php +++ b/includes/admin/plugin-updates/class-wc-plugins-screen-updates.php @@ -2,7 +2,7 @@ /** * Manages WooCommerce plugin updating on the Plugins screen. * - * @package WooCommerce/Admin + * @package WooCommerce\Admin * @version 3.2.0 */ diff --git a/includes/admin/plugin-updates/class-wc-updates-screen-updates.php b/includes/admin/plugin-updates/class-wc-updates-screen-updates.php index 5e55da51488..60510100610 100644 --- a/includes/admin/plugin-updates/class-wc-updates-screen-updates.php +++ b/includes/admin/plugin-updates/class-wc-updates-screen-updates.php @@ -2,7 +2,7 @@ /** * Manages WooCommerce plugin updating on the Updates screen. * - * @package WooCommerce/Admin + * @package WooCommerce\Admin * @version 3.2.0 */ diff --git a/includes/admin/reports/class-wc-admin-report.php b/includes/admin/reports/class-wc-admin-report.php index ca634b47c6e..d6dceb681f5 100644 --- a/includes/admin/reports/class-wc-admin-report.php +++ b/includes/admin/reports/class-wc-admin-report.php @@ -11,7 +11,7 @@ if ( ! defined( 'ABSPATH' ) ) { * * @author WooThemes * @category Admin - * @package WooCommerce/Admin/Reports + * @package WooCommerce\Admin\Reports * @version 2.1.0 */ class WC_Admin_Report { diff --git a/includes/admin/reports/class-wc-report-coupon-usage.php b/includes/admin/reports/class-wc-report-coupon-usage.php index c04be027c7f..c2bddf2fd26 100644 --- a/includes/admin/reports/class-wc-report-coupon-usage.php +++ b/includes/admin/reports/class-wc-report-coupon-usage.php @@ -2,7 +2,7 @@ /** * Coupon usage report functionality * - * @package WooCommerce/Admin/Reports + * @package WooCommerce\Admin\Reports */ if ( ! defined( 'ABSPATH' ) ) { @@ -14,7 +14,7 @@ if ( ! defined( 'ABSPATH' ) ) { * * @author WooThemes * @category Admin - * @package WooCommerce/Admin/Reports + * @package WooCommerce\Admin\Reports * @version 2.1.0 */ class WC_Report_Coupon_Usage extends WC_Admin_Report { diff --git a/includes/admin/reports/class-wc-report-customer-list.php b/includes/admin/reports/class-wc-report-customer-list.php index 36f47c9b24d..e223570b7b9 100644 --- a/includes/admin/reports/class-wc-report-customer-list.php +++ b/includes/admin/reports/class-wc-report-customer-list.php @@ -16,7 +16,7 @@ if ( ! class_exists( 'WP_List_Table' ) ) { /** * WC_Report_Customer_List. * - * @package WooCommerce/Admin/Reports + * @package WooCommerce\Admin\Reports * @version 2.1.0 */ class WC_Report_Customer_List extends WP_List_Table { diff --git a/includes/admin/reports/class-wc-report-customers.php b/includes/admin/reports/class-wc-report-customers.php index 4b949d2180d..0096a73f993 100644 --- a/includes/admin/reports/class-wc-report-customers.php +++ b/includes/admin/reports/class-wc-report-customers.php @@ -12,7 +12,7 @@ if ( ! defined( 'ABSPATH' ) ) { /** * WC_Report_Customers * - * @package WooCommerce/Admin/Reports + * @package WooCommerce\Admin\Reports * @version 2.1.0 */ class WC_Report_Customers extends WC_Admin_Report { diff --git a/includes/admin/reports/class-wc-report-downloads.php b/includes/admin/reports/class-wc-report-downloads.php index ff326ab01df..fcedb69eecf 100644 --- a/includes/admin/reports/class-wc-report-downloads.php +++ b/includes/admin/reports/class-wc-report-downloads.php @@ -4,7 +4,7 @@ * * @author WooThemes * @category Admin - * @package WooCommerce/Admin/Reports + * @package WooCommerce\Admin\Reports * @version 3.3.0 */ diff --git a/includes/admin/reports/class-wc-report-low-in-stock.php b/includes/admin/reports/class-wc-report-low-in-stock.php index 80ade878d4a..ed69b074bd6 100644 --- a/includes/admin/reports/class-wc-report-low-in-stock.php +++ b/includes/admin/reports/class-wc-report-low-in-stock.php @@ -2,7 +2,7 @@ /** * WC_Report_Low_In_Stock. * - * @package WooCommerce/Admin/Reports + * @package WooCommerce\Admin\Reports */ defined( 'ABSPATH' ) || exit; diff --git a/includes/admin/reports/class-wc-report-most-stocked.php b/includes/admin/reports/class-wc-report-most-stocked.php index 5d018f9df86..26058d43cf4 100644 --- a/includes/admin/reports/class-wc-report-most-stocked.php +++ b/includes/admin/reports/class-wc-report-most-stocked.php @@ -2,7 +2,7 @@ /** * WC_Report_Most_Stocked. * - * @package WooCommerce/Admin/Reports + * @package WooCommerce\Admin\Reports */ defined( 'ABSPATH' ) || exit; diff --git a/includes/admin/reports/class-wc-report-out-of-stock.php b/includes/admin/reports/class-wc-report-out-of-stock.php index 1e4a9940250..9eb594ad9a0 100644 --- a/includes/admin/reports/class-wc-report-out-of-stock.php +++ b/includes/admin/reports/class-wc-report-out-of-stock.php @@ -2,7 +2,7 @@ /** * WC_Report_Out_Of_Stock. * - * @package WooCommerce/Admin/Reports + * @package WooCommerce\Admin\Reports */ defined( 'ABSPATH' ) || exit; diff --git a/includes/admin/reports/class-wc-report-sales-by-category.php b/includes/admin/reports/class-wc-report-sales-by-category.php index 2a445eedea2..390e9de2c24 100644 --- a/includes/admin/reports/class-wc-report-sales-by-category.php +++ b/includes/admin/reports/class-wc-report-sales-by-category.php @@ -2,7 +2,7 @@ /** * Sales by category report functionality * - * @package WooCommerce/Admin/Reporting + * @package WooCommerce\Admin\Reporting */ if ( ! defined( 'ABSPATH' ) ) { @@ -14,7 +14,7 @@ if ( ! defined( 'ABSPATH' ) ) { * * @author WooThemes * @category Admin - * @package WooCommerce/Admin/Reports + * @package WooCommerce\Admin\Reports * @version 2.1.0 */ class WC_Report_Sales_By_Category extends WC_Admin_Report { diff --git a/includes/admin/reports/class-wc-report-sales-by-date.php b/includes/admin/reports/class-wc-report-sales-by-date.php index ae0b41146c7..10938ee633d 100644 --- a/includes/admin/reports/class-wc-report-sales-by-date.php +++ b/includes/admin/reports/class-wc-report-sales-by-date.php @@ -2,7 +2,7 @@ /** * WC_Report_Sales_By_Date * - * @package WooCommerce/Admin/Reports + * @package WooCommerce\Admin\Reports * @version 2.1.0 */ diff --git a/includes/admin/reports/class-wc-report-sales-by-product.php b/includes/admin/reports/class-wc-report-sales-by-product.php index f8f666252d6..69ae0b4c4a7 100644 --- a/includes/admin/reports/class-wc-report-sales-by-product.php +++ b/includes/admin/reports/class-wc-report-sales-by-product.php @@ -2,7 +2,7 @@ /** * Sales By Product Reporting * - * @package WooCommerce/Admin/Reporting + * @package WooCommerce\Admin\Reporting */ if ( ! defined( 'ABSPATH' ) ) { @@ -12,7 +12,7 @@ if ( ! defined( 'ABSPATH' ) ) { /** * WC_Report_Sales_By_Product * - * @package WooCommerce/Admin/Reports + * @package WooCommerce\Admin\Reports * @version 2.1.0 */ class WC_Report_Sales_By_Product extends WC_Admin_Report { diff --git a/includes/admin/reports/class-wc-report-stock.php b/includes/admin/reports/class-wc-report-stock.php index e9b6a4c93cd..e656ae0156c 100644 --- a/includes/admin/reports/class-wc-report-stock.php +++ b/includes/admin/reports/class-wc-report-stock.php @@ -13,7 +13,7 @@ if ( ! class_exists( 'WP_List_Table' ) ) { * * @author WooThemes * @category Admin - * @package WooCommerce/Admin/Reports + * @package WooCommerce\Admin\Reports * @version 2.1.0 */ class WC_Report_Stock extends WP_List_Table { diff --git a/includes/admin/reports/class-wc-report-taxes-by-code.php b/includes/admin/reports/class-wc-report-taxes-by-code.php index 024b4c3c383..a76375904e1 100644 --- a/includes/admin/reports/class-wc-report-taxes-by-code.php +++ b/includes/admin/reports/class-wc-report-taxes-by-code.php @@ -2,7 +2,7 @@ /** * Taxes by tax code report. * - * @package WooCommerce/Admin/Reports + * @package WooCommerce\Admin\Reports */ if ( ! defined( 'ABSPATH' ) ) { @@ -12,7 +12,7 @@ if ( ! defined( 'ABSPATH' ) ) { /** * WC_Report_Taxes_By_Code * - * @package WooCommerce/Admin/Reports + * @package WooCommerce\Admin\Reports * @version 2.1.0 */ class WC_Report_Taxes_By_Code extends WC_Admin_Report { diff --git a/includes/admin/reports/class-wc-report-taxes-by-date.php b/includes/admin/reports/class-wc-report-taxes-by-date.php index cd52454c982..37328325bda 100644 --- a/includes/admin/reports/class-wc-report-taxes-by-date.php +++ b/includes/admin/reports/class-wc-report-taxes-by-date.php @@ -9,7 +9,7 @@ if ( ! defined( 'ABSPATH' ) ) { * * @author WooThemes * @category Admin - * @package WooCommerce/Admin/Reports + * @package WooCommerce\Admin\Reports * @version 2.1.0 */ class WC_Report_Taxes_By_Date extends WC_Admin_Report { diff --git a/includes/admin/settings/class-wc-settings-accounts.php b/includes/admin/settings/class-wc-settings-accounts.php index 3ecf58c01b1..73802e61bdb 100644 --- a/includes/admin/settings/class-wc-settings-accounts.php +++ b/includes/admin/settings/class-wc-settings-accounts.php @@ -2,7 +2,7 @@ /** * WooCommerce Account Settings. * - * @package WooCommerce/Admin + * @package WooCommerce\Admin */ defined( 'ABSPATH' ) || exit; diff --git a/includes/admin/settings/class-wc-settings-advanced.php b/includes/admin/settings/class-wc-settings-advanced.php index 5056d4fdba8..13896e8a27c 100644 --- a/includes/admin/settings/class-wc-settings-advanced.php +++ b/includes/admin/settings/class-wc-settings-advanced.php @@ -2,7 +2,7 @@ /** * WooCommerce advanced settings * - * @package WooCommerce/Admin + * @package WooCommerce\Admin */ defined( 'ABSPATH' ) || exit; diff --git a/includes/admin/settings/class-wc-settings-emails.php b/includes/admin/settings/class-wc-settings-emails.php index 00df3150196..9b4c524ebc5 100644 --- a/includes/admin/settings/class-wc-settings-emails.php +++ b/includes/admin/settings/class-wc-settings-emails.php @@ -2,7 +2,7 @@ /** * WooCommerce Email Settings * - * @package WooCommerce/Admin + * @package WooCommerce\Admin * @version 2.1.0 */ diff --git a/includes/admin/settings/class-wc-settings-general.php b/includes/admin/settings/class-wc-settings-general.php index 6e0126b00da..8535dd7f956 100644 --- a/includes/admin/settings/class-wc-settings-general.php +++ b/includes/admin/settings/class-wc-settings-general.php @@ -2,7 +2,7 @@ /** * WooCommerce General Settings * - * @package WooCommerce/Admin + * @package WooCommerce\Admin */ defined( 'ABSPATH' ) || exit; diff --git a/includes/admin/settings/class-wc-settings-integrations.php b/includes/admin/settings/class-wc-settings-integrations.php index 248287f059f..b5859b6d155 100644 --- a/includes/admin/settings/class-wc-settings-integrations.php +++ b/includes/admin/settings/class-wc-settings-integrations.php @@ -4,7 +4,7 @@ * * @author WooThemes * @category Admin - * @package WooCommerce/Admin + * @package WooCommerce\Admin * @version 2.1.0 */ diff --git a/includes/admin/settings/class-wc-settings-page.php b/includes/admin/settings/class-wc-settings-page.php index 90b8cc8090a..b652182da23 100644 --- a/includes/admin/settings/class-wc-settings-page.php +++ b/includes/admin/settings/class-wc-settings-page.php @@ -4,7 +4,7 @@ * * @author WooThemes * @category Admin - * @package WooCommerce/Admin + * @package WooCommerce\Admin * @version 2.1.0 */ diff --git a/includes/admin/settings/class-wc-settings-payment-gateways.php b/includes/admin/settings/class-wc-settings-payment-gateways.php index 48c67e90a87..463f7ad9154 100644 --- a/includes/admin/settings/class-wc-settings-payment-gateways.php +++ b/includes/admin/settings/class-wc-settings-payment-gateways.php @@ -2,7 +2,7 @@ /** * WooCommerce Checkout Settings * - * @package WooCommerce/Admin + * @package WooCommerce\Admin */ defined( 'ABSPATH' ) || exit; diff --git a/includes/admin/settings/class-wc-settings-products.php b/includes/admin/settings/class-wc-settings-products.php index 6c7d885a1db..35a63f78b02 100644 --- a/includes/admin/settings/class-wc-settings-products.php +++ b/includes/admin/settings/class-wc-settings-products.php @@ -2,7 +2,7 @@ /** * WooCommerce Product Settings * - * @package WooCommerce/Admin + * @package WooCommerce\Admin * @version 2.4.0 */ diff --git a/includes/admin/settings/class-wc-settings-shipping.php b/includes/admin/settings/class-wc-settings-shipping.php index 11e66d59a5d..79183328c30 100644 --- a/includes/admin/settings/class-wc-settings-shipping.php +++ b/includes/admin/settings/class-wc-settings-shipping.php @@ -2,7 +2,7 @@ /** * WooCommerce Shipping Settings * - * @package WooCommerce/Admin + * @package WooCommerce\Admin * @version 2.6.0 */ diff --git a/includes/admin/settings/class-wc-settings-tax.php b/includes/admin/settings/class-wc-settings-tax.php index 739674e7092..8f489ffd6cd 100644 --- a/includes/admin/settings/class-wc-settings-tax.php +++ b/includes/admin/settings/class-wc-settings-tax.php @@ -4,7 +4,7 @@ * * @author WooThemes * @category Admin - * @package WooCommerce/Admin + * @package WooCommerce\Admin * @version 2.1.0 */ diff --git a/includes/admin/settings/views/html-admin-page-shipping-classes.php b/includes/admin/settings/views/html-admin-page-shipping-classes.php index 33d8f471201..ed2c906409a 100644 --- a/includes/admin/settings/views/html-admin-page-shipping-classes.php +++ b/includes/admin/settings/views/html-admin-page-shipping-classes.php @@ -2,7 +2,7 @@ /** * Shipping classes admin * - * @package WooCommerce/Admin/Shipping + * @package WooCommerce\Admin\Shipping */ if ( ! defined( 'ABSPATH' ) ) { diff --git a/includes/admin/settings/views/html-admin-page-shipping-zone-methods.php b/includes/admin/settings/views/html-admin-page-shipping-zone-methods.php index 8bc70e58312..d2af3e72481 100644 --- a/includes/admin/settings/views/html-admin-page-shipping-zone-methods.php +++ b/includes/admin/settings/views/html-admin-page-shipping-zone-methods.php @@ -2,7 +2,7 @@ /** * Shipping zone admin * - * @package WooCommerce/Admin/Shipping + * @package WooCommerce\Admin\Shipping */ if ( ! defined( 'ABSPATH' ) ) { diff --git a/includes/admin/settings/views/html-keys-edit.php b/includes/admin/settings/views/html-keys-edit.php index 7f317a7ced9..04da58e51cf 100644 --- a/includes/admin/settings/views/html-keys-edit.php +++ b/includes/admin/settings/views/html-keys-edit.php @@ -2,7 +2,7 @@ /** * Admin view: Edit API keys * - * @package WooCommerce/Admin/Settings + * @package WooCommerce\Admin\Settings */ defined( 'ABSPATH' ) || exit; diff --git a/includes/admin/settings/views/html-settings-tax.php b/includes/admin/settings/views/html-settings-tax.php index 98b6e910527..15a7a95cfbd 100644 --- a/includes/admin/settings/views/html-settings-tax.php +++ b/includes/admin/settings/views/html-settings-tax.php @@ -1,4 +1,10 @@ is_rest_api_loaded() ) { return null; } + if ( method_exists( \Automattic\WooCommerce\RestApi\Server::class, 'get_path' ) ) { + $path = \Automattic\WooCommerce\RestApi\Server::get_path(); + if ( 0 === strpos( $path, __DIR__ ) ) { + // We are loading API from included version. + return WC()->version; + } + } + // We are loading API from external plugin. return \Automattic\WooCommerce\RestApi\Package::get_version(); } @@ -52,6 +60,11 @@ class WC_API extends WC_Legacy_API { if ( ! $this->is_rest_api_loaded() ) { return null; } + if ( method_exists( \Automattic\WooCommerce\RestApi\Server::class, 'get_path' ) ) { + // We are loading API from included version. + return \Automattic\WooCommerce\RestApi\Server::get_path(); + } + // We are loading API from external plugin. return \Automattic\WooCommerce\RestApi\Package::get_path(); } @@ -62,7 +75,7 @@ class WC_API extends WC_Legacy_API { * @return boolean */ protected function is_rest_api_loaded() { - return class_exists( '\Automattic\WooCommerce\RestApi\Package', false ); + return class_exists( '\Automattic\WooCommerce\RestApi\Server', false ); } /** diff --git a/includes/class-wc-auth.php b/includes/class-wc-auth.php index b6754497b69..6afa9bfa392 100644 --- a/includes/class-wc-auth.php +++ b/includes/class-wc-auth.php @@ -4,7 +4,7 @@ * * Handles wc-auth endpoint requests. * - * @package WooCommerce/API + * @package WooCommerce\API * @since 2.4.0 */ diff --git a/includes/class-wc-autoloader.php b/includes/class-wc-autoloader.php index 480eddc3f5d..03153518d2c 100644 --- a/includes/class-wc-autoloader.php +++ b/includes/class-wc-autoloader.php @@ -2,7 +2,7 @@ /** * WooCommerce Autoloader. * - * @package WooCommerce/Classes + * @package WooCommerce\Classes * @version 2.3.0 */ diff --git a/includes/class-wc-background-emailer.php b/includes/class-wc-background-emailer.php index 331fa87a27f..5c9d751e07f 100644 --- a/includes/class-wc-background-emailer.php +++ b/includes/class-wc-background-emailer.php @@ -3,7 +3,7 @@ * Background Emailer * * @version 3.0.1 - * @package WooCommerce/Classes + * @package WooCommerce\Classes */ use Automattic\Jetpack\Constants; diff --git a/includes/class-wc-background-updater.php b/includes/class-wc-background-updater.php index a1253f6c4a2..424bb93262f 100644 --- a/includes/class-wc-background-updater.php +++ b/includes/class-wc-background-updater.php @@ -4,7 +4,7 @@ * * @version 2.6.0 * @deprecated 3.6.0 Replaced with queue. - * @package WooCommerce/Classes + * @package WooCommerce\Classes */ defined( 'ABSPATH' ) || exit; diff --git a/includes/class-wc-breadcrumb.php b/includes/class-wc-breadcrumb.php index 1a3aa6dc909..592fe86c53f 100644 --- a/includes/class-wc-breadcrumb.php +++ b/includes/class-wc-breadcrumb.php @@ -2,7 +2,7 @@ /** * WC_Breadcrumb class. * - * @package WooCommerce/Classes + * @package WooCommerce\Classes * @version 2.3.0 */ diff --git a/includes/class-wc-cache-helper.php b/includes/class-wc-cache-helper.php index e526659cbea..781fd735bea 100644 --- a/includes/class-wc-cache-helper.php +++ b/includes/class-wc-cache-helper.php @@ -2,7 +2,7 @@ /** * WC_Cache_Helper class. * - * @package WooCommerce/Classes + * @package WooCommerce\Classes */ defined( 'ABSPATH' ) || exit; diff --git a/includes/class-wc-cart-fees.php b/includes/class-wc-cart-fees.php index 6f5a29f5203..f80376f996a 100644 --- a/includes/class-wc-cart-fees.php +++ b/includes/class-wc-cart-fees.php @@ -6,7 +6,7 @@ * * We suggest using the action woocommerce_cart_calculate_fees hook for adding fees. * - * @package WooCommerce/Classes + * @package WooCommerce\Classes * @version 3.2.0 */ diff --git a/includes/class-wc-cart-session.php b/includes/class-wc-cart-session.php index 2bb0db176f6..d8eb30ca8e9 100644 --- a/includes/class-wc-cart-session.php +++ b/includes/class-wc-cart-session.php @@ -2,7 +2,7 @@ /** * Cart session handling class. * - * @package WooCommerce/Classes + * @package WooCommerce\Classes * @version 3.2.0 */ diff --git a/includes/class-wc-cart-totals.php b/includes/class-wc-cart-totals.php index 1ab3a15f5a9..a08c1847416 100644 --- a/includes/class-wc-cart-totals.php +++ b/includes/class-wc-cart-totals.php @@ -9,7 +9,7 @@ * - if something is being stored e.g. item total, store unrounded. This is so taxes can be recalculated later accurately. * - if calculating a total, round (if settings allow). * - * @package WooCommerce/Classes + * @package WooCommerce\Classes * @version 3.2.0 */ diff --git a/includes/class-wc-cart.php b/includes/class-wc-cart.php index 3b086137acd..9e2540a9120 100644 --- a/includes/class-wc-cart.php +++ b/includes/class-wc-cart.php @@ -5,7 +5,7 @@ * The WooCommerce cart class stores cart data and active coupons as well as handling customer sessions and some cart related urls. * The cart class also has a price calculation function which calls upon other classes to calculate totals. * - * @package WooCommerce/Classes + * @package WooCommerce\Classes * @version 2.1.0 */ @@ -1691,7 +1691,7 @@ class WC_Cart extends WC_Legacy_Cart { */ public function remove_coupon( $coupon_code ) { $coupon_code = wc_format_coupon_code( $coupon_code ); - $position = array_search( $coupon_code, $this->get_applied_coupons(), true ); + $position = array_search( $coupon_code, array_map( 'wc_format_coupon_code', $this->get_applied_coupons() ), true ); if ( false !== $position ) { unset( $this->applied_coupons[ $position ] ); diff --git a/includes/class-wc-checkout.php b/includes/class-wc-checkout.php index 955efb49726..195c8c2d7e5 100644 --- a/includes/class-wc-checkout.php +++ b/includes/class-wc-checkout.php @@ -4,7 +4,7 @@ * * The WooCommerce checkout class handles the checkout process, collecting user data and processing the payment. * - * @package WooCommerce/Classes + * @package WooCommerce\Classes * @version 3.4.0 */ diff --git a/includes/class-wc-comments.php b/includes/class-wc-comments.php index ab4e6945f01..2900e97d5e2 100644 --- a/includes/class-wc-comments.php +++ b/includes/class-wc-comments.php @@ -4,7 +4,7 @@ * * Handle comments (reviews and order notes). * - * @package WooCommerce/Classes/Products + * @package WooCommerce\Classes\Products * @version 2.3.0 */ diff --git a/includes/class-wc-coupon.php b/includes/class-wc-coupon.php index 5ca6867a5a8..9ef9e02319a 100644 --- a/includes/class-wc-coupon.php +++ b/includes/class-wc-coupon.php @@ -4,7 +4,7 @@ * * The WooCommerce coupons class gets coupon data from storage and checks coupon validity. * - * @package WooCommerce/Classes + * @package WooCommerce\Classes * @version 3.0.0 */ diff --git a/includes/class-wc-customer-download-log.php b/includes/class-wc-customer-download-log.php index 37d6e1ea23f..cec6c05095a 100644 --- a/includes/class-wc-customer-download-log.php +++ b/includes/class-wc-customer-download-log.php @@ -2,7 +2,7 @@ /** * Class for customer download logs. * - * @package WooCommerce/Classes + * @package WooCommerce\Classes * @version 3.3.0 * @since 3.3.0 */ diff --git a/includes/class-wc-customer-download.php b/includes/class-wc-customer-download.php index 26dadb0f46d..1e1a6d35323 100644 --- a/includes/class-wc-customer-download.php +++ b/includes/class-wc-customer-download.php @@ -2,7 +2,7 @@ /** * Class for customer download permissions. * - * @package WooCommerce/Classes + * @package WooCommerce\Classes * @version 3.0.0 * @since 3.0.0 */ diff --git a/includes/class-wc-customer.php b/includes/class-wc-customer.php index 4f338a9dd40..cb648fc83da 100644 --- a/includes/class-wc-customer.php +++ b/includes/class-wc-customer.php @@ -2,7 +2,7 @@ /** * The WooCommerce customer class handles storage of the current customer's data, such as location. * - * @package WooCommerce/Classes + * @package WooCommerce\Classes * @version 3.0.0 */ diff --git a/includes/class-wc-datetime.php b/includes/class-wc-datetime.php index 233f7953aba..1778503d5d1 100644 --- a/includes/class-wc-datetime.php +++ b/includes/class-wc-datetime.php @@ -4,7 +4,7 @@ * timezone is absent * * @since 3.0.0 - * @package WooCommerce/Classes + * @package WooCommerce\Classes */ defined( 'ABSPATH' ) || exit; diff --git a/includes/class-wc-discounts.php b/includes/class-wc-discounts.php index 2a11b517510..f1086810f78 100644 --- a/includes/class-wc-discounts.php +++ b/includes/class-wc-discounts.php @@ -2,7 +2,7 @@ /** * Discount calculation * - * @package WooCommerce/Classes + * @package WooCommerce\Classes * @since 3.2.0 */ diff --git a/includes/class-wc-download-handler.php b/includes/class-wc-download-handler.php index 1d4ae53abe5..a2826527f43 100644 --- a/includes/class-wc-download-handler.php +++ b/includes/class-wc-download-handler.php @@ -4,7 +4,7 @@ * * Handle digital downloads. * - * @package WooCommerce/Classes + * @package WooCommerce\Classes * @version 2.2.0 */ diff --git a/includes/class-wc-emails.php b/includes/class-wc-emails.php index 2d2f4d24c8d..a097a629e75 100644 --- a/includes/class-wc-emails.php +++ b/includes/class-wc-emails.php @@ -4,7 +4,7 @@ * * WooCommerce Emails Class which handles the sending on transactional emails and email templates. This class loads in available emails. * - * @package WooCommerce/Classes/Emails + * @package WooCommerce\Classes\Emails * @version 2.3.0 */ diff --git a/includes/class-wc-embed.php b/includes/class-wc-embed.php index 296e89c7040..da14279a749 100644 --- a/includes/class-wc-embed.php +++ b/includes/class-wc-embed.php @@ -3,7 +3,7 @@ * WooCommerce product embed * * @version 2.4.11 - * @package WooCommerce/Classes/Embed + * @package WooCommerce\Classes\Embed */ if ( ! defined( 'ABSPATH' ) ) { diff --git a/includes/class-wc-form-handler.php b/includes/class-wc-form-handler.php index 80a4336aba5..e64a0b3fcb6 100644 --- a/includes/class-wc-form-handler.php +++ b/includes/class-wc-form-handler.php @@ -2,7 +2,7 @@ /** * Handle frontend forms. * - * @package WooCommerce/Classes/ + * @package WooCommerce\Classes\ */ defined( 'ABSPATH' ) || exit; diff --git a/includes/class-wc-frontend-scripts.php b/includes/class-wc-frontend-scripts.php index 935f0696803..ffd18d99dea 100644 --- a/includes/class-wc-frontend-scripts.php +++ b/includes/class-wc-frontend-scripts.php @@ -2,7 +2,7 @@ /** * Handle frontend scripts * - * @package WooCommerce/Classes + * @package WooCommerce\Classes * @version 2.3.0 */ diff --git a/includes/class-wc-geo-ip.php b/includes/class-wc-geo-ip.php index 9aeb5afc1c2..c79be25b985 100644 --- a/includes/class-wc-geo-ip.php +++ b/includes/class-wc-geo-ip.php @@ -4,7 +4,7 @@ * * This class is a fork of GeoIP class from MaxMind LLC. * - * @package WooCommerce/Classes + * @package WooCommerce\Classes * @version 2.4.0 * @deprecated 3.4.0 */ diff --git a/includes/class-wc-geolocation.php b/includes/class-wc-geolocation.php index 64cf628f956..97d918404ab 100644 --- a/includes/class-wc-geolocation.php +++ b/includes/class-wc-geolocation.php @@ -6,7 +6,7 @@ * * This product includes GeoLite data created by MaxMind, available from http://www.maxmind.com. * - * @package WooCommerce/Classes + * @package WooCommerce\Classes * @version 3.9.0 */ diff --git a/includes/class-wc-https.php b/includes/class-wc-https.php index de3e6f4ce85..84e3aeddd7b 100644 --- a/includes/class-wc-https.php +++ b/includes/class-wc-https.php @@ -9,7 +9,7 @@ if ( ! defined( 'ABSPATH' ) ) { * * @class WC_HTTPS * @version 2.2.0 - * @package WooCommerce/Classes + * @package WooCommerce\Classes * @category Class * @author WooThemes */ diff --git a/includes/class-wc-install.php b/includes/class-wc-install.php index 9a58422fbaa..6a45e7b726a 100644 --- a/includes/class-wc-install.php +++ b/includes/class-wc-install.php @@ -2,7 +2,7 @@ /** * Installation related functions and actions. * - * @package WooCommerce/Classes + * @package WooCommerce\Classes * @version 3.0.0 */ @@ -153,6 +153,10 @@ class WC_Install { 'wc_update_440_insert_attribute_terms_for_variable_products', 'wc_update_440_db_version', ), + '4.5.0' => array( + 'wc_update_450_sanitize_coupons_code', + 'wc_update_450_db_version', + ), ); /** diff --git a/includes/class-wc-integrations.php b/includes/class-wc-integrations.php index 13d906060a2..f29360b011b 100644 --- a/includes/class-wc-integrations.php +++ b/includes/class-wc-integrations.php @@ -5,7 +5,7 @@ * Loads Integrations into WooCommerce. * * @version 3.9.0 - * @package WooCommerce/Classes/Integrations + * @package WooCommerce\Classes\Integrations */ defined( 'ABSPATH' ) || exit; diff --git a/includes/class-wc-log-levels.php b/includes/class-wc-log-levels.php index 0f635de92bd..f577b55c4c6 100644 --- a/includes/class-wc-log-levels.php +++ b/includes/class-wc-log-levels.php @@ -3,7 +3,7 @@ * Standard log levels * * @version 3.2.0 - * @package WooCommerce/Classes + * @package WooCommerce\Classes */ defined( 'ABSPATH' ) || exit; diff --git a/includes/class-wc-logger.php b/includes/class-wc-logger.php index 41bb7f6db2e..040dd580529 100644 --- a/includes/class-wc-logger.php +++ b/includes/class-wc-logger.php @@ -4,7 +4,7 @@ * * @class WC_Logger * @version 2.0.0 - * @package WooCommerce/Classes + * @package WooCommerce\Classes */ use Automattic\Jetpack\Constants; diff --git a/includes/class-wc-order-factory.php b/includes/class-wc-order-factory.php index bc67f4b8a03..a3077871813 100644 --- a/includes/class-wc-order-factory.php +++ b/includes/class-wc-order-factory.php @@ -5,7 +5,7 @@ * The WooCommerce order factory creating the right order objects. * * @version 3.0.0 - * @package WooCommerce/Classes + * @package WooCommerce\Classes */ defined( 'ABSPATH' ) || exit; diff --git a/includes/class-wc-order-item-coupon.php b/includes/class-wc-order-item-coupon.php index 5aada6cb307..1a8aa50702a 100644 --- a/includes/class-wc-order-item-coupon.php +++ b/includes/class-wc-order-item-coupon.php @@ -2,7 +2,7 @@ /** * Order Line Item (coupon) * - * @package WooCommerce/Classes + * @package WooCommerce\Classes * @version 3.0.0 * @since 3.0.0 */ diff --git a/includes/class-wc-order-item-fee.php b/includes/class-wc-order-item-fee.php index 7b280f05795..44e1ee12d2c 100644 --- a/includes/class-wc-order-item-fee.php +++ b/includes/class-wc-order-item-fee.php @@ -5,7 +5,7 @@ * Fee is an amount of money charged for a particular piece of work * or for a particular right or service, and not supposed to be negative. * - * @package WooCommerce/Classes + * @package WooCommerce\Classes * @version 3.0.0 * @since 3.0.0 */ diff --git a/includes/class-wc-order-item-meta.php b/includes/class-wc-order-item-meta.php index 2d2f72f03f9..693be232328 100644 --- a/includes/class-wc-order-item-meta.php +++ b/includes/class-wc-order-item-meta.php @@ -4,7 +4,7 @@ * * A Simple class for managing order item meta so plugins add it in the correct format. * - * @package WooCommerce/Classes + * @package WooCommerce\Classes * @deprecated 3.0.0 wc_display_item_meta function is used instead. * @version 2.4 */ diff --git a/includes/class-wc-order-item-product.php b/includes/class-wc-order-item-product.php index 24e8aab66ce..a32a6268357 100644 --- a/includes/class-wc-order-item-product.php +++ b/includes/class-wc-order-item-product.php @@ -2,7 +2,7 @@ /** * Order Line Item (product) * - * @package WooCommerce/Classes + * @package WooCommerce\Classes * @version 3.0.0 * @since 3.0.0 */ diff --git a/includes/class-wc-order-item-shipping.php b/includes/class-wc-order-item-shipping.php index c5ca078127d..6602e87ae24 100644 --- a/includes/class-wc-order-item-shipping.php +++ b/includes/class-wc-order-item-shipping.php @@ -2,7 +2,7 @@ /** * Order Line Item (shipping) * - * @package WooCommerce/Classes + * @package WooCommerce\Classes * @version 3.0.0 * @since 3.0.0 */ diff --git a/includes/class-wc-order-item-tax.php b/includes/class-wc-order-item-tax.php index ac34b61eccb..c41e3881f9f 100644 --- a/includes/class-wc-order-item-tax.php +++ b/includes/class-wc-order-item-tax.php @@ -2,7 +2,7 @@ /** * Order Line Item (tax) * - * @package WooCommerce/Classes + * @package WooCommerce\Classes * @version 3.0.0 * @since 3.0.0 */ diff --git a/includes/class-wc-order-item.php b/includes/class-wc-order-item.php index 6bc4df36c23..e7c916157cb 100644 --- a/includes/class-wc-order-item.php +++ b/includes/class-wc-order-item.php @@ -5,7 +5,7 @@ * A class which represents an item within an order and handles CRUD. * Uses ArrayAccess to be BW compatible with WC_Orders::get_items(). * - * @package WooCommerce/Classes + * @package WooCommerce\Classes * @version 3.0.0 * @since 3.0.0 */ diff --git a/includes/class-wc-order-query.php b/includes/class-wc-order-query.php index 8069ddd5e4d..4f481c8d0ef 100644 --- a/includes/class-wc-order-query.php +++ b/includes/class-wc-order-query.php @@ -3,7 +3,7 @@ * Parameter-based Order querying * Args and usage: https://github.com/woocommerce/woocommerce/wiki/wc_get_orders-and-WC_Order_Query * - * @package WooCommerce/Classes + * @package WooCommerce\Classes * @version 3.1.0 * @since 3.1.0 */ diff --git a/includes/class-wc-order-refund.php b/includes/class-wc-order-refund.php index d5a8518e5be..c384d044d65 100644 --- a/includes/class-wc-order-refund.php +++ b/includes/class-wc-order-refund.php @@ -4,7 +4,7 @@ * contain much of the same data. * * @version 3.0.0 - * @package WooCommerce/Classes + * @package WooCommerce\Classes */ defined( 'ABSPATH' ) || exit; diff --git a/includes/class-wc-payment-gateways.php b/includes/class-wc-payment-gateways.php index b7701cc758f..c6114ef68c7 100644 --- a/includes/class-wc-payment-gateways.php +++ b/includes/class-wc-payment-gateways.php @@ -5,7 +5,7 @@ * Loads payment gateways via hooks for use in the store. * * @version 2.2.0 - * @package WooCommerce/Classes/Payment + * @package WooCommerce\Classes\Payment */ defined( 'ABSPATH' ) || exit; diff --git a/includes/class-wc-payment-tokens.php b/includes/class-wc-payment-tokens.php index 8b56f5b4a87..85625d02acc 100644 --- a/includes/class-wc-payment-tokens.php +++ b/includes/class-wc-payment-tokens.php @@ -4,7 +4,7 @@ * * An API for storing and managing tokens for gateways and customers. * - * @package WooCommerce/Classes + * @package WooCommerce\Classes * @version 3.0.0 * @since 2.6.0 */ diff --git a/includes/class-wc-post-data.php b/includes/class-wc-post-data.php index cffabae2f38..39b89ba29b9 100644 --- a/includes/class-wc-post-data.php +++ b/includes/class-wc-post-data.php @@ -4,7 +4,7 @@ * * Standardises certain post data on save. * - * @package WooCommerce/Classes/Data + * @package WooCommerce\Classes\Data * @version 2.2.0 */ @@ -255,6 +255,9 @@ class WC_Post_Data { } } elseif ( 'product' === $data['post_type'] && 'auto-draft' === $data['post_status'] ) { $data['post_title'] = 'AUTO-DRAFT'; + } elseif ( 'shop_coupon' === $data['post_type'] ) { + // Coupons should never allow unfiltered HTML. + $data['post_title'] = wp_filter_kses( $data['post_title'] ); } return $data; diff --git a/includes/class-wc-post-types.php b/includes/class-wc-post-types.php index 36382e5fae3..79c65823ccc 100644 --- a/includes/class-wc-post-types.php +++ b/includes/class-wc-post-types.php @@ -4,7 +4,7 @@ * * Registers post types and taxonomies. * - * @package WooCommerce/Classes/Products + * @package WooCommerce\Classes\Products * @version 2.5.0 */ diff --git a/includes/class-wc-privacy-background-process.php b/includes/class-wc-privacy-background-process.php index 468b214bce0..f75fa4261da 100644 --- a/includes/class-wc-privacy-background-process.php +++ b/includes/class-wc-privacy-background-process.php @@ -2,7 +2,7 @@ /** * Order cleanup background process. * - * @package WooCommerce/Classes + * @package WooCommerce\Classes * @version 3.4.0 * @since 3.4.0 */ diff --git a/includes/class-wc-product-attribute.php b/includes/class-wc-product-attribute.php index 228dae1a319..14f1901c7aa 100644 --- a/includes/class-wc-product-attribute.php +++ b/includes/class-wc-product-attribute.php @@ -5,7 +5,7 @@ * Attributes can be global (taxonomy based) or local to the product itself. * Uses ArrayAccess to be BW compatible with previous ways of reading attributes. * - * @package WooCommerce/Classes + * @package WooCommerce\Classes * @version 3.0.0 * @since 3.0.0 */ diff --git a/includes/class-wc-product-download.php b/includes/class-wc-product-download.php index 96711bc4807..1c13bf1b3b4 100644 --- a/includes/class-wc-product-download.php +++ b/includes/class-wc-product-download.php @@ -2,7 +2,7 @@ /** * Represents a file which can be downloaded. * - * @package WooCommerce/Classes + * @package WooCommerce\Classes * @version 3.0.0 * @since 3.0.0 */ diff --git a/includes/class-wc-product-external.php b/includes/class-wc-product-external.php index b99f10b90bc..f88d45fb5c6 100644 --- a/includes/class-wc-product-external.php +++ b/includes/class-wc-product-external.php @@ -4,7 +4,7 @@ * * External products cannot be bought; they link offsite. Extends simple products. * - * @package WooCommerce/Classes/Products + * @package WooCommerce\Classes\Products * @version 3.0.0 */ diff --git a/includes/class-wc-product-factory.php b/includes/class-wc-product-factory.php index 02bd8d1cd5a..9344f675310 100644 --- a/includes/class-wc-product-factory.php +++ b/includes/class-wc-product-factory.php @@ -4,7 +4,7 @@ * * The WooCommerce product factory creating the right product object. * - * @package WooCommerce/Classes + * @package WooCommerce\Classes * @version 3.0.0 */ diff --git a/includes/class-wc-product-query.php b/includes/class-wc-product-query.php index 1de8bf210d5..8562a1fc1e9 100644 --- a/includes/class-wc-product-query.php +++ b/includes/class-wc-product-query.php @@ -4,7 +4,7 @@ * * Args and usage: https://github.com/woocommerce/woocommerce/wiki/wc_get_products-and-WC_Product_Query * - * @package WooCommerce/Classes + * @package WooCommerce\Classes * @version 3.2.0 * @since 3.2.0 */ diff --git a/includes/class-wc-product-simple.php b/includes/class-wc-product-simple.php index 5a828ec0d17..6dfebfc5191 100644 --- a/includes/class-wc-product-simple.php +++ b/includes/class-wc-product-simple.php @@ -4,7 +4,7 @@ * * The default product type kinda product. * - * @package WooCommerce/Classes/Products + * @package WooCommerce\Classes\Products */ defined( 'ABSPATH' ) || exit; diff --git a/includes/class-wc-product-variable.php b/includes/class-wc-product-variable.php index 93c1c237724..135705e81d5 100644 --- a/includes/class-wc-product-variable.php +++ b/includes/class-wc-product-variable.php @@ -5,7 +5,7 @@ * The WooCommerce product class handles individual product data. * * @version 3.0.0 - * @package WooCommerce/Classes/Products + * @package WooCommerce\Classes\Products */ defined( 'ABSPATH' ) || exit; diff --git a/includes/class-wc-product-variation.php b/includes/class-wc-product-variation.php index f574ba1fd01..2204f85d963 100644 --- a/includes/class-wc-product-variation.php +++ b/includes/class-wc-product-variation.php @@ -4,7 +4,7 @@ * * The WooCommerce product variation class handles product variation data. * - * @package WooCommerce/Classes + * @package WooCommerce\Classes * @version 3.0.0 */ diff --git a/includes/class-wc-rate-limiter.php b/includes/class-wc-rate-limiter.php index f50bd229b52..aba4fb01489 100644 --- a/includes/class-wc-rate-limiter.php +++ b/includes/class-wc-rate-limiter.php @@ -19,7 +19,7 @@ * add_notice( 'Sorry, too soon!' ); * } * - * @package WooCommerce/Classes + * @package WooCommerce\Classes * @version 3.9.0 * @since 3.9.0 */ diff --git a/includes/class-wc-regenerate-images-request.php b/includes/class-wc-regenerate-images-request.php index 0d0d90dbf33..0fc08a18c0f 100644 --- a/includes/class-wc-regenerate-images-request.php +++ b/includes/class-wc-regenerate-images-request.php @@ -2,7 +2,7 @@ /** * All functionality to regenerate images in the background when settings change. * - * @package WooCommerce/Classes + * @package WooCommerce\Classes * @version 3.3.0 * @since 3.3.0 */ diff --git a/includes/class-wc-regenerate-images.php b/includes/class-wc-regenerate-images.php index a26590a1cb9..023deabfd83 100644 --- a/includes/class-wc-regenerate-images.php +++ b/includes/class-wc-regenerate-images.php @@ -4,7 +4,7 @@ * * All functionality pertaining to regenerating product images in realtime. * - * @package WooCommerce/Classes + * @package WooCommerce\Classes * @version 3.5.0 * @since 3.3.0 */ diff --git a/includes/class-wc-register-wp-admin-settings.php b/includes/class-wc-register-wp-admin-settings.php index 822972f6985..9dbefe84b46 100644 --- a/includes/class-wc-register-wp-admin-settings.php +++ b/includes/class-wc-register-wp-admin-settings.php @@ -2,7 +2,7 @@ /** * Take settings registered for WP-Admin and hooks them up to the REST API * - * @package WooCommerce/Classes + * @package WooCommerce\Classes * @version 3.0.0 * @since 3.0.0 */ diff --git a/includes/class-wc-rest-authentication.php b/includes/class-wc-rest-authentication.php index e58108f69d4..12014593ce7 100644 --- a/includes/class-wc-rest-authentication.php +++ b/includes/class-wc-rest-authentication.php @@ -2,7 +2,7 @@ /** * REST API Authentication * - * @package WooCommerce/API + * @package WooCommerce\API * @since 2.6.0 */ diff --git a/includes/class-wc-rest-exception.php b/includes/class-wc-rest-exception.php index 8d7fd704a08..d22a183ee45 100644 --- a/includes/class-wc-rest-exception.php +++ b/includes/class-wc-rest-exception.php @@ -4,7 +4,7 @@ * * Extends Exception to provide additional data. * - * @package WooCommerce/API + * @package WooCommerce\API * @since 2.6.0 */ diff --git a/includes/class-wc-session-handler.php b/includes/class-wc-session-handler.php index 689e61e083e..cacda9e59a2 100644 --- a/includes/class-wc-session-handler.php +++ b/includes/class-wc-session-handler.php @@ -7,7 +7,7 @@ * * @class WC_Session_Handler * @version 2.5.0 - * @package WooCommerce/Classes + * @package WooCommerce\Classes */ use Automattic\Jetpack\Constants; diff --git a/includes/class-wc-shipping-rate.php b/includes/class-wc-shipping-rate.php index 06dc919123c..1248a70a423 100644 --- a/includes/class-wc-shipping-rate.php +++ b/includes/class-wc-shipping-rate.php @@ -4,7 +4,7 @@ * * Simple Class for storing rates. * - * @package WooCommerce/Classes/Shipping + * @package WooCommerce\Classes\Shipping * @since 2.6.0 */ diff --git a/includes/class-wc-shipping-zone.php b/includes/class-wc-shipping-zone.php index 3accf0a1d74..c52b5cf79fe 100644 --- a/includes/class-wc-shipping-zone.php +++ b/includes/class-wc-shipping-zone.php @@ -4,7 +4,7 @@ * * @since 2.6.0 * @version 3.0.0 - * @package WooCommerce/Classes + * @package WooCommerce\Classes */ defined( 'ABSPATH' ) || exit; diff --git a/includes/class-wc-shipping-zones.php b/includes/class-wc-shipping-zones.php index cb71dcd1abd..4acff8b8991 100644 --- a/includes/class-wc-shipping-zones.php +++ b/includes/class-wc-shipping-zones.php @@ -2,7 +2,7 @@ /** * Handles storage and retrieval of shipping zones * - * @package WooCommerce/Classes + * @package WooCommerce\Classes * @version 3.3.0 * @since 2.6.0 */ diff --git a/includes/class-wc-shipping.php b/includes/class-wc-shipping.php index a803c400f2f..91a9ab6a774 100644 --- a/includes/class-wc-shipping.php +++ b/includes/class-wc-shipping.php @@ -5,7 +5,7 @@ * Handles shipping and loads shipping methods via hooks. * * @version 2.6.0 - * @package WooCommerce/Classes/Shipping + * @package WooCommerce\Classes\Shipping */ use Automattic\Jetpack\Constants; diff --git a/includes/class-wc-shortcodes.php b/includes/class-wc-shortcodes.php index a26419ae89e..705b57d3e92 100644 --- a/includes/class-wc-shortcodes.php +++ b/includes/class-wc-shortcodes.php @@ -2,7 +2,7 @@ /** * Shortcodes * - * @package WooCommerce/Classes + * @package WooCommerce\Classes * @version 3.2.0 */ diff --git a/includes/class-wc-structured-data.php b/includes/class-wc-structured-data.php index 3c2354c5a5b..070bea646b0 100644 --- a/includes/class-wc-structured-data.php +++ b/includes/class-wc-structured-data.php @@ -2,7 +2,7 @@ /** * Structured data's handler and generator using JSON-LD format. * - * @package WooCommerce/Classes + * @package WooCommerce\Classes * @since 3.0.0 * @version 3.0.0 */ diff --git a/includes/class-wc-tax.php b/includes/class-wc-tax.php index a7784c3e702..70a34ab6c0c 100644 --- a/includes/class-wc-tax.php +++ b/includes/class-wc-tax.php @@ -2,7 +2,7 @@ /** * Tax calculation and rate finding class. * - * @package WooCommerce/Classes + * @package WooCommerce\Classes */ defined( 'ABSPATH' ) || exit; diff --git a/includes/class-wc-template-loader.php b/includes/class-wc-template-loader.php index 28e0606f6b4..cfbda8c8b0f 100644 --- a/includes/class-wc-template-loader.php +++ b/includes/class-wc-template-loader.php @@ -2,7 +2,7 @@ /** * Template Loader * - * @package WooCommerce/Classes + * @package WooCommerce\Classes */ defined( 'ABSPATH' ) || exit; diff --git a/includes/class-wc-tracker.php b/includes/class-wc-tracker.php index 02f8a169a8b..ca197d87bbe 100644 --- a/includes/class-wc-tracker.php +++ b/includes/class-wc-tracker.php @@ -7,7 +7,7 @@ * * @class WC_Tracker * @since 2.3.0 - * @package WooCommerce/Classes + * @package WooCommerce\Classes */ use Automattic\Jetpack\Constants; diff --git a/includes/class-wc-webhook.php b/includes/class-wc-webhook.php index c32f585cba4..b20766536fb 100644 --- a/includes/class-wc-webhook.php +++ b/includes/class-wc-webhook.php @@ -7,7 +7,7 @@ * Webhooks are enqueued to their associated actions, delivered, and logged. * * @version 3.2.0 - * @package WooCommerce/Webhooks + * @package WooCommerce\Webhooks * @since 2.2.0 */ diff --git a/includes/class-woocommerce.php b/includes/class-woocommerce.php index 4b7fd58ec17..2bad0bf07b0 100644 --- a/includes/class-woocommerce.php +++ b/includes/class-woocommerce.php @@ -22,7 +22,7 @@ final class WooCommerce { * * @var string */ - public $version = '4.4.0'; + public $version = '4.5.0'; /** * WooCommerce Schema version. @@ -198,6 +198,7 @@ final class WooCommerce { add_action( 'init', array( 'WC_Shortcodes', 'init' ) ); add_action( 'init', array( 'WC_Emails', 'init_transactional_emails' ) ); add_action( 'init', array( $this, 'add_image_sizes' ) ); + add_action( 'init', array( $this, 'load_rest_api' ) ); add_action( 'switch_blog', array( $this, 'wpdb_table_fix' ), 0 ); add_action( 'activated_plugin', array( $this, 'activated_plugin' ) ); add_action( 'deactivated_plugin', array( $this, 'deactivated_plugin' ) ); @@ -298,6 +299,13 @@ final class WooCommerce { return apply_filters( 'woocommerce_is_rest_api_request', $is_rest_api_request ); } + /** + * Load REST API. + */ + public function load_rest_api() { + \Automattic\WooCommerce\RestApi\Server::instance()->init(); + } + /** * What type of request is this? * diff --git a/includes/cli/class-wc-cli-rest-command.php b/includes/cli/class-wc-cli-rest-command.php index 0e8eb2a81c9..56063f9e62c 100644 --- a/includes/cli/class-wc-cli-rest-command.php +++ b/includes/cli/class-wc-cli-rest-command.php @@ -2,7 +2,7 @@ /** * WP_CLI_Rest_Command class file. * - * @package WooCommerce\Cli + * @package WooCommerce\CLI */ use Automattic\Jetpack\Constants; diff --git a/includes/data-stores/abstract-wc-order-data-store-cpt.php b/includes/data-stores/abstract-wc-order-data-store-cpt.php index 20b77b00797..fe8233e8bcf 100644 --- a/includes/data-stores/abstract-wc-order-data-store-cpt.php +++ b/includes/data-stores/abstract-wc-order-data-store-cpt.php @@ -2,7 +2,7 @@ /** * Abstract_WC_Order_Data_Store_CPT class file. * - * @package WooCommerce/Classes + * @package WooCommerce\Classes */ use Automattic\Jetpack\Constants; diff --git a/includes/data-stores/class-wc-coupon-data-store-cpt.php b/includes/data-stores/class-wc-coupon-data-store-cpt.php index dab16bfc1b9..da05e16f2a2 100644 --- a/includes/data-stores/class-wc-coupon-data-store-cpt.php +++ b/includes/data-stores/class-wc-coupon-data-store-cpt.php @@ -2,7 +2,7 @@ /** * Class WC_Coupon_Data_Store_CPT file. * - * @package WooCommerce\DataStore + * @package WooCommerce\DataStores */ if ( ! defined( 'ABSPATH' ) ) { @@ -721,7 +721,7 @@ class WC_Coupon_Data_Store_CPT extends WC_Data_Store_WP implements WC_Coupon_Dat return $wpdb->get_col( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE post_title = %s AND post_type = 'shop_coupon' AND post_status = 'publish' ORDER BY post_date DESC", - $code + wc_sanitize_coupon_code( $code ) ) ); } diff --git a/includes/data-stores/class-wc-data-store-wp.php b/includes/data-stores/class-wc-data-store-wp.php index fad8a1a3271..10222a73a27 100644 --- a/includes/data-stores/class-wc-data-store-wp.php +++ b/includes/data-stores/class-wc-data-store-wp.php @@ -6,7 +6,7 @@ * your own meta handling functions. * * @version 3.0.0 - * @package WooCommerce/Classes + * @package WooCommerce\Classes */ defined( 'ABSPATH' ) || exit; diff --git a/includes/data-stores/class-wc-order-data-store-cpt.php b/includes/data-stores/class-wc-order-data-store-cpt.php index bc8c2961f0d..0e19ecd4370 100644 --- a/includes/data-stores/class-wc-order-data-store-cpt.php +++ b/includes/data-stores/class-wc-order-data-store-cpt.php @@ -2,7 +2,7 @@ /** * WC_Order_Data_Store_CPT class file. * - * @package WooCommerce/Classes + * @package WooCommerce\Classes */ if ( ! defined( 'ABSPATH' ) ) { diff --git a/includes/data-stores/class-wc-order-item-shipping-data-store.php b/includes/data-stores/class-wc-order-item-shipping-data-store.php index e09545dfa30..0c4f3432e61 100644 --- a/includes/data-stores/class-wc-order-item-shipping-data-store.php +++ b/includes/data-stores/class-wc-order-item-shipping-data-store.php @@ -3,7 +3,7 @@ * WC Order Item Shipping Data Store * * @version 3.0.0 - * @package data-stores + * @package WooCommerce\DataStores */ if ( ! defined( 'ABSPATH' ) ) { 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 e417c1d9c6a..9e17fb52869 100644 --- a/includes/data-stores/class-wc-product-data-store-cpt.php +++ b/includes/data-stores/class-wc-product-data-store-cpt.php @@ -2,7 +2,7 @@ /** * WC_Product_Data_Store_CPT class file. * - * @package WooCommerce/Classes + * @package WooCommerce\Classes */ use Automattic\Jetpack\Constants; diff --git a/includes/data-stores/class-wc-product-variable-data-store-cpt.php b/includes/data-stores/class-wc-product-variable-data-store-cpt.php index dd2c2b16bbe..0f09da96679 100644 --- a/includes/data-stores/class-wc-product-variable-data-store-cpt.php +++ b/includes/data-stores/class-wc-product-variable-data-store-cpt.php @@ -2,7 +2,7 @@ /** * File for WC Variable Product Data Store class. * - * @package WooCommerce/Classes + * @package WooCommerce\Classes */ if ( ! defined( 'ABSPATH' ) ) { diff --git a/includes/data-stores/class-wc-webhook-data-store.php b/includes/data-stores/class-wc-webhook-data-store.php index a684981238b..947e89edb13 100644 --- a/includes/data-stores/class-wc-webhook-data-store.php +++ b/includes/data-stores/class-wc-webhook-data-store.php @@ -3,7 +3,7 @@ * Webhook Data Store * * @version 3.3.0 - * @package WooCommerce/Classes/Data_Store + * @package WooCommerce\Classes\Data_Store */ if ( ! defined( 'ABSPATH' ) ) { diff --git a/includes/emails/class-wc-email-cancelled-order.php b/includes/emails/class-wc-email-cancelled-order.php index d245a9d4f91..eeb4d34c73e 100644 --- a/includes/emails/class-wc-email-cancelled-order.php +++ b/includes/emails/class-wc-email-cancelled-order.php @@ -18,7 +18,7 @@ if ( ! class_exists( 'WC_Email_Cancelled_Order', false ) ) : * * @class WC_Email_Cancelled_Order * @version 2.2.7 - * @package WooCommerce/Classes/Emails + * @package WooCommerce\Classes\Emails * @extends WC_Email */ class WC_Email_Cancelled_Order extends WC_Email { diff --git a/includes/emails/class-wc-email-customer-completed-order.php b/includes/emails/class-wc-email-customer-completed-order.php index 91c1fe884ca..17958670e8c 100644 --- a/includes/emails/class-wc-email-customer-completed-order.php +++ b/includes/emails/class-wc-email-customer-completed-order.php @@ -18,7 +18,7 @@ if ( ! class_exists( 'WC_Email_Customer_Completed_Order', false ) ) : * * @class WC_Email_Customer_Completed_Order * @version 2.0.0 - * @package WooCommerce/Classes/Emails + * @package WooCommerce\Classes\Emails * @extends WC_Email */ class WC_Email_Customer_Completed_Order extends WC_Email { diff --git a/includes/emails/class-wc-email-customer-invoice.php b/includes/emails/class-wc-email-customer-invoice.php index a1973bfbc3e..0a2592cacf5 100644 --- a/includes/emails/class-wc-email-customer-invoice.php +++ b/includes/emails/class-wc-email-customer-invoice.php @@ -18,7 +18,7 @@ if ( ! class_exists( 'WC_Email_Customer_Invoice', false ) ) : * * @class WC_Email_Customer_Invoice * @version 3.5.0 - * @package WooCommerce/Classes/Emails + * @package WooCommerce\Classes\Emails * @extends WC_Email */ class WC_Email_Customer_Invoice extends WC_Email { diff --git a/includes/emails/class-wc-email-customer-new-account.php b/includes/emails/class-wc-email-customer-new-account.php index 674d18b3890..3cd0a825b58 100644 --- a/includes/emails/class-wc-email-customer-new-account.php +++ b/includes/emails/class-wc-email-customer-new-account.php @@ -18,7 +18,7 @@ if ( ! class_exists( 'WC_Email_Customer_New_Account', false ) ) : * * @class WC_Email_Customer_New_Account * @version 3.5.0 - * @package WooCommerce/Classes/Emails + * @package WooCommerce\Classes\Emails * @extends WC_Email */ class WC_Email_Customer_New_Account extends WC_Email { diff --git a/includes/emails/class-wc-email-customer-note.php b/includes/emails/class-wc-email-customer-note.php index 33e8aa6e8c1..cf1b2651901 100644 --- a/includes/emails/class-wc-email-customer-note.php +++ b/includes/emails/class-wc-email-customer-note.php @@ -18,7 +18,7 @@ if ( ! class_exists( 'WC_Email_Customer_Note', false ) ) : * * @class WC_Email_Customer_Note * @version 3.5.0 - * @package WooCommerce/Classes/Emails + * @package WooCommerce\Classes\Emails * @extends WC_Email */ class WC_Email_Customer_Note extends WC_Email { diff --git a/includes/emails/class-wc-email-customer-on-hold-order.php b/includes/emails/class-wc-email-customer-on-hold-order.php index 2985c40a0fc..080abb92395 100644 --- a/includes/emails/class-wc-email-customer-on-hold-order.php +++ b/includes/emails/class-wc-email-customer-on-hold-order.php @@ -18,7 +18,7 @@ if ( ! class_exists( 'WC_Email_Customer_On_Hold_Order', false ) ) : * * @class WC_Email_Customer_On_Hold_Order * @version 2.6.0 - * @package WooCommerce/Classes/Emails + * @package WooCommerce\Classes\Emails * @extends WC_Email */ class WC_Email_Customer_On_Hold_Order extends WC_Email { diff --git a/includes/emails/class-wc-email-customer-processing-order.php b/includes/emails/class-wc-email-customer-processing-order.php index 42fc858c038..18b8c95ac51 100644 --- a/includes/emails/class-wc-email-customer-processing-order.php +++ b/includes/emails/class-wc-email-customer-processing-order.php @@ -18,7 +18,7 @@ if ( ! class_exists( 'WC_Email_Customer_Processing_Order', false ) ) : * * @class WC_Email_Customer_Processing_Order * @version 3.5.0 - * @package WooCommerce/Classes/Emails + * @package WooCommerce\Classes\Emails * @extends WC_Email */ class WC_Email_Customer_Processing_Order extends WC_Email { diff --git a/includes/emails/class-wc-email-customer-refunded-order.php b/includes/emails/class-wc-email-customer-refunded-order.php index c70373759d9..1536ba33131 100644 --- a/includes/emails/class-wc-email-customer-refunded-order.php +++ b/includes/emails/class-wc-email-customer-refunded-order.php @@ -18,7 +18,7 @@ if ( ! class_exists( 'WC_Email_Customer_Refunded_Order', false ) ) : * * @class WC_Email_Customer_Refunded_Order * @version 3.5.0 - * @package WooCommerce/Classes/Emails + * @package WooCommerce\Classes\Emails * @extends WC_Email */ class WC_Email_Customer_Refunded_Order extends WC_Email { diff --git a/includes/emails/class-wc-email-customer-reset-password.php b/includes/emails/class-wc-email-customer-reset-password.php index 9392e1f26fd..7603097e4de 100644 --- a/includes/emails/class-wc-email-customer-reset-password.php +++ b/includes/emails/class-wc-email-customer-reset-password.php @@ -18,7 +18,7 @@ if ( ! class_exists( 'WC_Email_Customer_Reset_Password', false ) ) : * * @class WC_Email_Customer_Reset_Password * @version 3.5.0 - * @package WooCommerce/Classes/Emails + * @package WooCommerce\Classes\Emails * @extends WC_Email */ class WC_Email_Customer_Reset_Password extends WC_Email { diff --git a/includes/emails/class-wc-email-failed-order.php b/includes/emails/class-wc-email-failed-order.php index bf7c43fdf15..0766d9ac9b4 100644 --- a/includes/emails/class-wc-email-failed-order.php +++ b/includes/emails/class-wc-email-failed-order.php @@ -18,7 +18,7 @@ if ( ! class_exists( 'WC_Email_Failed_Order', false ) ) : * * @class WC_Email_Failed_Order * @version 2.5.0 - * @package WooCommerce/Classes/Emails + * @package WooCommerce\Classes\Emails * @extends WC_Email */ class WC_Email_Failed_Order extends WC_Email { diff --git a/includes/emails/class-wc-email-new-order.php b/includes/emails/class-wc-email-new-order.php index 159c6e45b30..0c063d61e76 100644 --- a/includes/emails/class-wc-email-new-order.php +++ b/includes/emails/class-wc-email-new-order.php @@ -18,7 +18,7 @@ if ( ! class_exists( 'WC_Email_New_Order' ) ) : * * @class WC_Email_New_Order * @version 2.0.0 - * @package WooCommerce/Classes/Emails + * @package WooCommerce\Classes\Emails * @extends WC_Email */ class WC_Email_New_Order extends WC_Email { diff --git a/includes/emails/class-wc-email.php b/includes/emails/class-wc-email.php index a16abe1a252..a47736ff75f 100644 --- a/includes/emails/class-wc-email.php +++ b/includes/emails/class-wc-email.php @@ -20,7 +20,7 @@ if ( class_exists( 'WC_Email', false ) ) { * * @class WC_Email * @version 2.5.0 - * @package WooCommerce/Classes/Emails + * @package WooCommerce\Classes\Emails * @extends WC_Settings_API */ class WC_Email extends WC_Settings_API { diff --git a/includes/export/abstract-wc-csv-batch-exporter.php b/includes/export/abstract-wc-csv-batch-exporter.php index 17e97aa7f53..b48fd53a4ae 100644 --- a/includes/export/abstract-wc-csv-batch-exporter.php +++ b/includes/export/abstract-wc-csv-batch-exporter.php @@ -4,7 +4,7 @@ * * Based on https://pippinsplugins.com/batch-processing-for-big-data/ * - * @package WooCommerce/Export + * @package WooCommerce\Export * @version 3.1.0 */ diff --git a/includes/export/abstract-wc-csv-exporter.php b/includes/export/abstract-wc-csv-exporter.php index cda963538db..e5631b9a5a3 100644 --- a/includes/export/abstract-wc-csv-exporter.php +++ b/includes/export/abstract-wc-csv-exporter.php @@ -2,7 +2,7 @@ /** * Handles CSV export. * - * @package WooCommerce/Export + * @package WooCommerce\Export * @version 3.1.0 */ diff --git a/includes/export/class-wc-product-csv-exporter.php b/includes/export/class-wc-product-csv-exporter.php index 394353bd6e4..23264604176 100644 --- a/includes/export/class-wc-product-csv-exporter.php +++ b/includes/export/class-wc-product-csv-exporter.php @@ -2,7 +2,7 @@ /** * Handles product CSV export. * - * @package WooCommerce/Export + * @package WooCommerce\Export * @version 3.1.0 */ diff --git a/includes/gateways/bacs/class-wc-gateway-bacs.php b/includes/gateways/bacs/class-wc-gateway-bacs.php index 83f43639e01..3ec175dfd59 100644 --- a/includes/gateways/bacs/class-wc-gateway-bacs.php +++ b/includes/gateways/bacs/class-wc-gateway-bacs.php @@ -17,7 +17,7 @@ if ( ! defined( 'ABSPATH' ) ) { * @class WC_Gateway_BACS * @extends WC_Payment_Gateway * @version 2.1.0 - * @package WooCommerce/Classes/Payment + * @package WooCommerce\Classes\Payment */ class WC_Gateway_BACS extends WC_Payment_Gateway { diff --git a/includes/gateways/cheque/class-wc-gateway-cheque.php b/includes/gateways/cheque/class-wc-gateway-cheque.php index 40f2599438f..7f5d003b72e 100644 --- a/includes/gateways/cheque/class-wc-gateway-cheque.php +++ b/includes/gateways/cheque/class-wc-gateway-cheque.php @@ -17,7 +17,7 @@ if ( ! defined( 'ABSPATH' ) ) { * @class WC_Gateway_Cheque * @extends WC_Payment_Gateway * @version 2.1.0 - * @package WooCommerce/Classes/Payment + * @package WooCommerce\Classes\Payment */ class WC_Gateway_Cheque extends WC_Payment_Gateway { diff --git a/includes/gateways/class-wc-payment-gateway-cc.php b/includes/gateways/class-wc-payment-gateway-cc.php index 22fe83bee3f..0ddd4621697 100644 --- a/includes/gateways/class-wc-payment-gateway-cc.php +++ b/includes/gateways/class-wc-payment-gateway-cc.php @@ -13,7 +13,7 @@ if ( ! defined( 'ABSPATH' ) ) { * Credit Card Payment Gateway * * @since 2.6.0 - * @package WooCommerce/Classes + * @package WooCommerce\Classes */ class WC_Payment_Gateway_CC extends WC_Payment_Gateway { diff --git a/includes/gateways/class-wc-payment-gateway-echeck.php b/includes/gateways/class-wc-payment-gateway-echeck.php index 69a564816df..c7754b664f9 100644 --- a/includes/gateways/class-wc-payment-gateway-echeck.php +++ b/includes/gateways/class-wc-payment-gateway-echeck.php @@ -13,7 +13,7 @@ if ( ! defined( 'ABSPATH' ) ) { * Class for eCheck Payment Gateway * * @since 2.6.0 - * @package WooCommerce/Classes + * @package WooCommerce\Classes */ class WC_Payment_Gateway_ECheck extends WC_Payment_Gateway { diff --git a/includes/gateways/cod/class-wc-gateway-cod.php b/includes/gateways/cod/class-wc-gateway-cod.php index a77bbede872..33c17643bf0 100644 --- a/includes/gateways/cod/class-wc-gateway-cod.php +++ b/includes/gateways/cod/class-wc-gateway-cod.php @@ -19,7 +19,7 @@ if ( ! defined( 'ABSPATH' ) ) { * @class WC_Gateway_COD * @extends WC_Payment_Gateway * @version 2.1.0 - * @package WooCommerce/Classes/Payment + * @package WooCommerce\Classes\Payment */ class WC_Gateway_COD extends WC_Payment_Gateway { diff --git a/includes/gateways/paypal/class-wc-gateway-paypal.php b/includes/gateways/paypal/class-wc-gateway-paypal.php index 03d7972e7bd..6cd1a03dd83 100644 --- a/includes/gateways/paypal/class-wc-gateway-paypal.php +++ b/includes/gateways/paypal/class-wc-gateway-paypal.php @@ -7,7 +7,7 @@ * @class WC_Gateway_Paypal * @extends WC_Payment_Gateway * @version 2.3.0 - * @package WooCommerce/Classes/Payment + * @package WooCommerce\Classes\Payment */ use Automattic\Jetpack\Constants; diff --git a/includes/gateways/paypal/includes/class-wc-gateway-paypal-ipn-handler.php b/includes/gateways/paypal/includes/class-wc-gateway-paypal-ipn-handler.php index ca15f4a74d6..b98b8eee30b 100644 --- a/includes/gateways/paypal/includes/class-wc-gateway-paypal-ipn-handler.php +++ b/includes/gateways/paypal/includes/class-wc-gateway-paypal-ipn-handler.php @@ -2,7 +2,7 @@ /** * Handles responses from PayPal IPN. * - * @package WooCommerce/PayPal + * @package WooCommerce\PayPal * @version 3.3.0 */ diff --git a/includes/gateways/paypal/includes/settings-paypal.php b/includes/gateways/paypal/includes/settings-paypal.php index 00bad8a706e..199c56ea758 100644 --- a/includes/gateways/paypal/includes/settings-paypal.php +++ b/includes/gateways/paypal/includes/settings-paypal.php @@ -2,7 +2,7 @@ /** * Settings for PayPal Gateway. * - * @package WooCommerce/Classes/Payment + * @package WooCommerce\Classes\Payment */ defined( 'ABSPATH' ) || exit; diff --git a/includes/import/abstract-wc-product-importer.php b/includes/import/abstract-wc-product-importer.php index 9cdd55af0f0..aa65051e0fe 100644 --- a/includes/import/abstract-wc-product-importer.php +++ b/includes/import/abstract-wc-product-importer.php @@ -2,7 +2,7 @@ /** * Abstract Product importer * - * @package WooCommerce/Import + * @package WooCommerce\Import * @version 3.1.0 */ diff --git a/includes/import/class-wc-product-csv-importer.php b/includes/import/class-wc-product-csv-importer.php index c361f95faea..c9c11f44684 100644 --- a/includes/import/class-wc-product-csv-importer.php +++ b/includes/import/class-wc-product-csv-importer.php @@ -2,7 +2,7 @@ /** * WooCommerce Product CSV importer * - * @package WooCommerce/Import + * @package WooCommerce\Import * @version 3.1.0 */ diff --git a/includes/integrations/maxmind-geolocation/class-wc-integration-maxmind-database-service.php b/includes/integrations/maxmind-geolocation/class-wc-integration-maxmind-database-service.php index 0778276626e..8a78efb4fbb 100644 --- a/includes/integrations/maxmind-geolocation/class-wc-integration-maxmind-database-service.php +++ b/includes/integrations/maxmind-geolocation/class-wc-integration-maxmind-database-service.php @@ -3,7 +3,7 @@ * The database service class file. * * @version 3.9.0 - * @package WooCommerce/Integrations + * @package WooCommerce\Integrations */ defined( 'ABSPATH' ) || exit; diff --git a/includes/integrations/maxmind-geolocation/class-wc-integration-maxmind-geolocation.php b/includes/integrations/maxmind-geolocation/class-wc-integration-maxmind-geolocation.php index 80a338ea2b8..1b84f3f06a9 100644 --- a/includes/integrations/maxmind-geolocation/class-wc-integration-maxmind-geolocation.php +++ b/includes/integrations/maxmind-geolocation/class-wc-integration-maxmind-geolocation.php @@ -3,7 +3,7 @@ * MaxMind Geolocation Integration * * @version 3.9.0 - * @package WooCommerce/Integrations + * @package WooCommerce\Integrations */ defined( 'ABSPATH' ) || exit; diff --git a/includes/interfaces/class-wc-abstract-order-data-store-interface.php b/includes/interfaces/class-wc-abstract-order-data-store-interface.php index 5688361b918..c043c34e214 100644 --- a/includes/interfaces/class-wc-abstract-order-data-store-interface.php +++ b/includes/interfaces/class-wc-abstract-order-data-store-interface.php @@ -3,7 +3,7 @@ * Order Data Store Interface * * @version 3.0.0 - * @package WooCommerce/Interfaces + * @package WooCommerce\Interfaces */ if ( ! defined( 'ABSPATH' ) ) { diff --git a/includes/interfaces/class-wc-coupon-data-store-interface.php b/includes/interfaces/class-wc-coupon-data-store-interface.php index c97e2c2f6d2..95256bcc00d 100644 --- a/includes/interfaces/class-wc-coupon-data-store-interface.php +++ b/includes/interfaces/class-wc-coupon-data-store-interface.php @@ -3,7 +3,7 @@ * Coupon Data Store Interface * * @version 3.0.0 - * @package WooCommerce/Interfaces + * @package WooCommerce\Interfaces */ if ( ! defined( 'ABSPATH' ) ) { diff --git a/includes/interfaces/class-wc-customer-data-store-interface.php b/includes/interfaces/class-wc-customer-data-store-interface.php index b353fc3b0f2..d2601898516 100644 --- a/includes/interfaces/class-wc-customer-data-store-interface.php +++ b/includes/interfaces/class-wc-customer-data-store-interface.php @@ -3,7 +3,7 @@ * Customer Data Store Interface * * @version 3.0.0 - * @package WooCommerce/Interface + * @package WooCommerce\Interface */ if ( ! defined( 'ABSPATH' ) ) { diff --git a/includes/interfaces/class-wc-customer-download-data-store-interface.php b/includes/interfaces/class-wc-customer-download-data-store-interface.php index 4f9cb235aeb..4ef79c59395 100644 --- a/includes/interfaces/class-wc-customer-download-data-store-interface.php +++ b/includes/interfaces/class-wc-customer-download-data-store-interface.php @@ -3,7 +3,7 @@ * Customer Download Data Store Interface * * @version 3.0.0 - * @package WooCommerce/Interface + * @package WooCommerce\Interface */ if ( ! defined( 'ABSPATH' ) ) { diff --git a/includes/interfaces/class-wc-customer-download-log-data-store-interface.php b/includes/interfaces/class-wc-customer-download-log-data-store-interface.php index 06d9e1d5cf4..32c5b168c4f 100644 --- a/includes/interfaces/class-wc-customer-download-log-data-store-interface.php +++ b/includes/interfaces/class-wc-customer-download-log-data-store-interface.php @@ -3,7 +3,7 @@ * Customer Download Log Data Store Interface * * @version 3.3.0 - * @package WooCommerce/Interface + * @package WooCommerce\Interface */ if ( ! defined( 'ABSPATH' ) ) { diff --git a/includes/interfaces/class-wc-importer-interface.php b/includes/interfaces/class-wc-importer-interface.php index fc6e3f9de48..fa4317932b8 100644 --- a/includes/interfaces/class-wc-importer-interface.php +++ b/includes/interfaces/class-wc-importer-interface.php @@ -2,7 +2,7 @@ /** * WooCommerce Importer Interface * - * @package WooCommerce/Interface + * @package WooCommerce\Interface * @version 3.1.0 */ diff --git a/includes/interfaces/class-wc-log-handler-interface.php b/includes/interfaces/class-wc-log-handler-interface.php index c8b3dd8e455..d84e39720c8 100644 --- a/includes/interfaces/class-wc-log-handler-interface.php +++ b/includes/interfaces/class-wc-log-handler-interface.php @@ -3,7 +3,7 @@ * Log Handler Interface * * @version 3.3.0 - * @package WooCommerce/Interface + * @package WooCommerce\Interface */ if ( ! defined( 'ABSPATH' ) ) { diff --git a/includes/interfaces/class-wc-logger-interface.php b/includes/interfaces/class-wc-logger-interface.php index 13194d19ccb..726b20cfe66 100644 --- a/includes/interfaces/class-wc-logger-interface.php +++ b/includes/interfaces/class-wc-logger-interface.php @@ -3,7 +3,7 @@ * Logger Interface * * @version 3.0.0 - * @package WooCommerce/Interface + * @package WooCommerce\Interface */ if ( ! defined( 'ABSPATH' ) ) { diff --git a/includes/interfaces/class-wc-object-data-store-interface.php b/includes/interfaces/class-wc-object-data-store-interface.php index 006873621ad..f6f276aaae6 100644 --- a/includes/interfaces/class-wc-object-data-store-interface.php +++ b/includes/interfaces/class-wc-object-data-store-interface.php @@ -3,7 +3,7 @@ * Object Data Store Interface * * @version 3.0.0 - * @package WooCommerce/Interface + * @package WooCommerce\Interface */ if ( ! defined( 'ABSPATH' ) ) { diff --git a/includes/interfaces/class-wc-order-data-store-interface.php b/includes/interfaces/class-wc-order-data-store-interface.php index 4727b9eb181..6f00c8a3efe 100644 --- a/includes/interfaces/class-wc-order-data-store-interface.php +++ b/includes/interfaces/class-wc-order-data-store-interface.php @@ -3,7 +3,7 @@ * Order Data Store Interface * * @version 3.0.0 - * @package WooCommerce/Interface + * @package WooCommerce\Interface */ if ( ! defined( 'ABSPATH' ) ) { diff --git a/includes/interfaces/class-wc-order-item-data-store-interface.php b/includes/interfaces/class-wc-order-item-data-store-interface.php index cbb13c27428..68ba29b4013 100644 --- a/includes/interfaces/class-wc-order-item-data-store-interface.php +++ b/includes/interfaces/class-wc-order-item-data-store-interface.php @@ -3,7 +3,7 @@ * Order Item Data Store Interface * * @version 3.0.0 - * @package WooCommerce/Interface + * @package WooCommerce\Interface */ if ( ! defined( 'ABSPATH' ) ) { diff --git a/includes/interfaces/class-wc-order-item-product-data-store-interface.php b/includes/interfaces/class-wc-order-item-product-data-store-interface.php index a6551ae0c1c..f5cd402ea2f 100644 --- a/includes/interfaces/class-wc-order-item-product-data-store-interface.php +++ b/includes/interfaces/class-wc-order-item-product-data-store-interface.php @@ -3,7 +3,7 @@ * Order Item Product Data Store Interface * * @version 3.0.0 - * @package WooCommerce/Interface + * @package WooCommerce\Interface */ if ( ! defined( 'ABSPATH' ) ) { diff --git a/includes/interfaces/class-wc-order-item-type-data-store-interface.php b/includes/interfaces/class-wc-order-item-type-data-store-interface.php index 548b4676fa7..006a4e6b0d4 100644 --- a/includes/interfaces/class-wc-order-item-type-data-store-interface.php +++ b/includes/interfaces/class-wc-order-item-type-data-store-interface.php @@ -3,7 +3,7 @@ * Order Item Type Data Store Interface * * @version 3.0.0 - * @package WooCommerce/Interface + * @package WooCommerce\Interface */ if ( ! defined( 'ABSPATH' ) ) { diff --git a/includes/interfaces/class-wc-order-refund-data-store-interface.php b/includes/interfaces/class-wc-order-refund-data-store-interface.php index 30961beef6c..b97864d06e3 100644 --- a/includes/interfaces/class-wc-order-refund-data-store-interface.php +++ b/includes/interfaces/class-wc-order-refund-data-store-interface.php @@ -3,7 +3,7 @@ * Order Refund Data Store Interface * * @version 3.0.0 - * @package WooCommerce/Interface + * @package WooCommerce\Interface */ if ( ! defined( 'ABSPATH' ) ) { diff --git a/includes/interfaces/class-wc-payment-token-data-store-interface.php b/includes/interfaces/class-wc-payment-token-data-store-interface.php index 3fdf0eacbef..fb2a581e741 100644 --- a/includes/interfaces/class-wc-payment-token-data-store-interface.php +++ b/includes/interfaces/class-wc-payment-token-data-store-interface.php @@ -3,7 +3,7 @@ * Payment Token Data Store Interface * * @version 3.0.0 - * @package WooCommerce/Interface + * @package WooCommerce\Interface */ if ( ! defined( 'ABSPATH' ) ) { diff --git a/includes/interfaces/class-wc-product-data-store-interface.php b/includes/interfaces/class-wc-product-data-store-interface.php index c4f92f5d597..3e50cfedf14 100644 --- a/includes/interfaces/class-wc-product-data-store-interface.php +++ b/includes/interfaces/class-wc-product-data-store-interface.php @@ -3,7 +3,7 @@ * Product Data Store Interface * * @version 3.0.0 - * @package WooCommerce/Interface + * @package WooCommerce\Interface */ if ( ! defined( 'ABSPATH' ) ) { diff --git a/includes/interfaces/class-wc-product-variable-data-store-interface.php b/includes/interfaces/class-wc-product-variable-data-store-interface.php index 3a62ac87a90..b22893dc3a6 100644 --- a/includes/interfaces/class-wc-product-variable-data-store-interface.php +++ b/includes/interfaces/class-wc-product-variable-data-store-interface.php @@ -3,7 +3,7 @@ * Product Variable Data Store Interface * * @version 3.0.0 - * @package WooCommerce/Interface + * @package WooCommerce\Interface */ if ( ! defined( 'ABSPATH' ) ) { diff --git a/includes/interfaces/class-wc-queue-interface.php b/includes/interfaces/class-wc-queue-interface.php index c341b6aff28..5a960d383f7 100644 --- a/includes/interfaces/class-wc-queue-interface.php +++ b/includes/interfaces/class-wc-queue-interface.php @@ -3,7 +3,7 @@ * Queue Interface * * @version 3.5.0 - * @package WooCommerce/Interface + * @package WooCommerce\Interface */ if ( ! defined( 'ABSPATH' ) ) { diff --git a/includes/interfaces/class-wc-shipping-zone-data-store-interface.php b/includes/interfaces/class-wc-shipping-zone-data-store-interface.php index 830ca6cf5bd..7009c5b7209 100644 --- a/includes/interfaces/class-wc-shipping-zone-data-store-interface.php +++ b/includes/interfaces/class-wc-shipping-zone-data-store-interface.php @@ -3,7 +3,7 @@ * Shipping Zone Data Store Interface * * @version 3.0.0 - * @package WooCommerce/Interface + * @package WooCommerce\Interface */ if ( ! defined( 'ABSPATH' ) ) { diff --git a/includes/interfaces/class-wc-webhooks-data-store-interface.php b/includes/interfaces/class-wc-webhooks-data-store-interface.php index 3d4839de9f0..f0e2e729843 100644 --- a/includes/interfaces/class-wc-webhooks-data-store-interface.php +++ b/includes/interfaces/class-wc-webhooks-data-store-interface.php @@ -3,7 +3,7 @@ * Webhook Data Store Interface * * @version 3.2.0 - * @package WooCommerce/Interface + * @package WooCommerce\Interface */ if ( ! defined( 'ABSPATH' ) ) { diff --git a/includes/legacy/abstract-wc-legacy-order.php b/includes/legacy/abstract-wc-legacy-order.php index ea7729f5833..8867096c739 100644 --- a/includes/legacy/abstract-wc-legacy-order.php +++ b/includes/legacy/abstract-wc-legacy-order.php @@ -10,7 +10,7 @@ if ( ! defined( 'ABSPATH' ) ) { * This class will be removed in future versions. * * @version 3.0.0 - * @package WooCommerce/Abstracts + * @package WooCommerce\Abstracts * @category Abstract Class * @author WooThemes */ diff --git a/includes/legacy/abstract-wc-legacy-payment-token.php b/includes/legacy/abstract-wc-legacy-payment-token.php index d1316d1b4cd..ed7201a3038 100644 --- a/includes/legacy/abstract-wc-legacy-payment-token.php +++ b/includes/legacy/abstract-wc-legacy-payment-token.php @@ -11,7 +11,7 @@ if ( ! defined( 'ABSPATH' ) ) { * directly on the object. * * @version 3.0.0 - * @package WooCommerce/Classes + * @package WooCommerce\Classes * @category Class * @author WooCommerce */ diff --git a/includes/legacy/abstract-wc-legacy-product.php b/includes/legacy/abstract-wc-legacy-product.php index 5e3464af08b..97f370e2ed1 100644 --- a/includes/legacy/abstract-wc-legacy-product.php +++ b/includes/legacy/abstract-wc-legacy-product.php @@ -11,7 +11,7 @@ if ( ! defined( 'ABSPATH' ) ) { * This class will be removed in future versions. * * @version 3.0.0 - * @package WooCommerce/Abstracts + * @package WooCommerce\Abstracts * @category Abstract Class * @author WooThemes */ diff --git a/includes/legacy/api/class-wc-rest-legacy-coupons-controller.php b/includes/legacy/api/class-wc-rest-legacy-coupons-controller.php index 3378de41a40..19be8c3cb55 100644 --- a/includes/legacy/api/class-wc-rest-legacy-coupons-controller.php +++ b/includes/legacy/api/class-wc-rest-legacy-coupons-controller.php @@ -6,7 +6,7 @@ * * @author WooThemes * @category API - * @package WooCommerce/API + * @package WooCommerce\API * @since 3.0.0 */ @@ -17,7 +17,7 @@ if ( ! defined( 'ABSPATH' ) ) { /** * REST API Legacy Coupons controller class. * - * @package WooCommerce/API + * @package WooCommerce\API * @extends WC_REST_CRUD_Controller */ class WC_REST_Legacy_Coupons_Controller extends WC_REST_CRUD_Controller { diff --git a/includes/legacy/api/class-wc-rest-legacy-orders-controller.php b/includes/legacy/api/class-wc-rest-legacy-orders-controller.php index ccc9db267f3..180e0299241 100644 --- a/includes/legacy/api/class-wc-rest-legacy-orders-controller.php +++ b/includes/legacy/api/class-wc-rest-legacy-orders-controller.php @@ -6,7 +6,7 @@ * * @author WooThemes * @category API - * @package WooCommerce/API + * @package WooCommerce\API * @since 3.0.0 */ @@ -17,7 +17,7 @@ if ( ! defined( 'ABSPATH' ) ) { /** * REST API Legacy Orders controller class. * - * @package WooCommerce/API + * @package WooCommerce\API * @extends WC_REST_CRUD_Controller */ class WC_REST_Legacy_Orders_Controller extends WC_REST_CRUD_Controller { diff --git a/includes/legacy/api/class-wc-rest-legacy-products-controller.php b/includes/legacy/api/class-wc-rest-legacy-products-controller.php index 9096cbd0f98..b04ff689726 100644 --- a/includes/legacy/api/class-wc-rest-legacy-products-controller.php +++ b/includes/legacy/api/class-wc-rest-legacy-products-controller.php @@ -6,7 +6,7 @@ * * @author WooThemes * @category API - * @package WooCommerce/API + * @package WooCommerce\API * @since 3.0.0 */ @@ -17,7 +17,7 @@ if ( ! defined( 'ABSPATH' ) ) { /** * REST API Legacy Products controller class. * - * @package WooCommerce/API + * @package WooCommerce\API * @extends WC_REST_CRUD_Controller */ class WC_REST_Legacy_Products_Controller extends WC_REST_CRUD_Controller { diff --git a/includes/legacy/api/v1/class-wc-api-authentication.php b/includes/legacy/api/v1/class-wc-api-authentication.php index 1d26ae80ff6..a95abce7b8c 100644 --- a/includes/legacy/api/v1/class-wc-api-authentication.php +++ b/includes/legacy/api/v1/class-wc-api-authentication.php @@ -4,7 +4,7 @@ * * @author WooThemes * @category API - * @package WooCommerce/API + * @package WooCommerce\API * @since 2.1.0 * @version 2.4.0 */ diff --git a/includes/legacy/api/v1/class-wc-api-coupons.php b/includes/legacy/api/v1/class-wc-api-coupons.php index 244b5efd3b0..3f1a2b0428b 100644 --- a/includes/legacy/api/v1/class-wc-api-coupons.php +++ b/includes/legacy/api/v1/class-wc-api-coupons.php @@ -6,7 +6,7 @@ * * @author WooThemes * @category API - * @package WooCommerce/API + * @package WooCommerce\API * @since 2.1 * @version 2.1 */ diff --git a/includes/legacy/api/v1/class-wc-api-customers.php b/includes/legacy/api/v1/class-wc-api-customers.php index d5edb1503a2..81752d8860d 100644 --- a/includes/legacy/api/v1/class-wc-api-customers.php +++ b/includes/legacy/api/v1/class-wc-api-customers.php @@ -6,7 +6,7 @@ * * @author WooThemes * @category API - * @package WooCommerce/API + * @package WooCommerce\API * @since 2.1 * @version 2.1 */ diff --git a/includes/legacy/api/v1/class-wc-api-json-handler.php b/includes/legacy/api/v1/class-wc-api-json-handler.php index 691bbb9663c..df01283228d 100644 --- a/includes/legacy/api/v1/class-wc-api-json-handler.php +++ b/includes/legacy/api/v1/class-wc-api-json-handler.php @@ -6,7 +6,7 @@ * * @author WooThemes * @category API - * @package WooCommerce/API + * @package WooCommerce\API * @since 2.1 * @version 2.1 */ diff --git a/includes/legacy/api/v1/class-wc-api-orders.php b/includes/legacy/api/v1/class-wc-api-orders.php index c4a391d5bdb..e2fee006afb 100644 --- a/includes/legacy/api/v1/class-wc-api-orders.php +++ b/includes/legacy/api/v1/class-wc-api-orders.php @@ -6,7 +6,7 @@ * * @author WooThemes * @category API - * @package WooCommerce/API + * @package WooCommerce\API * @since 2.1 * @version 2.1 */ diff --git a/includes/legacy/api/v1/class-wc-api-products.php b/includes/legacy/api/v1/class-wc-api-products.php index b608a3f326c..b81bc653cd1 100644 --- a/includes/legacy/api/v1/class-wc-api-products.php +++ b/includes/legacy/api/v1/class-wc-api-products.php @@ -6,7 +6,7 @@ * * @author WooThemes * @category API - * @package WooCommerce/API + * @package WooCommerce\API * @since 2.1 * @version 3.0 */ diff --git a/includes/legacy/api/v1/class-wc-api-reports.php b/includes/legacy/api/v1/class-wc-api-reports.php index 527ea64bf94..f8b95db0bd7 100644 --- a/includes/legacy/api/v1/class-wc-api-reports.php +++ b/includes/legacy/api/v1/class-wc-api-reports.php @@ -6,7 +6,7 @@ * * @author WooThemes * @category API - * @package WooCommerce/API + * @package WooCommerce\API * @since 2.1 * @version 2.1 */ diff --git a/includes/legacy/api/v1/class-wc-api-resource.php b/includes/legacy/api/v1/class-wc-api-resource.php index d419f8346e9..ae1bd956fd5 100644 --- a/includes/legacy/api/v1/class-wc-api-resource.php +++ b/includes/legacy/api/v1/class-wc-api-resource.php @@ -6,7 +6,7 @@ * * @author WooThemes * @category API - * @package WooCommerce/API + * @package WooCommerce\API * @since 2.1 * @version 2.1 */ diff --git a/includes/legacy/api/v1/class-wc-api-server.php b/includes/legacy/api/v1/class-wc-api-server.php index 182d482b51f..15ba35f05e5 100644 --- a/includes/legacy/api/v1/class-wc-api-server.php +++ b/includes/legacy/api/v1/class-wc-api-server.php @@ -9,7 +9,7 @@ * * @author WooThemes * @category API - * @package WooCommerce/API + * @package WooCommerce\API * @since 2.1 * @version 2.1 */ diff --git a/includes/legacy/api/v1/class-wc-api-xml-handler.php b/includes/legacy/api/v1/class-wc-api-xml-handler.php index 04f47e669e4..a9b5fc7d4ea 100644 --- a/includes/legacy/api/v1/class-wc-api-xml-handler.php +++ b/includes/legacy/api/v1/class-wc-api-xml-handler.php @@ -6,7 +6,7 @@ * * @author WooThemes * @category API - * @package WooCommerce/API + * @package WooCommerce\API * @since 2.1 * @version 2.1 */ diff --git a/includes/legacy/api/v1/interface-wc-api-handler.php b/includes/legacy/api/v1/interface-wc-api-handler.php index 464d9cb73cb..334c204f241 100644 --- a/includes/legacy/api/v1/interface-wc-api-handler.php +++ b/includes/legacy/api/v1/interface-wc-api-handler.php @@ -6,7 +6,7 @@ * * @author WooThemes * @category API - * @package WooCommerce/API + * @package WooCommerce\API * @since 2.1 * @version 2.1 */ diff --git a/includes/legacy/api/v2/class-wc-api-authentication.php b/includes/legacy/api/v2/class-wc-api-authentication.php index 2c75a75b605..31ee951667d 100644 --- a/includes/legacy/api/v2/class-wc-api-authentication.php +++ b/includes/legacy/api/v2/class-wc-api-authentication.php @@ -4,7 +4,7 @@ * * @author WooThemes * @category API - * @package WooCommerce/API + * @package WooCommerce\API * @since 2.1.0 * @version 2.4.0 */ diff --git a/includes/legacy/api/v2/class-wc-api-coupons.php b/includes/legacy/api/v2/class-wc-api-coupons.php index ca57fb4670e..4a7b12ae0bf 100644 --- a/includes/legacy/api/v2/class-wc-api-coupons.php +++ b/includes/legacy/api/v2/class-wc-api-coupons.php @@ -6,7 +6,7 @@ * * @author WooThemes * @category API - * @package WooCommerce/API + * @package WooCommerce\API * @since 2.1 */ diff --git a/includes/legacy/api/v2/class-wc-api-customers.php b/includes/legacy/api/v2/class-wc-api-customers.php index 4912be3b34e..03b18dde8d2 100644 --- a/includes/legacy/api/v2/class-wc-api-customers.php +++ b/includes/legacy/api/v2/class-wc-api-customers.php @@ -6,7 +6,7 @@ * * @author WooThemes * @category API - * @package WooCommerce/API + * @package WooCommerce\API * @since 2.2 */ diff --git a/includes/legacy/api/v2/class-wc-api-exception.php b/includes/legacy/api/v2/class-wc-api-exception.php index 834ed04d6eb..986f56d5903 100644 --- a/includes/legacy/api/v2/class-wc-api-exception.php +++ b/includes/legacy/api/v2/class-wc-api-exception.php @@ -6,7 +6,7 @@ * * @author WooThemes * @category API - * @package WooCommerce/API + * @package WooCommerce\API * @since 2.2 */ diff --git a/includes/legacy/api/v2/class-wc-api-json-handler.php b/includes/legacy/api/v2/class-wc-api-json-handler.php index 672aa8850c2..b86f2e41820 100644 --- a/includes/legacy/api/v2/class-wc-api-json-handler.php +++ b/includes/legacy/api/v2/class-wc-api-json-handler.php @@ -6,7 +6,7 @@ * * @author WooThemes * @category API - * @package WooCommerce/API + * @package WooCommerce\API * @since 2.1 */ diff --git a/includes/legacy/api/v2/class-wc-api-orders.php b/includes/legacy/api/v2/class-wc-api-orders.php index 67fc745d364..a935a44541f 100644 --- a/includes/legacy/api/v2/class-wc-api-orders.php +++ b/includes/legacy/api/v2/class-wc-api-orders.php @@ -6,7 +6,7 @@ * * @author WooThemes * @category API - * @package WooCommerce/API + * @package WooCommerce\API * @since 2.1 */ diff --git a/includes/legacy/api/v2/class-wc-api-products.php b/includes/legacy/api/v2/class-wc-api-products.php index 0ceaf023a0c..d065f886572 100644 --- a/includes/legacy/api/v2/class-wc-api-products.php +++ b/includes/legacy/api/v2/class-wc-api-products.php @@ -6,7 +6,7 @@ * * @author WooThemes * @category API - * @package WooCommerce/API + * @package WooCommerce\API * @since 2.1 * @version 3.0 */ diff --git a/includes/legacy/api/v2/class-wc-api-reports.php b/includes/legacy/api/v2/class-wc-api-reports.php index 8387a2e7b9b..aae6d5f822c 100644 --- a/includes/legacy/api/v2/class-wc-api-reports.php +++ b/includes/legacy/api/v2/class-wc-api-reports.php @@ -6,7 +6,7 @@ * * @author WooThemes * @category API - * @package WooCommerce/API + * @package WooCommerce\API * @since 2.1 */ diff --git a/includes/legacy/api/v2/class-wc-api-resource.php b/includes/legacy/api/v2/class-wc-api-resource.php index cd2a9ecde6f..9475ad3a27b 100644 --- a/includes/legacy/api/v2/class-wc-api-resource.php +++ b/includes/legacy/api/v2/class-wc-api-resource.php @@ -6,7 +6,7 @@ * * @author WooThemes * @category API - * @package WooCommerce/API + * @package WooCommerce\API * @since 2.1 */ diff --git a/includes/legacy/api/v2/class-wc-api-server.php b/includes/legacy/api/v2/class-wc-api-server.php index 4df7e3bfcb3..eefa415b626 100644 --- a/includes/legacy/api/v2/class-wc-api-server.php +++ b/includes/legacy/api/v2/class-wc-api-server.php @@ -9,7 +9,7 @@ * * @author WooThemes * @category API - * @package WooCommerce/API + * @package WooCommerce\API * @since 2.1 */ diff --git a/includes/legacy/api/v2/class-wc-api-webhooks.php b/includes/legacy/api/v2/class-wc-api-webhooks.php index 83121936eaf..a8395e9ab82 100644 --- a/includes/legacy/api/v2/class-wc-api-webhooks.php +++ b/includes/legacy/api/v2/class-wc-api-webhooks.php @@ -6,7 +6,7 @@ * * @author WooThemes * @category API - * @package WooCommerce/API + * @package WooCommerce\API * @since 2.2 */ diff --git a/includes/legacy/api/v2/interface-wc-api-handler.php b/includes/legacy/api/v2/interface-wc-api-handler.php index 484f9f57f02..30fa3642b98 100644 --- a/includes/legacy/api/v2/interface-wc-api-handler.php +++ b/includes/legacy/api/v2/interface-wc-api-handler.php @@ -6,7 +6,7 @@ * * @author WooThemes * @category API - * @package WooCommerce/API + * @package WooCommerce\API * @since 2.1 */ diff --git a/includes/legacy/api/v3/class-wc-api-authentication.php b/includes/legacy/api/v3/class-wc-api-authentication.php index 88f0a711644..c57f74a9d11 100644 --- a/includes/legacy/api/v3/class-wc-api-authentication.php +++ b/includes/legacy/api/v3/class-wc-api-authentication.php @@ -4,7 +4,7 @@ * * @author WooThemes * @category API - * @package WooCommerce/API + * @package WooCommerce\API * @since 2.1.0 * @version 2.4.0 */ diff --git a/includes/legacy/api/v3/class-wc-api-coupons.php b/includes/legacy/api/v3/class-wc-api-coupons.php index 43c71cb817c..49dbc0d4677 100644 --- a/includes/legacy/api/v3/class-wc-api-coupons.php +++ b/includes/legacy/api/v3/class-wc-api-coupons.php @@ -6,7 +6,7 @@ * * @author WooThemes * @category API - * @package WooCommerce/API + * @package WooCommerce\API * @since 2.1 */ diff --git a/includes/legacy/api/v3/class-wc-api-customers.php b/includes/legacy/api/v3/class-wc-api-customers.php index 8e6c5a13d0b..afb092726ad 100644 --- a/includes/legacy/api/v3/class-wc-api-customers.php +++ b/includes/legacy/api/v3/class-wc-api-customers.php @@ -6,7 +6,7 @@ * * @author WooThemes * @category API - * @package WooCommerce/API + * @package WooCommerce\API * @since 2.2 */ diff --git a/includes/legacy/api/v3/class-wc-api-exception.php b/includes/legacy/api/v3/class-wc-api-exception.php index 834ed04d6eb..986f56d5903 100644 --- a/includes/legacy/api/v3/class-wc-api-exception.php +++ b/includes/legacy/api/v3/class-wc-api-exception.php @@ -6,7 +6,7 @@ * * @author WooThemes * @category API - * @package WooCommerce/API + * @package WooCommerce\API * @since 2.2 */ diff --git a/includes/legacy/api/v3/class-wc-api-json-handler.php b/includes/legacy/api/v3/class-wc-api-json-handler.php index 672aa8850c2..b86f2e41820 100644 --- a/includes/legacy/api/v3/class-wc-api-json-handler.php +++ b/includes/legacy/api/v3/class-wc-api-json-handler.php @@ -6,7 +6,7 @@ * * @author WooThemes * @category API - * @package WooCommerce/API + * @package WooCommerce\API * @since 2.1 */ diff --git a/includes/legacy/api/v3/class-wc-api-orders.php b/includes/legacy/api/v3/class-wc-api-orders.php index aa2f69219f6..e2d682d461b 100644 --- a/includes/legacy/api/v3/class-wc-api-orders.php +++ b/includes/legacy/api/v3/class-wc-api-orders.php @@ -6,7 +6,7 @@ * * @author WooThemes * @category API - * @package WooCommerce/API + * @package WooCommerce\API * @since 2.1 */ diff --git a/includes/legacy/api/v3/class-wc-api-products.php b/includes/legacy/api/v3/class-wc-api-products.php index 35b7c206644..c9720ec9039 100644 --- a/includes/legacy/api/v3/class-wc-api-products.php +++ b/includes/legacy/api/v3/class-wc-api-products.php @@ -6,7 +6,7 @@ * * @author WooThemes * @category API - * @package WooCommerce/API + * @package WooCommerce\API * @since 2.1 * @version 3.0 */ diff --git a/includes/legacy/api/v3/class-wc-api-reports.php b/includes/legacy/api/v3/class-wc-api-reports.php index 552fd253fda..c4613a63916 100644 --- a/includes/legacy/api/v3/class-wc-api-reports.php +++ b/includes/legacy/api/v3/class-wc-api-reports.php @@ -6,7 +6,7 @@ * * @author WooThemes * @category API - * @package WooCommerce/API + * @package WooCommerce\API * @since 2.1 */ diff --git a/includes/legacy/api/v3/class-wc-api-resource.php b/includes/legacy/api/v3/class-wc-api-resource.php index 4aface69c0c..e87e9e9268d 100644 --- a/includes/legacy/api/v3/class-wc-api-resource.php +++ b/includes/legacy/api/v3/class-wc-api-resource.php @@ -6,7 +6,7 @@ * * @author WooThemes * @category API - * @package WooCommerce/API + * @package WooCommerce\API * @since 2.1 */ diff --git a/includes/legacy/api/v3/class-wc-api-server.php b/includes/legacy/api/v3/class-wc-api-server.php index 9c22f6ef6fa..fe218dcf6c8 100644 --- a/includes/legacy/api/v3/class-wc-api-server.php +++ b/includes/legacy/api/v3/class-wc-api-server.php @@ -9,7 +9,7 @@ * * @author WooThemes * @category API - * @package WooCommerce/API + * @package WooCommerce\API * @since 2.1 */ diff --git a/includes/legacy/api/v3/class-wc-api-taxes.php b/includes/legacy/api/v3/class-wc-api-taxes.php index 0ff45885226..fef17b98bb3 100644 --- a/includes/legacy/api/v3/class-wc-api-taxes.php +++ b/includes/legacy/api/v3/class-wc-api-taxes.php @@ -6,7 +6,7 @@ * * @author WooThemes * @category API - * @package WooCommerce/API + * @package WooCommerce\API * @since 2.5.0 */ diff --git a/includes/legacy/api/v3/class-wc-api-webhooks.php b/includes/legacy/api/v3/class-wc-api-webhooks.php index 83121936eaf..a8395e9ab82 100644 --- a/includes/legacy/api/v3/class-wc-api-webhooks.php +++ b/includes/legacy/api/v3/class-wc-api-webhooks.php @@ -6,7 +6,7 @@ * * @author WooThemes * @category API - * @package WooCommerce/API + * @package WooCommerce\API * @since 2.2 */ diff --git a/includes/legacy/api/v3/interface-wc-api-handler.php b/includes/legacy/api/v3/interface-wc-api-handler.php index 484f9f57f02..30fa3642b98 100644 --- a/includes/legacy/api/v3/interface-wc-api-handler.php +++ b/includes/legacy/api/v3/interface-wc-api-handler.php @@ -6,7 +6,7 @@ * * @author WooThemes * @category API - * @package WooCommerce/API + * @package WooCommerce\API * @since 2.1 */ diff --git a/includes/legacy/class-wc-legacy-api.php b/includes/legacy/class-wc-legacy-api.php index 4bb14fa0b62..f121fff8c82 100644 --- a/includes/legacy/class-wc-legacy-api.php +++ b/includes/legacy/class-wc-legacy-api.php @@ -4,7 +4,7 @@ * * @author WooThemes * @category API - * @package WooCommerce/API + * @package WooCommerce\API * @since 2.6 */ diff --git a/includes/legacy/class-wc-legacy-cart.php b/includes/legacy/class-wc-legacy-cart.php index 4a5b14e0518..94a65443a38 100644 --- a/includes/legacy/class-wc-legacy-cart.php +++ b/includes/legacy/class-wc-legacy-cart.php @@ -6,7 +6,7 @@ * This class will be removed in future versions. * * @version 3.2.0 - * @package WooCommerce/Classes + * @package WooCommerce\Classes * @category Class * @author Automattic */ diff --git a/includes/legacy/class-wc-legacy-coupon.php b/includes/legacy/class-wc-legacy-coupon.php index cad0fc1fdb1..95b3be4a9b5 100644 --- a/includes/legacy/class-wc-legacy-coupon.php +++ b/includes/legacy/class-wc-legacy-coupon.php @@ -11,7 +11,7 @@ if ( ! defined( 'ABSPATH' ) ) { * * @class WC_Legacy_Coupon * @version 3.0.0 - * @package WooCommerce/Classes + * @package WooCommerce\Classes * @category Class * @author WooThemes */ diff --git a/includes/legacy/class-wc-legacy-customer.php b/includes/legacy/class-wc-legacy-customer.php index 5ad3d952afe..37ed5922ad9 100644 --- a/includes/legacy/class-wc-legacy-customer.php +++ b/includes/legacy/class-wc-legacy-customer.php @@ -7,7 +7,7 @@ if ( ! defined( 'ABSPATH' ) ) { * Legacy Customer. * * @version 3.0.0 - * @package WooCommerce/Classes + * @package WooCommerce\Classes * @category Class * @author WooThemes */ diff --git a/includes/legacy/class-wc-legacy-shipping-zone.php b/includes/legacy/class-wc-legacy-shipping-zone.php index aa06337a36c..3c1c3a0d754 100644 --- a/includes/legacy/class-wc-legacy-shipping-zone.php +++ b/includes/legacy/class-wc-legacy-shipping-zone.php @@ -7,7 +7,7 @@ if ( ! defined( 'ABSPATH' ) ) { * Legacy Shipping Zone. * * @version 3.0.0 - * @package WooCommerce/Classes + * @package WooCommerce\Classes * @category Class * @author WooThemes */ diff --git a/includes/legacy/class-wc-legacy-webhook.php b/includes/legacy/class-wc-legacy-webhook.php index e18000eedff..d1e703477b1 100644 --- a/includes/legacy/class-wc-legacy-webhook.php +++ b/includes/legacy/class-wc-legacy-webhook.php @@ -6,7 +6,7 @@ * This class will be removed in future versions. * * @version 3.2.0 - * @package WooCommerce/Classes + * @package WooCommerce\Classes * @category Class * @author Automattic */ diff --git a/includes/log-handlers/class-wc-log-handler-db.php b/includes/log-handlers/class-wc-log-handler-db.php index 77b4f929642..ef961334e23 100644 --- a/includes/log-handlers/class-wc-log-handler-db.php +++ b/includes/log-handlers/class-wc-log-handler-db.php @@ -16,7 +16,7 @@ if ( ! defined( 'ABSPATH' ) ) { * * @class WC_Log_Handler_DB * @version 1.0.0 - * @package WooCommerce/Classes/Log_Handlers + * @package WooCommerce\Classes\Log_Handlers */ class WC_Log_Handler_DB extends WC_Log_Handler { diff --git a/includes/log-handlers/class-wc-log-handler-email.php b/includes/log-handlers/class-wc-log-handler-email.php index 6e23005dbaa..d3d6a683c43 100644 --- a/includes/log-handlers/class-wc-log-handler-email.php +++ b/includes/log-handlers/class-wc-log-handler-email.php @@ -28,7 +28,7 @@ if ( ! defined( 'ABSPATH' ) ) { * * @class WC_Log_Handler_Email * @version 1.0.0 - * @package WooCommerce/Classes/Log_Handlers + * @package WooCommerce\Classes\Log_Handlers */ class WC_Log_Handler_Email extends WC_Log_Handler { diff --git a/includes/log-handlers/class-wc-log-handler-file.php b/includes/log-handlers/class-wc-log-handler-file.php index 136603d44d5..0fc59a36a2d 100644 --- a/includes/log-handlers/class-wc-log-handler-file.php +++ b/includes/log-handlers/class-wc-log-handler-file.php @@ -16,7 +16,7 @@ if ( ! defined( 'ABSPATH' ) ) { * * @class WC_Log_Handler_File * @version 1.0.0 - * @package WooCommerce/Classes/Log_Handlers + * @package WooCommerce\Classes\Log_Handlers */ class WC_Log_Handler_File extends WC_Log_Handler { diff --git a/includes/payment-tokens/class-wc-payment-token-cc.php b/includes/payment-tokens/class-wc-payment-token-cc.php index 32abf6c4e38..f658389846a 100644 --- a/includes/payment-tokens/class-wc-payment-token-cc.php +++ b/includes/payment-tokens/class-wc-payment-token-cc.php @@ -17,7 +17,7 @@ if ( ! defined( 'ABSPATH' ) ) { * @class WC_Payment_Token_CC * @version 3.0.0 * @since 2.6.0 - * @package WooCommerce/PaymentTokens + * @package WooCommerce\PaymentTokens */ class WC_Payment_Token_CC extends WC_Payment_Token { diff --git a/includes/payment-tokens/class-wc-payment-token-echeck.php b/includes/payment-tokens/class-wc-payment-token-echeck.php index edb3b0ecf31..4e1e21e7f7a 100644 --- a/includes/payment-tokens/class-wc-payment-token-echeck.php +++ b/includes/payment-tokens/class-wc-payment-token-echeck.php @@ -17,7 +17,7 @@ if ( ! defined( 'ABSPATH' ) ) { * @class WC_Payment_Token_ECheck * @version 3.0.0 * @since 2.6.0 - * @package WooCommerce/PaymentTokens + * @package WooCommerce\PaymentTokens */ class WC_Payment_Token_ECheck extends WC_Payment_Token { diff --git a/includes/queue/class-wc-action-queue.php b/includes/queue/class-wc-action-queue.php index f0e3445f42e..702861f54d3 100644 --- a/includes/queue/class-wc-action-queue.php +++ b/includes/queue/class-wc-action-queue.php @@ -3,7 +3,7 @@ * Action Queue * * @version 3.5.0 - * @package WooCommerce/Interface + * @package WooCommerce\Interface */ if ( ! defined( 'ABSPATH' ) ) { diff --git a/includes/queue/class-wc-queue.php b/includes/queue/class-wc-queue.php index e17bbb6b9f7..80dd4b67a95 100644 --- a/includes/queue/class-wc-queue.php +++ b/includes/queue/class-wc-queue.php @@ -3,7 +3,7 @@ * WC Queue * * @version 3.5.0 - * @package WooCommerce/Interface + * @package WooCommerce\Interface */ if ( ! defined( 'ABSPATH' ) ) { diff --git a/includes/rest-api/Controllers/Version1/class-wc-rest-coupons-v1-controller.php b/includes/rest-api/Controllers/Version1/class-wc-rest-coupons-v1-controller.php new file mode 100644 index 00000000000..f9f312b6609 --- /dev/null +++ b/includes/rest-api/Controllers/Version1/class-wc-rest-coupons-v1-controller.php @@ -0,0 +1,580 @@ +post_type}_query", array( $this, 'query_args' ), 10, 2 ); + } + + /** + * Register the routes for coupons. + */ + public function register_routes() { + register_rest_route( $this->namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( + 'code' => array( + 'description' => __( 'Coupon code.', 'woocommerce' ), + 'required' => true, + 'type' => 'string', + ), + ) ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'type' => 'boolean', + 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'batch_items' ), + 'permission_callback' => array( $this, 'batch_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_batch_schema' ), + ) ); + } + + /** + * Query args. + * + * @param array $args Query args + * @param WP_REST_Request $request Request data. + * @return array + */ + public function query_args( $args, $request ) { + if ( ! empty( $request['code'] ) ) { + $id = wc_get_coupon_id_by_code( $request['code'] ); + $args['post__in'] = array( $id ); + } + + return $args; + } + + /** + * Prepare a single coupon output for response. + * + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $data + */ + public function prepare_item_for_response( $post, $request ) { + $coupon = new WC_Coupon( (int) $post->ID ); + $_data = $coupon->get_data(); + + $format_decimal = array( 'amount', 'minimum_amount', 'maximum_amount' ); + $format_date = array( 'date_created', 'date_modified' ); + $format_date_utc = array( 'date_expires' ); + $format_null = array( 'usage_limit', 'usage_limit_per_user' ); + + // Format decimal values. + foreach ( $format_decimal as $key ) { + $_data[ $key ] = wc_format_decimal( $_data[ $key ], 2 ); + } + + // Format date values. + foreach ( $format_date as $key ) { + $_data[ $key ] = $_data[ $key ] ? wc_rest_prepare_date_response( $_data[ $key ], false ) : null; + } + foreach ( $format_date_utc as $key ) { + $_data[ $key ] = $_data[ $key ] ? wc_rest_prepare_date_response( $_data[ $key ] ) : null; + } + + // Format null values. + foreach ( $format_null as $key ) { + $_data[ $key ] = $_data[ $key ] ? $_data[ $key ] : null; + } + + $data = array( + 'id' => $_data['id'], + 'code' => $_data['code'], + 'date_created' => $_data['date_created'], + 'date_modified' => $_data['date_modified'], + 'discount_type' => $_data['discount_type'], + 'description' => $_data['description'], + 'amount' => $_data['amount'], + 'expiry_date' => $_data['date_expires'], + 'usage_count' => $_data['usage_count'], + 'individual_use' => $_data['individual_use'], + 'product_ids' => $_data['product_ids'], + 'exclude_product_ids' => $_data['excluded_product_ids'], + 'usage_limit' => $_data['usage_limit'], + 'usage_limit_per_user' => $_data['usage_limit_per_user'], + 'limit_usage_to_x_items' => $_data['limit_usage_to_x_items'], + 'free_shipping' => $_data['free_shipping'], + 'product_categories' => $_data['product_categories'], + 'excluded_product_categories' => $_data['excluded_product_categories'], + 'exclude_sale_items' => $_data['exclude_sale_items'], + 'minimum_amount' => $_data['minimum_amount'], + 'maximum_amount' => $_data['maximum_amount'], + 'email_restrictions' => $_data['email_restrictions'], + 'used_by' => $_data['used_by'], + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + $response = rest_ensure_response( $data ); + $response->add_links( $this->prepare_links( $post, $request ) ); + + /** + * Filter the data for a response. + * + * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being + * prepared for the response. + * + * @param WP_REST_Response $response The response object. + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $post, $request ); + } + + /** + * Only return writable props from schema. + * @param array $schema + * @return bool + */ + protected function filter_writable_props( $schema ) { + return empty( $schema['readonly'] ); + } + + /** + * Prepare a single coupon for create or update. + * + * @param WP_REST_Request $request Request object. + * @return WP_Error|stdClass $data Post object. + */ + protected function prepare_item_for_database( $request ) { + $id = isset( $request['id'] ) ? absint( $request['id'] ) : 0; + $coupon = new WC_Coupon( $id ); + $schema = $this->get_item_schema(); + $data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) ); + + // Update to schema to make compatible with CRUD schema. + if ( $request['exclude_product_ids'] ) { + $request['excluded_product_ids'] = $request['exclude_product_ids']; + } + if ( $request['expiry_date'] ) { + $request['date_expires'] = $request['expiry_date']; + } + + // Validate required POST fields. + if ( 'POST' === $request->get_method() && 0 === $coupon->get_id() ) { + if ( empty( $request['code'] ) ) { + return new WP_Error( 'woocommerce_rest_empty_coupon_code', sprintf( __( 'The coupon code cannot be empty.', 'woocommerce' ), 'code' ), array( 'status' => 400 ) ); + } + } + + // Handle all writable props. + foreach ( $data_keys as $key ) { + $value = $request[ $key ]; + + if ( ! is_null( $value ) ) { + switch ( $key ) { + case 'code' : + $coupon_code = wc_format_coupon_code( $value ); + $id = $coupon->get_id() ? $coupon->get_id() : 0; + $id_from_code = wc_get_coupon_id_by_code( $coupon_code, $id ); + + if ( $id_from_code ) { + return new WP_Error( 'woocommerce_rest_coupon_code_already_exists', __( 'The coupon code already exists', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $coupon->set_code( $coupon_code ); + break; + case 'description' : + $coupon->set_description( wp_filter_post_kses( $value ) ); + break; + case 'expiry_date' : + $coupon->set_date_expires( $value ); + break; + default : + if ( is_callable( array( $coupon, "set_{$key}" ) ) ) { + $coupon->{"set_{$key}"}( $value ); + } + break; + } + } + } + + /** + * Filter the query_vars used in `get_items` for the constructed query. + * + * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being + * prepared for insertion. + * + * @param WC_Coupon $coupon The coupon object. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}", $coupon, $request ); + } + + /** + * Create a single item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + if ( ! empty( $request['id'] ) ) { + /* translators: %s: post type */ + return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) ); + } + + $coupon_id = $this->save_coupon( $request ); + if ( is_wp_error( $coupon_id ) ) { + return $coupon_id; + } + + $post = get_post( $coupon_id ); + $this->update_additional_fields_for_object( $post, $request ); + + $this->add_post_meta_fields( $post, $request ); + + /** + * Fires after a single item is created or updated via the REST API. + * + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating item, false when updating. + */ + do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, true ); + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $post, $request ); + $response = rest_ensure_response( $response ); + $response->set_status( 201 ); + $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $post->ID ) ) ); + + return $response; + } + + /** + * Update a single coupon. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function update_item( $request ) { + try { + $post_id = (int) $request['id']; + + if ( empty( $post_id ) || get_post_type( $post_id ) !== $this->post_type ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $coupon_id = $this->save_coupon( $request ); + if ( is_wp_error( $coupon_id ) ) { + return $coupon_id; + } + + $post = get_post( $coupon_id ); + $this->update_additional_fields_for_object( $post, $request ); + + /** + * Fires after a single item is created or updated via the REST API. + * + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating item, false when updating. + */ + do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, false ); + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $post, $request ); + return rest_ensure_response( $response ); + + } catch ( Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Saves a coupon to the database. + * + * @since 3.0.0 + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|int + */ + protected function save_coupon( $request ) { + try { + $coupon = $this->prepare_item_for_database( $request ); + + if ( is_wp_error( $coupon ) ) { + return $coupon; + } + + $coupon->save(); + return $coupon->get_id(); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); + } catch ( WC_REST_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get the Coupon's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => $this->post_type, + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the object.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'code' => array( + 'description' => __( 'Coupon code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'date_created' => array( + 'description' => __( "The date the coupon was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the coupon was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'description' => array( + 'description' => __( 'Coupon description.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'discount_type' => array( + 'description' => __( 'Determines the type of discount that will be applied.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'fixed_cart', + 'enum' => array_keys( wc_get_coupon_types() ), + 'context' => array( 'view', 'edit' ), + ), + 'amount' => array( + 'description' => __( 'The amount of discount. Should always be numeric, even if setting a percentage.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'expiry_date' => array( + 'description' => __( 'UTC DateTime when the coupon expires.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'usage_count' => array( + 'description' => __( 'Number of times the coupon has been used already.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'individual_use' => array( + 'description' => __( 'If true, the coupon can only be used individually. Other applied coupons will be removed from the cart.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'product_ids' => array( + 'description' => __( "List of product IDs the coupon can be used on.", 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + ), + 'exclude_product_ids' => array( + 'description' => __( "List of product IDs the coupon cannot be used on.", 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + ), + 'usage_limit' => array( + 'description' => __( 'How many times the coupon can be used in total.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'usage_limit_per_user' => array( + 'description' => __( 'How many times the coupon can be used per customer.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'limit_usage_to_x_items' => array( + 'description' => __( 'Max number of items in the cart the coupon can be applied to.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'free_shipping' => array( + 'description' => __( 'If true and if the free shipping method requires a coupon, this coupon will enable free shipping.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'product_categories' => array( + 'description' => __( "List of category IDs the coupon applies to.", 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + ), + 'excluded_product_categories' => array( + 'description' => __( "List of category IDs the coupon does not apply to.", 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + ), + 'exclude_sale_items' => array( + 'description' => __( 'If true, this coupon will not be applied to items that have sale prices.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'minimum_amount' => array( + 'description' => __( 'Minimum order amount that needs to be in the cart before coupon applies.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'maximum_amount' => array( + 'description' => __( 'Maximum order amount allowed when using the coupon.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'email_restrictions' => array( + 'description' => __( 'List of email addresses that can use this coupon.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + 'context' => array( 'view', 'edit' ), + ), + 'used_by' => array( + 'description' => __( 'List of user IDs (or guest email addresses) that have used the coupon.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections of attachments. + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + $params['code'] = array( + 'description' => __( 'Limit result set to resources with a specific code.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } +} diff --git a/includes/rest-api/Controllers/Version1/class-wc-rest-customer-downloads-v1-controller.php b/includes/rest-api/Controllers/Version1/class-wc-rest-customer-downloads-v1-controller.php new file mode 100644 index 00000000000..0161f47a0df --- /dev/null +++ b/includes/rest-api/Controllers/Version1/class-wc-rest-customer-downloads-v1-controller.php @@ -0,0 +1,252 @@ +/downloads endpoint. + * + * @author WooThemes + * @category API + * @package Automattic/WooCommerce/RestApi + * @since 3.0.0 + */ + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +/** + * REST API Customers controller class. + * + * @package Automattic/WooCommerce/RestApi + * @extends WC_REST_Controller + */ +class WC_REST_Customer_Downloads_V1_Controller extends WC_REST_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + protected $namespace = 'wc/v1'; + + /** + * Route base. + * + * @var string + */ + protected $rest_base = 'customers/(?P[\d]+)/downloads'; + + /** + * Register the routes for customers. + */ + public function register_routes() { + register_rest_route( $this->namespace, '/' . $this->rest_base, array( + 'args' => array( + 'customer_id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + } + + /** + * Check whether a given request has permission to read customers. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + $customer = get_user_by( 'id', (int) $request['customer_id'] ); + + if ( ! $customer ) { + return new WP_Error( 'woocommerce_rest_customer_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + if ( ! wc_rest_check_user_permissions( 'read', $customer->get_id() ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Get all customer downloads. + * + * @param WP_REST_Request $request + * @return array + */ + public function get_items( $request ) { + $downloads = wc_get_customer_available_downloads( (int) $request['customer_id'] ); + + $data = array(); + foreach ( $downloads as $download_data ) { + $download = $this->prepare_item_for_response( (object) $download_data, $request ); + $download = $this->prepare_response_for_collection( $download ); + $data[] = $download; + } + + return rest_ensure_response( $data ); + } + + /** + * Prepare a single download output for response. + * + * @param stdObject $download Download object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $download, $request ) { + $data = (array) $download; + $data['access_expires'] = $data['access_expires'] ? wc_rest_prepare_date_response( $data['access_expires'] ) : 'never'; + $data['downloads_remaining'] = '' === $data['downloads_remaining'] ? 'unlimited' : $data['downloads_remaining']; + + // Remove "product_name" since it's new in 3.0. + unset( $data['product_name'] ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $download, $request ) ); + + /** + * Filter customer download data returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param stdObject $download Download object used to create response. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'woocommerce_rest_prepare_customer_download', $response, $download, $request ); + } + + /** + * Prepare links for the request. + * + * @param stdClass $download Download object. + * @param WP_REST_Request $request Request object. + * @return array Links for the given customer download. + */ + protected function prepare_links( $download, $request ) { + $base = str_replace( '(?P[\d]+)', $request['customer_id'], $this->rest_base ); + $links = array( + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ), + ), + 'product' => array( + 'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $download->product_id ) ), + ), + 'order' => array( + 'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $download->order_id ) ), + ), + ); + + return $links; + } + + /** + * Get the Customer Download's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'customer_download', + 'type' => 'object', + 'properties' => array( + 'download_url' => array( + 'description' => __( 'Download file URL.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'download_id' => array( + 'description' => __( 'Download ID (MD5).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'product_id' => array( + 'description' => __( 'Downloadable product ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'download_name' => array( + 'description' => __( 'Downloadable file name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'order_id' => array( + 'description' => __( 'Order ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'order_key' => array( + 'description' => __( 'Order key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'downloads_remaining' => array( + 'description' => __( 'Number of downloads remaining.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'access_expires' => array( + 'description' => __( "The date when download access expires, in the site's timezone.", 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'file' => array( + 'description' => __( 'File details.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view' ), + 'readonly' => true, + 'properties' => array( + 'name' => array( + 'description' => __( 'File name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'file' => array( + 'description' => __( 'File URL.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + return array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ); + } +} diff --git a/includes/rest-api/Controllers/Version1/class-wc-rest-customers-v1-controller.php b/includes/rest-api/Controllers/Version1/class-wc-rest-customers-v1-controller.php new file mode 100644 index 00000000000..18b8d9786f6 --- /dev/null +++ b/includes/rest-api/Controllers/Version1/class-wc-rest-customers-v1-controller.php @@ -0,0 +1,924 @@ +namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( + 'email' => array( + 'required' => true, + 'type' => 'string', + 'description' => __( 'New user email address.', 'woocommerce' ), + ), + 'username' => array( + 'required' => 'no' === get_option( 'woocommerce_registration_generate_username', 'yes' ), + 'description' => __( 'New user username.', 'woocommerce' ), + 'type' => 'string', + ), + 'password' => array( + 'required' => 'no' === get_option( 'woocommerce_registration_generate_password', 'no' ), + 'description' => __( 'New user password.', 'woocommerce' ), + 'type' => 'string', + ), + ) ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'type' => 'boolean', + 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), + ), + 'reassign' => array( + 'default' => 0, + 'type' => 'integer', + 'description' => __( 'ID to reassign posts to.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'batch_items' ), + 'permission_callback' => array( $this, 'batch_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_batch_schema' ), + ) ); + } + + /** + * Check whether a given request has permission to read customers. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_user_permissions( 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access create customers. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return bool|WP_Error + */ + public function create_item_permissions_check( $request ) { + if ( ! wc_rest_check_user_permissions( 'create' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to read a customer. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + $id = (int) $request['id']; + + if ( ! wc_rest_check_user_permissions( 'read', $id ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access update a customer. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return bool|WP_Error + */ + public function update_item_permissions_check( $request ) { + $id = (int) $request['id']; + + if ( ! wc_rest_check_user_permissions( 'edit', $id ) ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access delete a customer. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return bool|WP_Error + */ + public function delete_item_permissions_check( $request ) { + $id = (int) $request['id']; + + if ( ! wc_rest_check_user_permissions( 'delete', $id ) ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access batch create, update and delete items. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return bool|WP_Error + */ + public function batch_items_permissions_check( $request ) { + if ( ! wc_rest_check_user_permissions( 'batch' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_batch', __( 'Sorry, you are not allowed to batch manipulate this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Get all customers. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + $prepared_args = array(); + $prepared_args['exclude'] = $request['exclude']; + $prepared_args['include'] = $request['include']; + $prepared_args['order'] = $request['order']; + $prepared_args['number'] = $request['per_page']; + if ( ! empty( $request['offset'] ) ) { + $prepared_args['offset'] = $request['offset']; + } else { + $prepared_args['offset'] = ( $request['page'] - 1 ) * $prepared_args['number']; + } + $orderby_possibles = array( + 'id' => 'ID', + 'include' => 'include', + 'name' => 'display_name', + 'registered_date' => 'registered', + ); + $prepared_args['orderby'] = $orderby_possibles[ $request['orderby'] ]; + $prepared_args['search'] = $request['search']; + + if ( '' !== $prepared_args['search'] ) { + $prepared_args['search'] = '*' . $prepared_args['search'] . '*'; + } + + // Filter by email. + if ( ! empty( $request['email'] ) ) { + $prepared_args['search'] = $request['email']; + $prepared_args['search_columns'] = array( 'user_email' ); + } + + // Filter by role. + if ( 'all' !== $request['role'] ) { + $prepared_args['role'] = $request['role']; + } + + /** + * Filter arguments, before passing to WP_User_Query, when querying users via the REST API. + * + * @see https://developer.wordpress.org/reference/classes/wp_user_query/ + * + * @param array $prepared_args Array of arguments for WP_User_Query. + * @param WP_REST_Request $request The current request. + */ + $prepared_args = apply_filters( 'woocommerce_rest_customer_query', $prepared_args, $request ); + + $query = new WP_User_Query( $prepared_args ); + + $users = array(); + foreach ( $query->results as $user ) { + $data = $this->prepare_item_for_response( $user, $request ); + $users[] = $this->prepare_response_for_collection( $data ); + } + + $response = rest_ensure_response( $users ); + + // Store pagination values for headers then unset for count query. + $per_page = (int) $prepared_args['number']; + $page = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 ); + + $prepared_args['fields'] = 'ID'; + + $total_users = $query->get_total(); + if ( $total_users < 1 ) { + // Out-of-bounds, run the query again without LIMIT for total count. + unset( $prepared_args['number'] ); + unset( $prepared_args['offset'] ); + $count_query = new WP_User_Query( $prepared_args ); + $total_users = $count_query->get_total(); + } + $response->header( 'X-WP-Total', (int) $total_users ); + $max_pages = ceil( $total_users / $per_page ); + $response->header( 'X-WP-TotalPages', (int) $max_pages ); + + $base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) ); + if ( $page > 1 ) { + $prev_page = $page - 1; + if ( $prev_page > $max_pages ) { + $prev_page = $max_pages; + } + $prev_link = add_query_arg( 'page', $prev_page, $base ); + $response->link_header( 'prev', $prev_link ); + } + if ( $max_pages > $page ) { + $next_page = $page + 1; + $next_link = add_query_arg( 'page', $next_page, $base ); + $response->link_header( 'next', $next_link ); + } + + return $response; + } + + /** + * Create a single customer. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + try { + if ( ! empty( $request['id'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_customer_exists', __( 'Cannot create existing resource.', 'woocommerce' ), 400 ); + } + + // Sets the username. + $request['username'] = ! empty( $request['username'] ) ? $request['username'] : ''; + + // Sets the password. + $request['password'] = ! empty( $request['password'] ) ? $request['password'] : ''; + + // Create customer. + $customer = new WC_Customer; + $customer->set_username( $request['username'] ); + $customer->set_password( $request['password'] ); + $customer->set_email( $request['email'] ); + $this->update_customer_meta_fields( $customer, $request ); + $customer->save(); + + if ( ! $customer->get_id() ) { + throw new WC_REST_Exception( 'woocommerce_rest_cannot_create', __( 'This resource cannot be created.', 'woocommerce' ), 400 ); + } + + $user_data = get_userdata( $customer->get_id() ); + $this->update_additional_fields_for_object( $user_data, $request ); + + /** + * Fires after a customer is created or updated via the REST API. + * + * @param WP_User $user_data Data used to create the customer. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating customer, false when updating customer. + */ + do_action( 'woocommerce_rest_insert_customer', $user_data, $request, true ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $user_data, $request ); + $response = rest_ensure_response( $response ); + $response->set_status( 201 ); + $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $customer->get_id() ) ) ); + + return $response; + } catch ( Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get a single customer. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + $id = (int) $request['id']; + $user_data = get_userdata( $id ); + + if ( empty( $id ) || empty( $user_data->ID ) ) { + return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $customer = $this->prepare_item_for_response( $user_data, $request ); + $response = rest_ensure_response( $customer ); + + return $response; + } + + /** + * Update a single user. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function update_item( $request ) { + try { + $id = (int) $request['id']; + $customer = new WC_Customer( $id ); + + if ( ! $customer->get_id() ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), 400 ); + } + + if ( ! empty( $request['email'] ) && email_exists( $request['email'] ) && $request['email'] !== $customer->get_email() ) { + throw new WC_REST_Exception( 'woocommerce_rest_customer_invalid_email', __( 'Email address is invalid.', 'woocommerce' ), 400 ); + } + + if ( ! empty( $request['username'] ) && $request['username'] !== $customer->get_username() ) { + throw new WC_REST_Exception( 'woocommerce_rest_customer_invalid_argument', __( "Username isn't editable.", 'woocommerce' ), 400 ); + } + + // Customer email. + if ( isset( $request['email'] ) ) { + $customer->set_email( sanitize_email( $request['email'] ) ); + } + + // Customer password. + if ( isset( $request['password'] ) ) { + $customer->set_password( $request['password'] ); + } + + $this->update_customer_meta_fields( $customer, $request ); + $customer->save(); + + $user_data = get_userdata( $customer->get_id() ); + $this->update_additional_fields_for_object( $user_data, $request ); + + if ( ! is_user_member_of_blog( $user_data->ID ) ) { + $user_data->add_role( 'customer' ); + } + + /** + * Fires after a customer is created or updated via the REST API. + * + * @param WP_User $customer Data used to create the customer. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating customer, false when updating customer. + */ + do_action( 'woocommerce_rest_insert_customer', $user_data, $request, false ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $user_data, $request ); + $response = rest_ensure_response( $response ); + return $response; + } catch ( Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Delete a single customer. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function delete_item( $request ) { + $id = (int) $request['id']; + $reassign = isset( $request['reassign'] ) ? absint( $request['reassign'] ) : null; + $force = isset( $request['force'] ) ? (bool) $request['force'] : false; + + // We don't support trashing for this type, error out. + if ( ! $force ) { + return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Customers do not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); + } + + $user_data = get_userdata( $id ); + if ( ! $user_data ) { + return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + if ( ! empty( $reassign ) ) { + if ( $reassign === $id || ! get_userdata( $reassign ) ) { + return new WP_Error( 'woocommerce_rest_customer_invalid_reassign', __( 'Invalid resource id for reassignment.', 'woocommerce' ), array( 'status' => 400 ) ); + } + } + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $user_data, $request ); + + /** Include admin customer functions to get access to wp_delete_user() */ + require_once ABSPATH . 'wp-admin/includes/user.php'; + + $customer = new WC_Customer( $id ); + + if ( ! is_null( $reassign ) ) { + $result = $customer->delete_and_reassign( $reassign ); + } else { + $result = $customer->delete(); + } + + if ( ! $result ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'The resource cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) ); + } + + /** + * Fires after a customer is deleted via the REST API. + * + * @param WP_User $user_data User data. + * @param WP_REST_Response $response The response returned from the API. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( 'woocommerce_rest_delete_customer', $user_data, $response, $request ); + + return $response; + } + + /** + * Prepare a single customer output for response. + * + * @param WP_User $user_data User object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $user_data, $request ) { + $customer = new WC_Customer( $user_data->ID ); + $_data = $customer->get_data(); + $last_order = wc_get_customer_last_order( $customer->get_id() ); + $format_date = array( 'date_created', 'date_modified' ); + + // Format date values. + foreach ( $format_date as $key ) { + $_data[ $key ] = $_data[ $key ] ? wc_rest_prepare_date_response( $_data[ $key ] ) : null; // v1 API used UTC. + } + + $data = array( + 'id' => $_data['id'], + 'date_created' => $_data['date_created'], + 'date_modified' => $_data['date_modified'], + 'email' => $_data['email'], + 'first_name' => $_data['first_name'], + 'last_name' => $_data['last_name'], + 'username' => $_data['username'], + 'last_order' => array( + 'id' => is_object( $last_order ) ? $last_order->get_id() : null, + 'date' => is_object( $last_order ) ? wc_rest_prepare_date_response( $last_order->get_date_created() ) : null, // v1 API used UTC. + ), + 'orders_count' => $customer->get_order_count(), + 'total_spent' => $customer->get_total_spent(), + 'avatar_url' => $customer->get_avatar_url(), + 'billing' => $_data['billing'], + 'shipping' => $_data['shipping'], + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + $response = rest_ensure_response( $data ); + $response->add_links( $this->prepare_links( $user_data ) ); + + /** + * Filter customer data returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param WP_User $user_data User object used to create response. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'woocommerce_rest_prepare_customer', $response, $user_data, $request ); + } + + /** + * Update customer meta fields. + * + * @param WC_Customer $customer + * @param WP_REST_Request $request + */ + protected function update_customer_meta_fields( $customer, $request ) { + $schema = $this->get_item_schema(); + + // Customer first name. + if ( isset( $request['first_name'] ) ) { + $customer->set_first_name( wc_clean( $request['first_name'] ) ); + } + + // Customer last name. + if ( isset( $request['last_name'] ) ) { + $customer->set_last_name( wc_clean( $request['last_name'] ) ); + } + + // Customer billing address. + if ( isset( $request['billing'] ) ) { + foreach ( array_keys( $schema['properties']['billing']['properties'] ) as $field ) { + if ( isset( $request['billing'][ $field ] ) && is_callable( array( $customer, "set_billing_{$field}" ) ) ) { + $customer->{"set_billing_{$field}"}( $request['billing'][ $field ] ); + } + } + } + + // Customer shipping address. + if ( isset( $request['shipping'] ) ) { + foreach ( array_keys( $schema['properties']['shipping']['properties'] ) as $field ) { + if ( isset( $request['shipping'][ $field ] ) && is_callable( array( $customer, "set_shipping_{$field}" ) ) ) { + $customer->{"set_shipping_{$field}"}( $request['shipping'][ $field ] ); + } + } + } + } + + /** + * Prepare links for the request. + * + * @param WP_User $customer Customer object. + * @return array Links for the given customer. + */ + protected function prepare_links( $customer ) { + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $customer->ID ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + + return $links; + } + + /** + * Get the Customer's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'customer', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( 'The date the customer was created, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( 'The date the customer was last modified, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'email' => array( + 'description' => __( 'The email address for the customer.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'email', + 'context' => array( 'view', 'edit' ), + ), + 'first_name' => array( + 'description' => __( 'Customer first name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'last_name' => array( + 'description' => __( 'Customer last name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'username' => array( + 'description' => __( 'Customer login name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_user', + ), + ), + 'password' => array( + 'description' => __( 'Customer password.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'edit' ), + ), + 'last_order' => array( + 'description' => __( 'Last order data.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'properties' => array( + 'id' => array( + 'description' => __( 'Last order ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date' => array( + 'description' => __( 'The date of the customer last order, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + 'orders_count' => array( + 'description' => __( 'Quantity of orders made by the customer.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total_spent' => array( + 'description' => __( 'Total amount spent.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'avatar_url' => array( + 'description' => __( 'Avatar URL.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'billing' => array( + 'description' => __( 'List of billing address data.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'first_name' => array( + 'description' => __( 'First name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'last_name' => array( + 'description' => __( 'Last name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'company' => array( + 'description' => __( 'Company name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_1' => array( + 'description' => __( 'Address line 1.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_2' => array( + 'description' => __( 'Address line 2.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'city' => array( + 'description' => __( 'City name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'state' => array( + 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'postcode' => array( + 'description' => __( 'Postal code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'country' => array( + 'description' => __( 'ISO code of the country.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'email' => array( + 'description' => __( 'Email address.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'email', + 'context' => array( 'view', 'edit' ), + ), + 'phone' => array( + 'description' => __( 'Phone number.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'shipping' => array( + 'description' => __( 'List of shipping address data.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'first_name' => array( + 'description' => __( 'First name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'last_name' => array( + 'description' => __( 'Last name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'company' => array( + 'description' => __( 'Company name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_1' => array( + 'description' => __( 'Address line 1.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_2' => array( + 'description' => __( 'Address line 2.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'city' => array( + 'description' => __( 'City name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'state' => array( + 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'postcode' => array( + 'description' => __( 'Postal code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'country' => array( + 'description' => __( 'ISO code of the country.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get role names. + * + * @return array + */ + protected function get_role_names() { + global $wp_roles; + + return array_keys( $wp_roles->role_names ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + $params['context']['default'] = 'view'; + + $params['exclude'] = array( + 'description' => __( 'Ensure result set excludes specific IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'default' => array(), + 'sanitize_callback' => 'wp_parse_id_list', + ); + $params['include'] = array( + 'description' => __( 'Limit result set to specific IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'default' => array(), + 'sanitize_callback' => 'wp_parse_id_list', + ); + $params['offset'] = array( + 'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['order'] = array( + 'default' => 'asc', + 'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ), + 'enum' => array( 'asc', 'desc' ), + 'sanitize_callback' => 'sanitize_key', + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['orderby'] = array( + 'default' => 'name', + 'description' => __( 'Sort collection by object attribute.', 'woocommerce' ), + 'enum' => array( + 'id', + 'include', + 'name', + 'registered_date', + ), + 'sanitize_callback' => 'sanitize_key', + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['email'] = array( + 'description' => __( 'Limit result set to resources with a specific email.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'email', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['role'] = array( + 'description' => __( 'Limit result set to resources with a specific role.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'customer', + 'enum' => array_merge( array( 'all' ), $this->get_role_names() ), + 'validate_callback' => 'rest_validate_request_arg', + ); + return $params; + } +} diff --git a/includes/rest-api/Controllers/Version1/class-wc-rest-order-notes-v1-controller.php b/includes/rest-api/Controllers/Version1/class-wc-rest-order-notes-v1-controller.php new file mode 100644 index 00000000000..731e5c9c4e6 --- /dev/null +++ b/includes/rest-api/Controllers/Version1/class-wc-rest-order-notes-v1-controller.php @@ -0,0 +1,439 @@ +/notes endpoint. + * + * @author WooThemes + * @category API + * @package Automattic/WooCommerce/RestApi + * @since 3.0.0 + */ + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +/** + * REST API Order Notes controller class. + * + * @package Automattic/WooCommerce/RestApi + * @extends WC_REST_Controller + */ +class WC_REST_Order_Notes_V1_Controller extends WC_REST_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + protected $namespace = 'wc/v1'; + + /** + * Route base. + * + * @var string + */ + protected $rest_base = 'orders/(?P[\d]+)/notes'; + + /** + * Post type. + * + * @var string + */ + protected $post_type = 'shop_order'; + + /** + * Register the routes for order notes. + */ + public function register_routes() { + register_rest_route( $this->namespace, '/' . $this->rest_base, array( + 'args' => array( + 'order_id' => array( + 'description' => __( 'The order ID.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( + 'note' => array( + 'type' => 'string', + 'description' => __( 'Order note content.', 'woocommerce' ), + 'required' => true, + ), + ) ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + ), + 'order_id' => array( + 'description' => __( 'The order ID.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'type' => 'boolean', + 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + } + + /** + * Check whether a given request has permission to read order notes. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_post_permissions( $this->post_type, 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access create order notes. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return bool|WP_Error + */ + public function create_item_permissions_check( $request ) { + if ( ! wc_rest_check_post_permissions( $this->post_type, 'create' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to read a order note. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + $order = wc_get_order( (int) $request['order_id'] ); + + if ( $order && ! wc_rest_check_post_permissions( $this->post_type, 'read', $order->get_id() ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access delete a order note. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return bool|WP_Error + */ + public function delete_item_permissions_check( $request ) { + $order = wc_get_order( (int) $request['order_id'] ); + + if ( $order && ! wc_rest_check_post_permissions( $this->post_type, 'delete', $order->get_id() ) ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Get order notes from an order. + * + * @param WP_REST_Request $request + * + * @return array|WP_Error + */ + public function get_items( $request ) { + $order = wc_get_order( (int) $request['order_id'] ); + + if ( ! $order || $this->post_type !== $order->get_type() ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid order ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $args = array( + 'post_id' => $order->get_id(), + 'approve' => 'approve', + 'type' => 'order_note', + ); + + remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 ); + + $notes = get_comments( $args ); + + add_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 ); + + $data = array(); + foreach ( $notes as $note ) { + $order_note = $this->prepare_item_for_response( $note, $request ); + $order_note = $this->prepare_response_for_collection( $order_note ); + $data[] = $order_note; + } + + return rest_ensure_response( $data ); + } + + /** + * Create a single order note. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + if ( ! empty( $request['id'] ) ) { + /* translators: %s: post type */ + return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) ); + } + + $order = wc_get_order( (int) $request['order_id'] ); + + if ( ! $order || $this->post_type !== $order->get_type() ) { + return new WP_Error( 'woocommerce_rest_order_invalid_id', __( 'Invalid order ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + // Create the note. + $note_id = $order->add_order_note( $request['note'], $request['customer_note'] ); + + if ( ! $note_id ) { + return new WP_Error( 'woocommerce_api_cannot_create_order_note', __( 'Cannot create order note, please try again.', 'woocommerce' ), array( 'status' => 500 ) ); + } + + $note = get_comment( $note_id ); + $this->update_additional_fields_for_object( $note, $request ); + + /** + * Fires after a order note is created or updated via the REST API. + * + * @param WP_Comment $note New order note object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating item, false when updating. + */ + do_action( 'woocommerce_rest_insert_order_note', $note, $request, true ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $note, $request ); + $response = rest_ensure_response( $response ); + $response->set_status( 201 ); + $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, str_replace( '(?P[\d]+)', $order->get_id(), $this->rest_base ), $note_id ) ) ); + + return $response; + } + + /** + * Get a single order note. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + $id = (int) $request['id']; + $order = wc_get_order( (int) $request['order_id'] ); + + if ( ! $order || $this->post_type !== $order->get_type() ) { + return new WP_Error( 'woocommerce_rest_order_invalid_id', __( 'Invalid order ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $note = get_comment( $id ); + + if ( empty( $id ) || empty( $note ) || intval( $note->comment_post_ID ) !== intval( $order->get_id() ) ) { + return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $order_note = $this->prepare_item_for_response( $note, $request ); + $response = rest_ensure_response( $order_note ); + + return $response; + } + + /** + * Delete a single order note. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error + */ + public function delete_item( $request ) { + $id = (int) $request['id']; + $force = isset( $request['force'] ) ? (bool) $request['force'] : false; + + // We don't support trashing for this type, error out. + if ( ! $force ) { + return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Webhooks do not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); + } + + $order = wc_get_order( (int) $request['order_id'] ); + + if ( ! $order || $this->post_type !== $order->get_type() ) { + return new WP_Error( 'woocommerce_rest_order_invalid_id', __( 'Invalid order ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $note = get_comment( $id ); + + if ( empty( $id ) || empty( $note ) || intval( $note->comment_post_ID ) !== intval( $order->get_id() ) ) { + return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $note, $request ); + + $result = wc_delete_order_note( $note->comment_ID ); + + if ( ! $result ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), 'order_note' ), array( 'status' => 500 ) ); + } + + /** + * Fires after a order note is deleted or trashed via the REST API. + * + * @param WP_Comment $note The deleted or trashed order note. + * @param WP_REST_Response $response The response data. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( 'woocommerce_rest_delete_order_note', $note, $response, $request ); + + return $response; + } + + /** + * Prepare a single order note output for response. + * + * @param WP_Comment $note Order note object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $note, $request ) { + $data = array( + 'id' => (int) $note->comment_ID, + 'date_created' => wc_rest_prepare_date_response( $note->comment_date_gmt ), + 'note' => $note->comment_content, + 'customer_note' => (bool) get_comment_meta( $note->comment_ID, 'is_customer_note', true ), + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $note ) ); + + /** + * Filter order note object returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param WP_Comment $note Order note object used to create response. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'woocommerce_rest_prepare_order_note', $response, $note, $request ); + } + + /** + * Prepare links for the request. + * + * @param WP_Comment $note Delivery order_note object. + * @return array Links for the given order note. + */ + protected function prepare_links( $note ) { + $order_id = (int) $note->comment_post_ID; + $base = str_replace( '(?P[\d]+)', $order_id, $this->rest_base ); + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $note->comment_ID ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ), + ), + 'up' => array( + 'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $order_id ) ), + ), + ); + + return $links; + } + + /** + * Get the Order Notes schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'order_note', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( "The date the order note was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'note' => array( + 'description' => __( 'Order note.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'customer_note' => array( + 'description' => __( 'Shows/define if the note is only for reference or for the customer (the user will be notified).', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + return array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ); + } +} diff --git a/includes/rest-api/Controllers/Version1/class-wc-rest-order-refunds-v1-controller.php b/includes/rest-api/Controllers/Version1/class-wc-rest-order-refunds-v1-controller.php new file mode 100644 index 00000000000..d5a02e06ac0 --- /dev/null +++ b/includes/rest-api/Controllers/Version1/class-wc-rest-order-refunds-v1-controller.php @@ -0,0 +1,530 @@ +/refunds endpoint. + * + * @author WooThemes + * @category API + * @package Automattic/WooCommerce/RestApi + * @since 2.6.0 + */ + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +/** + * REST API Order Refunds controller class. + * + * @package Automattic/WooCommerce/RestApi + * @extends WC_REST_Orders_V1_Controller + */ +class WC_REST_Order_Refunds_V1_Controller extends WC_REST_Orders_V1_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + protected $namespace = 'wc/v1'; + + /** + * Route base. + * + * @var string + */ + protected $rest_base = 'orders/(?P[\d]+)/refunds'; + + /** + * Post type. + * + * @var string + */ + protected $post_type = 'shop_order_refund'; + + /** + * Order refunds actions. + */ + public function __construct() { + add_filter( "woocommerce_rest_{$this->post_type}_trashable", '__return_false' ); + add_filter( "woocommerce_rest_{$this->post_type}_query", array( $this, 'query_args' ), 10, 2 ); + } + + /** + * Register the routes for order refunds. + */ + public function register_routes() { + register_rest_route( $this->namespace, '/' . $this->rest_base, array( + 'args' => array( + 'order_id' => array( + 'description' => __( 'The order ID.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + 'args' => array( + 'order_id' => array( + 'description' => __( 'The order ID.', 'woocommerce' ), + 'type' => 'integer', + ), + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => true, + 'type' => 'boolean', + 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + } + + /** + * Prepare a single order refund output for response. + * + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + * + * @return WP_Error|WP_REST_Response + */ + public function prepare_item_for_response( $post, $request ) { + $order = wc_get_order( (int) $request['order_id'] ); + + if ( ! $order ) { + return new WP_Error( 'woocommerce_rest_invalid_order_id', __( 'Invalid order ID.', 'woocommerce' ), 404 ); + } + + $refund = wc_get_order( $post ); + + if ( ! $refund || $refund->get_parent_id() !== $order->get_id() ) { + return new WP_Error( 'woocommerce_rest_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 404 ); + } + + $dp = is_null( $request['dp'] ) ? wc_get_price_decimals() : absint( $request['dp'] ); + + $data = array( + 'id' => $refund->get_id(), + 'date_created' => wc_rest_prepare_date_response( $refund->get_date_created() ), + 'amount' => wc_format_decimal( $refund->get_amount(), $dp ), + 'reason' => $refund->get_reason(), + 'line_items' => array(), + ); + + // Add line items. + foreach ( $refund->get_items() as $item_id => $item ) { + $product = $refund->get_product_from_item( $item ); + $product_id = 0; + $variation_id = 0; + $product_sku = null; + + // Check if the product exists. + if ( is_object( $product ) ) { + $product_id = $item->get_product_id(); + $variation_id = $item->get_variation_id(); + $product_sku = $product->get_sku(); + } + + $item_meta = array(); + + $hideprefix = 'true' === $request['all_item_meta'] ? null : '_'; + + foreach ( $item->get_formatted_meta_data( $hideprefix, true ) as $meta_key => $formatted_meta ) { + $item_meta[] = array( + 'key' => $formatted_meta->key, + 'label' => $formatted_meta->display_key, + 'value' => wc_clean( $formatted_meta->display_value ), + ); + } + + $line_item = array( + 'id' => $item_id, + 'name' => $item['name'], + 'sku' => $product_sku, + 'product_id' => (int) $product_id, + 'variation_id' => (int) $variation_id, + 'quantity' => wc_stock_amount( $item['qty'] ), + 'tax_class' => ! empty( $item['tax_class'] ) ? $item['tax_class'] : '', + 'price' => wc_format_decimal( $refund->get_item_total( $item, false, false ), $dp ), + 'subtotal' => wc_format_decimal( $refund->get_line_subtotal( $item, false, false ), $dp ), + 'subtotal_tax' => wc_format_decimal( $item['line_subtotal_tax'], $dp ), + 'total' => wc_format_decimal( $refund->get_line_total( $item, false, false ), $dp ), + 'total_tax' => wc_format_decimal( $item['line_tax'], $dp ), + 'taxes' => array(), + 'meta' => $item_meta, + ); + + $item_line_taxes = maybe_unserialize( $item['line_tax_data'] ); + if ( isset( $item_line_taxes['total'] ) ) { + $line_tax = array(); + + foreach ( $item_line_taxes['total'] as $tax_rate_id => $tax ) { + $line_tax[ $tax_rate_id ] = array( + 'id' => $tax_rate_id, + 'total' => $tax, + 'subtotal' => '', + ); + } + + foreach ( $item_line_taxes['subtotal'] as $tax_rate_id => $tax ) { + $line_tax[ $tax_rate_id ]['subtotal'] = $tax; + } + + $line_item['taxes'] = array_values( $line_tax ); + } + + $data['line_items'][] = $line_item; + } + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $refund, $request ) ); + + /** + * Filter the data for a response. + * + * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being + * prepared for the response. + * + * @param WP_REST_Response $response The response object. + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $post, $request ); + } + + /** + * Prepare links for the request. + * + * @param WC_Order_Refund $refund Comment object. + * @param WP_REST_Request $request Request object. + * @return array Links for the given order refund. + */ + protected function prepare_links( $refund, $request ) { + $order_id = $refund->get_parent_id(); + $base = str_replace( '(?P[\d]+)', $order_id, $this->rest_base ); + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $refund->get_id() ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ), + ), + 'up' => array( + 'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $order_id ) ), + ), + ); + + return $links; + } + + /** + * Query args. + * + * @param array $args Request args. + * @param WP_REST_Request $request Request object. + * @return array + */ + public function query_args( $args, $request ) { + $args['post_status'] = array_keys( wc_get_order_statuses() ); + $args['post_parent__in'] = array( absint( $request['order_id'] ) ); + + return $args; + } + + /** + * Create a single item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + if ( ! empty( $request['id'] ) ) { + /* translators: %s: post type */ + return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) ); + } + + $order_data = get_post( (int) $request['order_id'] ); + + if ( empty( $order_data ) ) { + return new WP_Error( 'woocommerce_rest_invalid_order', __( 'Order is invalid', 'woocommerce' ), 400 ); + } + + if ( 0 > $request['amount'] ) { + return new WP_Error( 'woocommerce_rest_invalid_order_refund', __( 'Refund amount must be greater than zero.', 'woocommerce' ), 400 ); + } + + // Create the refund. + $refund = wc_create_refund( array( + 'order_id' => $order_data->ID, + 'amount' => $request['amount'], + 'reason' => empty( $request['reason'] ) ? null : $request['reason'], + 'refund_payment' => is_bool( $request['api_refund'] ) ? $request['api_refund'] : true, + 'restock_items' => true, + ) ); + + if ( is_wp_error( $refund ) ) { + return new WP_Error( 'woocommerce_rest_cannot_create_order_refund', $refund->get_error_message(), 500 ); + } + + if ( ! $refund ) { + return new WP_Error( 'woocommerce_rest_cannot_create_order_refund', __( 'Cannot create order refund, please try again.', 'woocommerce' ), 500 ); + } + + $post = get_post( $refund->get_id() ); + $this->update_additional_fields_for_object( $post, $request ); + + /** + * Fires after a single item is created or updated via the REST API. + * + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating item, false when updating. + */ + do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, true ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $post, $request ); + $response = rest_ensure_response( $response ); + $response->set_status( 201 ); + $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $post->ID ) ) ); + + return $response; + } + + /** + * Get the Order's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => $this->post_type, + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( "The date the order refund was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'amount' => array( + 'description' => __( 'Refund amount.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'reason' => array( + 'description' => __( 'Reason for refund.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'line_items' => array( + 'description' => __( 'Line items data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Product name.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'sku' => array( + 'description' => __( 'Product SKU.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'product_id' => array( + 'description' => __( 'Product ID.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'variation_id' => array( + 'description' => __( 'Variation ID, if applicable.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'quantity' => array( + 'description' => __( 'Quantity ordered.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'tax_class' => array( + 'description' => __( 'Tax class of product.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'price' => array( + 'description' => __( 'Product price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'subtotal' => array( + 'description' => __( 'Line subtotal (before discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'subtotal_tax' => array( + 'description' => __( 'Line subtotal tax (before discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Line total (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total_tax' => array( + 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'taxes' => array( + 'description' => __( 'Line taxes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Tax rate ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Tax total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'subtotal' => array( + 'description' => __( 'Tax subtotal.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + 'meta' => array( + 'description' => __( 'Line item meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'label' => array( + 'description' => __( 'Meta label.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + ), + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + $params['dp'] = array( + 'default' => wc_get_price_decimals(), + 'description' => __( 'Number of decimal points to use in each resource.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } +} diff --git a/includes/rest-api/Controllers/Version1/class-wc-rest-orders-v1-controller.php b/includes/rest-api/Controllers/Version1/class-wc-rest-orders-v1-controller.php new file mode 100644 index 00000000000..52d4487a7bf --- /dev/null +++ b/includes/rest-api/Controllers/Version1/class-wc-rest-orders-v1-controller.php @@ -0,0 +1,1631 @@ +post_type}_query", array( $this, 'query_args' ), 10, 2 ); + } + + /** + * Register the routes for orders. + */ + public function register_routes() { + register_rest_route( $this->namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'type' => 'boolean', + 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'batch_items' ), + 'permission_callback' => array( $this, 'batch_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_batch_schema' ), + ) ); + } + + /** + * Prepare a single order output for response. + * + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $data + */ + public function prepare_item_for_response( $post, $request ) { + $order = wc_get_order( $post ); + $dp = is_null( $request['dp'] ) ? wc_get_price_decimals() : absint( $request['dp'] ); + + $data = array( + 'id' => $order->get_id(), + 'parent_id' => $order->get_parent_id(), + 'status' => $order->get_status(), + 'order_key' => $order->get_order_key(), + 'number' => $order->get_order_number(), + 'currency' => $order->get_currency(), + 'version' => $order->get_version(), + 'prices_include_tax' => $order->get_prices_include_tax(), + 'date_created' => wc_rest_prepare_date_response( $order->get_date_created() ), // v1 API used UTC. + 'date_modified' => wc_rest_prepare_date_response( $order->get_date_modified() ), // v1 API used UTC. + 'customer_id' => $order->get_customer_id(), + 'discount_total' => wc_format_decimal( $order->get_total_discount(), $dp ), + 'discount_tax' => wc_format_decimal( $order->get_discount_tax(), $dp ), + 'shipping_total' => wc_format_decimal( $order->get_shipping_total(), $dp ), + 'shipping_tax' => wc_format_decimal( $order->get_shipping_tax(), $dp ), + 'cart_tax' => wc_format_decimal( $order->get_cart_tax(), $dp ), + 'total' => wc_format_decimal( $order->get_total(), $dp ), + 'total_tax' => wc_format_decimal( $order->get_total_tax(), $dp ), + 'billing' => array(), + 'shipping' => array(), + 'payment_method' => $order->get_payment_method(), + 'payment_method_title' => $order->get_payment_method_title(), + 'transaction_id' => $order->get_transaction_id(), + 'customer_ip_address' => $order->get_customer_ip_address(), + 'customer_user_agent' => $order->get_customer_user_agent(), + 'created_via' => $order->get_created_via(), + 'customer_note' => $order->get_customer_note(), + 'date_completed' => wc_rest_prepare_date_response( $order->get_date_completed(), false ), // v1 API used local time. + 'date_paid' => wc_rest_prepare_date_response( $order->get_date_paid(), false ), // v1 API used local time. + 'cart_hash' => $order->get_cart_hash(), + 'line_items' => array(), + 'tax_lines' => array(), + 'shipping_lines' => array(), + 'fee_lines' => array(), + 'coupon_lines' => array(), + 'refunds' => array(), + ); + + // Add addresses. + $data['billing'] = $order->get_address( 'billing' ); + $data['shipping'] = $order->get_address( 'shipping' ); + + // Add line items. + foreach ( $order->get_items() as $item_id => $item ) { + $product = $order->get_product_from_item( $item ); + $product_id = 0; + $variation_id = 0; + $product_sku = null; + + // Check if the product exists. + if ( is_object( $product ) ) { + $product_id = $item->get_product_id(); + $variation_id = $item->get_variation_id(); + $product_sku = $product->get_sku(); + } + + $item_meta = array(); + + $hideprefix = 'true' === $request['all_item_meta'] ? null : '_'; + + foreach ( $item->get_formatted_meta_data( $hideprefix, true ) as $meta_key => $formatted_meta ) { + $item_meta[] = array( + 'key' => $formatted_meta->key, + 'label' => $formatted_meta->display_key, + 'value' => wc_clean( $formatted_meta->display_value ), + ); + } + + $line_item = array( + 'id' => $item_id, + 'name' => $item['name'], + 'sku' => $product_sku, + 'product_id' => (int) $product_id, + 'variation_id' => (int) $variation_id, + 'quantity' => wc_stock_amount( $item['qty'] ), + 'tax_class' => ! empty( $item['tax_class'] ) ? $item['tax_class'] : '', + 'price' => wc_format_decimal( $order->get_item_total( $item, false, false ), $dp ), + 'subtotal' => wc_format_decimal( $order->get_line_subtotal( $item, false, false ), $dp ), + 'subtotal_tax' => wc_format_decimal( $item['line_subtotal_tax'], $dp ), + 'total' => wc_format_decimal( $order->get_line_total( $item, false, false ), $dp ), + 'total_tax' => wc_format_decimal( $item['line_tax'], $dp ), + 'taxes' => array(), + 'meta' => $item_meta, + ); + + $item_line_taxes = maybe_unserialize( $item['line_tax_data'] ); + if ( isset( $item_line_taxes['total'] ) ) { + $line_tax = array(); + + foreach ( $item_line_taxes['total'] as $tax_rate_id => $tax ) { + $line_tax[ $tax_rate_id ] = array( + 'id' => $tax_rate_id, + 'total' => $tax, + 'subtotal' => '', + ); + } + + foreach ( $item_line_taxes['subtotal'] as $tax_rate_id => $tax ) { + $line_tax[ $tax_rate_id ]['subtotal'] = $tax; + } + + $line_item['taxes'] = array_values( $line_tax ); + } + + $data['line_items'][] = $line_item; + } + + // Add taxes. + foreach ( $order->get_items( 'tax' ) as $key => $tax ) { + $tax_line = array( + 'id' => $key, + 'rate_code' => $tax['name'], + 'rate_id' => $tax['rate_id'], + 'label' => isset( $tax['label'] ) ? $tax['label'] : $tax['name'], + 'compound' => (bool) $tax['compound'], + 'tax_total' => wc_format_decimal( $tax['tax_amount'], $dp ), + 'shipping_tax_total' => wc_format_decimal( $tax['shipping_tax_amount'], $dp ), + ); + + $data['tax_lines'][] = $tax_line; + } + + // Add shipping. + foreach ( $order->get_shipping_methods() as $shipping_item_id => $shipping_item ) { + $shipping_line = array( + 'id' => $shipping_item_id, + 'method_title' => $shipping_item['name'], + 'method_id' => $shipping_item['method_id'], + 'total' => wc_format_decimal( $shipping_item['cost'], $dp ), + 'total_tax' => wc_format_decimal( '', $dp ), + 'taxes' => array(), + ); + + $shipping_taxes = $shipping_item->get_taxes(); + + if ( ! empty( $shipping_taxes['total'] ) ) { + $shipping_line['total_tax'] = wc_format_decimal( array_sum( $shipping_taxes['total'] ), $dp ); + + foreach ( $shipping_taxes['total'] as $tax_rate_id => $tax ) { + $shipping_line['taxes'][] = array( + 'id' => $tax_rate_id, + 'total' => $tax, + ); + } + } + + $data['shipping_lines'][] = $shipping_line; + } + + // Add fees. + foreach ( $order->get_fees() as $fee_item_id => $fee_item ) { + $fee_line = array( + 'id' => $fee_item_id, + 'name' => $fee_item['name'], + 'tax_class' => ! empty( $fee_item['tax_class'] ) ? $fee_item['tax_class'] : '', + 'tax_status' => 'taxable', + 'total' => wc_format_decimal( $order->get_line_total( $fee_item ), $dp ), + 'total_tax' => wc_format_decimal( $order->get_line_tax( $fee_item ), $dp ), + 'taxes' => array(), + ); + + $fee_line_taxes = maybe_unserialize( $fee_item['line_tax_data'] ); + if ( isset( $fee_line_taxes['total'] ) ) { + $fee_tax = array(); + + foreach ( $fee_line_taxes['total'] as $tax_rate_id => $tax ) { + $fee_tax[ $tax_rate_id ] = array( + 'id' => $tax_rate_id, + 'total' => $tax, + 'subtotal' => '', + ); + } + + if ( isset( $fee_line_taxes['subtotal'] ) ) { + foreach ( $fee_line_taxes['subtotal'] as $tax_rate_id => $tax ) { + $fee_tax[ $tax_rate_id ]['subtotal'] = $tax; + } + } + + $fee_line['taxes'] = array_values( $fee_tax ); + } + + $data['fee_lines'][] = $fee_line; + } + + // Add coupons. + foreach ( $order->get_items( 'coupon' ) as $coupon_item_id => $coupon_item ) { + $coupon_line = array( + 'id' => $coupon_item_id, + 'code' => $coupon_item['name'], + 'discount' => wc_format_decimal( $coupon_item['discount_amount'], $dp ), + 'discount_tax' => wc_format_decimal( $coupon_item['discount_amount_tax'], $dp ), + ); + + $data['coupon_lines'][] = $coupon_line; + } + + // Add refunds. + foreach ( $order->get_refunds() as $refund ) { + $data['refunds'][] = array( + 'id' => $refund->get_id(), + 'refund' => $refund->get_reason() ? $refund->get_reason() : '', + 'total' => '-' . wc_format_decimal( $refund->get_amount(), $dp ), + ); + } + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $order, $request ) ); + + /** + * Filter the data for a response. + * + * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being + * prepared for the response. + * + * @param WP_REST_Response $response The response object. + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $post, $request ); + } + + /** + * Prepare links for the request. + * + * @param WC_Order $order Order object. + * @param WP_REST_Request $request Request object. + * @return array Links for the given order. + */ + protected function prepare_links( $order, $request ) { + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $order->get_id() ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + if ( 0 !== (int) $order->get_user_id() ) { + $links['customer'] = array( + 'href' => rest_url( sprintf( '/%s/customers/%d', $this->namespace, $order->get_user_id() ) ), + ); + } + if ( 0 !== (int) $order->get_parent_id() ) { + $links['up'] = array( + 'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $order->get_parent_id() ) ), + ); + } + return $links; + } + + /** + * Query args. + * + * @param array $args + * @param WP_REST_Request $request + * @return array + */ + public function query_args( $args, $request ) { + global $wpdb; + + // Set post_status. + if ( 'any' !== $request['status'] ) { + $args['post_status'] = 'wc-' . $request['status']; + } else { + $args['post_status'] = 'any'; + } + + if ( isset( $request['customer'] ) ) { + if ( ! empty( $args['meta_query'] ) ) { + $args['meta_query'] = array(); + } + + $args['meta_query'][] = array( + 'key' => '_customer_user', + 'value' => $request['customer'], + 'type' => 'NUMERIC', + ); + } + + // Search by product. + if ( ! empty( $request['product'] ) ) { + $order_ids = $wpdb->get_col( $wpdb->prepare( " + SELECT order_id + FROM {$wpdb->prefix}woocommerce_order_items + WHERE order_item_id IN ( SELECT order_item_id FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE meta_key = '_product_id' AND meta_value = %d ) + AND order_item_type = 'line_item' + ", $request['product'] ) ); + + // Force WP_Query return empty if don't found any order. + $order_ids = ! empty( $order_ids ) ? $order_ids : array( 0 ); + + $args['post__in'] = $order_ids; + } + + // Search. + if ( ! empty( $args['s'] ) ) { + $order_ids = wc_order_search( $args['s'] ); + + if ( ! empty( $order_ids ) ) { + unset( $args['s'] ); + $args['post__in'] = array_merge( $order_ids, array( 0 ) ); + } + } + + return $args; + } + + /** + * Prepare a single order for create. + * + * @param WP_REST_Request $request Request object. + * @return WP_Error|WC_Order $data Object. + */ + protected function prepare_item_for_database( $request ) { + $id = isset( $request['id'] ) ? absint( $request['id'] ) : 0; + $order = new WC_Order( $id ); + $schema = $this->get_item_schema(); + $data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) ); + + // Handle all writable props + foreach ( $data_keys as $key ) { + $value = $request[ $key ]; + + if ( ! is_null( $value ) ) { + switch ( $key ) { + case 'billing' : + case 'shipping' : + $this->update_address( $order, $value, $key ); + break; + case 'line_items' : + case 'shipping_lines' : + case 'fee_lines' : + case 'coupon_lines' : + if ( is_array( $value ) ) { + foreach ( $value as $item ) { + if ( is_array( $item ) ) { + if ( $this->item_is_null( $item ) || ( isset( $item['quantity'] ) && 0 === $item['quantity'] ) ) { + $order->remove_item( $item['id'] ); + } else { + $this->set_item( $order, $key, $item ); + } + } + } + } + break; + default : + if ( is_callable( array( $order, "set_{$key}" ) ) ) { + $order->{"set_{$key}"}( $value ); + } + break; + } + } + } + + /** + * Filter the data for the insert. + * + * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being + * prepared for the response. + * + * @param WC_Order $order The order object. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}", $order, $request ); + } + + /** + * Create base WC Order object. + * @deprecated 3.0.0 + * @param array $data + * @return WC_Order + */ + protected function create_base_order( $data ) { + return wc_create_order( $data ); + } + + /** + * Only return writable props from schema. + * @param array $schema + * @return bool + */ + protected function filter_writable_props( $schema ) { + return empty( $schema['readonly'] ); + } + + /** + * Create order. + * + * @param WP_REST_Request $request Full details about the request. + * @return int|WP_Error + */ + protected function create_order( $request ) { + try { + // Make sure customer exists. + if ( ! is_null( $request['customer_id'] ) && 0 !== $request['customer_id'] && false === get_user_by( 'id', $request['customer_id'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_customer_id',__( 'Customer ID is invalid.', 'woocommerce' ), 400 ); + } + + // Make sure customer is part of blog. + if ( is_multisite() && ! is_user_member_of_blog( $request['customer_id'] ) ) { + add_user_to_blog( get_current_blog_id(), $request['customer_id'], 'customer' ); + } + + $order = $this->prepare_item_for_database( $request ); + $order->set_created_via( 'rest-api' ); + $order->set_prices_include_tax( 'yes' === get_option( 'woocommerce_prices_include_tax' ) ); + $order->calculate_totals(); + $order->save(); + + // Handle set paid. + if ( true === $request['set_paid'] ) { + $order->payment_complete( $request['transaction_id'] ); + } + + return $order->get_id(); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); + } catch ( WC_REST_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Update order. + * + * @param WP_REST_Request $request Full details about the request. + * @return int|WP_Error + */ + protected function update_order( $request ) { + try { + $order = $this->prepare_item_for_database( $request ); + $order->save(); + + // Handle set paid. + if ( $order->needs_payment() && true === $request['set_paid'] ) { + $order->payment_complete( $request['transaction_id'] ); + } + + // If items have changed, recalculate order totals. + if ( isset( $request['billing'] ) || isset( $request['shipping'] ) || isset( $request['line_items'] ) || isset( $request['shipping_lines'] ) || isset( $request['fee_lines'] ) || isset( $request['coupon_lines'] ) ) { + $order->calculate_totals( true ); + } + + return $order->get_id(); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); + } catch ( WC_REST_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Update address. + * + * @param WC_Order $order + * @param array $posted + * @param string $type + */ + protected function update_address( $order, $posted, $type = 'billing' ) { + foreach ( $posted as $key => $value ) { + if ( is_callable( array( $order, "set_{$type}_{$key}" ) ) ) { + $order->{"set_{$type}_{$key}"}( $value ); + } + } + } + + /** + * Gets the product ID from the SKU or posted ID. + * + * @throws WC_REST_Exception When SKU or ID is not valid. + * @param array $posted Request data. + * @param string $action 'create' to add line item or 'update' to update it. + * @return int + */ + protected function get_product_id( $posted, $action = 'create' ) { + if ( ! empty( $posted['sku'] ) ) { + $product_id = (int) wc_get_product_id_by_sku( $posted['sku'] ); + } elseif ( ! empty( $posted['product_id'] ) && empty( $posted['variation_id'] ) ) { + $product_id = (int) $posted['product_id']; + } elseif ( ! empty( $posted['variation_id'] ) ) { + $product_id = (int) $posted['variation_id']; + } elseif ( 'update' === $action ) { + $product_id = 0; + } else { + throw new WC_REST_Exception( 'woocommerce_rest_required_product_reference', __( 'Product ID or SKU is required.', 'woocommerce' ), 400 ); + } + return $product_id; + } + + /** + * Maybe set an item prop if the value was posted. + * @param WC_Order_Item $item + * @param string $prop + * @param array $posted Request data. + */ + protected function maybe_set_item_prop( $item, $prop, $posted ) { + if ( isset( $posted[ $prop ] ) ) { + $item->{"set_$prop"}( $posted[ $prop ] ); + } + } + + /** + * Maybe set item props if the values were posted. + * @param WC_Order_Item $item + * @param string[] $props + * @param array $posted Request data. + */ + protected function maybe_set_item_props( $item, $props, $posted ) { + foreach ( $props as $prop ) { + $this->maybe_set_item_prop( $item, $prop, $posted ); + } + } + + /** + * Create or update a line item. + * + * @param array $posted Line item data. + * @param string $action 'create' to add line item or 'update' to update it. + * + * @return WC_Order_Item_Product + * @throws WC_REST_Exception Invalid data, server error. + */ + protected function prepare_line_items( $posted, $action = 'create' ) { + $item = new WC_Order_Item_Product( ! empty( $posted['id'] ) ? $posted['id'] : '' ); + $product = wc_get_product( $this->get_product_id( $posted, $action ) ); + + if ( $product && $product !== $item->get_product() ) { + $item->set_product( $product ); + + if ( 'create' === $action ) { + $quantity = isset( $posted['quantity'] ) ? $posted['quantity'] : 1; + $total = wc_get_price_excluding_tax( $product, array( 'qty' => $quantity ) ); + $item->set_total( $total ); + $item->set_subtotal( $total ); + } + } + + $this->maybe_set_item_props( $item, array( 'name', 'quantity', 'total', 'subtotal', 'tax_class' ), $posted ); + + return $item; + } + + /** + * Create or update an order shipping method. + * + * @param $posted $shipping Item data. + * @param string $action 'create' to add shipping or 'update' to update it. + * + * @return WC_Order_Item_Shipping + * @throws WC_REST_Exception Invalid data, server error. + */ + protected function prepare_shipping_lines( $posted, $action ) { + $item = new WC_Order_Item_Shipping( ! empty( $posted['id'] ) ? $posted['id'] : '' ); + + if ( 'create' === $action ) { + if ( empty( $posted['method_id'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_shipping_item', __( 'Shipping method ID is required.', 'woocommerce' ), 400 ); + } + } + + $this->maybe_set_item_props( $item, array( 'method_id', 'method_title', 'total' ), $posted ); + + return $item; + } + + /** + * Create or update an order fee. + * + * @param array $posted Item data. + * @param string $action 'create' to add fee or 'update' to update it. + * + * @return WC_Order_Item_Fee + * @throws WC_REST_Exception Invalid data, server error. + */ + protected function prepare_fee_lines( $posted, $action ) { + $item = new WC_Order_Item_Fee( ! empty( $posted['id'] ) ? $posted['id'] : '' ); + + if ( 'create' === $action ) { + if ( empty( $posted['name'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_fee_item', __( 'Fee name is required.', 'woocommerce' ), 400 ); + } + } + + $this->maybe_set_item_props( $item, array( 'name', 'tax_class', 'tax_status', 'total' ), $posted ); + + return $item; + } + + /** + * Create or update an order coupon. + * + * @param array $posted Item data. + * @param string $action 'create' to add coupon or 'update' to update it. + * + * @return WC_Order_Item_Coupon + * @throws WC_REST_Exception Invalid data, server error. + */ + protected function prepare_coupon_lines( $posted, $action ) { + $item = new WC_Order_Item_Coupon( ! empty( $posted['id'] ) ? $posted['id'] : '' ); + + if ( 'create' === $action ) { + if ( empty( $posted['code'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_coupon_coupon', __( 'Coupon code is required.', 'woocommerce' ), 400 ); + } + } + + $this->maybe_set_item_props( $item, array( 'code', 'discount' ), $posted ); + + return $item; + } + + /** + * Wrapper method to create/update order items. + * When updating, the item ID provided is checked to ensure it is associated + * with the order. + * + * @param WC_Order $order order + * @param string $item_type + * @param array $posted item provided in the request body + * @throws WC_REST_Exception If item ID is not associated with order + */ + protected function set_item( $order, $item_type, $posted ) { + global $wpdb; + + if ( ! empty( $posted['id'] ) ) { + $action = 'update'; + } else { + $action = 'create'; + } + + $method = 'prepare_' . $item_type; + + // Verify provided line item ID is associated with order. + if ( 'update' === $action ) { + $result = $wpdb->get_row( + $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id = %d AND order_id = %d", + absint( $posted['id'] ), + absint( $order->get_id() ) + ) ); + if ( is_null( $result ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_item_id', __( 'Order item ID provided is not associated with order.', 'woocommerce' ), 400 ); + } + } + + // Prepare item data + $item = $this->$method( $posted, $action ); + + /** + * Action hook to adjust item before save. + * @since 3.0.0 + */ + do_action( 'woocommerce_rest_set_order_item', $item, $posted ); + + // Save or add to order + if ( 'create' === $action ) { + $order->add_item( $item ); + } else { + $item->save(); + } + } + + /** + * Helper method to check if the resource ID associated with the provided item is null. + * Items can be deleted by setting the resource ID to null. + * + * @param array $item Item provided in the request body. + * @return bool True if the item resource ID is null, false otherwise. + */ + protected function item_is_null( $item ) { + $keys = array( 'product_id', 'method_id', 'method_title', 'name', 'code' ); + + foreach ( $keys as $key ) { + if ( array_key_exists( $key, $item ) && is_null( $item[ $key ] ) ) { + return true; + } + } + + return false; + } + + /** + * Create a single item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + if ( ! empty( $request['id'] ) ) { + /* translators: %s: post type */ + return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) ); + } + + $order_id = $this->create_order( $request ); + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + $post = get_post( $order_id ); + $this->update_additional_fields_for_object( $post, $request ); + + /** + * Fires after a single item is created or updated via the REST API. + * + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating item, false when updating. + */ + do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, true ); + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $post, $request ); + $response = rest_ensure_response( $response ); + $response->set_status( 201 ); + $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $post->ID ) ) ); + + return $response; + } + + /** + * Update a single order. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function update_item( $request ) { + try { + $post_id = (int) $request['id']; + + if ( empty( $post_id ) || get_post_type( $post_id ) !== $this->post_type ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $order_id = $this->update_order( $request ); + if ( is_wp_error( $order_id ) ) { + return $order_id; + } + + $post = get_post( $order_id ); + $this->update_additional_fields_for_object( $post, $request ); + + /** + * Fires after a single item is created or updated via the REST API. + * + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating item, false when updating. + */ + do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, false ); + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $post, $request ); + return rest_ensure_response( $response ); + + } catch ( Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get order statuses without prefixes. + * @return array + */ + protected function get_order_statuses() { + $order_statuses = array(); + + foreach ( array_keys( wc_get_order_statuses() ) as $status ) { + $order_statuses[] = str_replace( 'wc-', '', $status ); + } + + return $order_statuses; + } + + /** + * Get the Order's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => $this->post_type, + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'parent_id' => array( + 'description' => __( 'Parent order ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'status' => array( + 'description' => __( 'Order status.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'pending', + 'enum' => $this->get_order_statuses(), + 'context' => array( 'view', 'edit' ), + ), + 'order_key' => array( + 'description' => __( 'Order key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'number' => array( + 'description' => __( 'Order number.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'currency' => array( + 'description' => __( 'Currency the order was created with, in ISO format.', 'woocommerce' ), + 'type' => 'string', + 'default' => get_woocommerce_currency(), + 'enum' => array_keys( get_woocommerce_currencies() ), + 'context' => array( 'view', 'edit' ), + ), + 'version' => array( + 'description' => __( 'Version of WooCommerce which last updated the order.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'prices_include_tax' => array( + 'description' => __( 'True the prices included tax during checkout.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( "The date the order was created, as GMT.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the order was last modified, as GMT.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'customer_id' => array( + 'description' => __( 'User ID who owns the order. 0 for guests.', 'woocommerce' ), + 'type' => 'integer', + 'default' => 0, + 'context' => array( 'view', 'edit' ), + ), + 'discount_total' => array( + 'description' => __( 'Total discount amount for the order.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'discount_tax' => array( + 'description' => __( 'Total discount tax amount for the order.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'shipping_total' => array( + 'description' => __( 'Total shipping amount for the order.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'shipping_tax' => array( + 'description' => __( 'Total shipping tax amount for the order.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'cart_tax' => array( + 'description' => __( 'Sum of line item taxes only.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Grand total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total_tax' => array( + 'description' => __( 'Sum of all taxes.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'billing' => array( + 'description' => __( 'Billing address.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'first_name' => array( + 'description' => __( 'First name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'last_name' => array( + 'description' => __( 'Last name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'company' => array( + 'description' => __( 'Company name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_1' => array( + 'description' => __( 'Address line 1.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_2' => array( + 'description' => __( 'Address line 2.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'city' => array( + 'description' => __( 'City name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'state' => array( + 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'postcode' => array( + 'description' => __( 'Postal code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'country' => array( + 'description' => __( 'Country code in ISO 3166-1 alpha-2 format.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'email' => array( + 'description' => __( 'Email address.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'email', + 'context' => array( 'view', 'edit' ), + ), + 'phone' => array( + 'description' => __( 'Phone number.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'shipping' => array( + 'description' => __( 'Shipping address.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'first_name' => array( + 'description' => __( 'First name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'last_name' => array( + 'description' => __( 'Last name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'company' => array( + 'description' => __( 'Company name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_1' => array( + 'description' => __( 'Address line 1.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_2' => array( + 'description' => __( 'Address line 2.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'city' => array( + 'description' => __( 'City name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'state' => array( + 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'postcode' => array( + 'description' => __( 'Postal code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'country' => array( + 'description' => __( 'Country code in ISO 3166-1 alpha-2 format.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'payment_method' => array( + 'description' => __( 'Payment method ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'payment_method_title' => array( + 'description' => __( 'Payment method title.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'set_paid' => array( + 'description' => __( 'Define if the order is paid. It will set the status to processing and reduce stock items.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'edit' ), + ), + 'transaction_id' => array( + 'description' => __( 'Unique transaction ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'customer_ip_address' => array( + 'description' => __( "Customer's IP address.", 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'customer_user_agent' => array( + 'description' => __( 'User agent of the customer.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'created_via' => array( + 'description' => __( 'Shows where the order was created.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'customer_note' => array( + 'description' => __( 'Note left by customer during checkout.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'date_completed' => array( + 'description' => __( "The date the order was completed, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_paid' => array( + 'description' => __( "The date the order was paid, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'cart_hash' => array( + 'description' => __( 'MD5 hash of cart items to ensure orders are not modified.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'line_items' => array( + 'description' => __( 'Line items data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Product name.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'sku' => array( + 'description' => __( 'Product SKU.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'product_id' => array( + 'description' => __( 'Product ID.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + ), + 'variation_id' => array( + 'description' => __( 'Variation ID, if applicable.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'quantity' => array( + 'description' => __( 'Quantity ordered.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'tax_class' => array( + 'description' => __( 'Tax class of product.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'price' => array( + 'description' => __( 'Product price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'subtotal' => array( + 'description' => __( 'Line subtotal (before discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'subtotal_tax' => array( + 'description' => __( 'Line subtotal tax (before discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'total' => array( + 'description' => __( 'Line total (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'total_tax' => array( + 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'taxes' => array( + 'description' => __( 'Line taxes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Tax rate ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Tax total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'subtotal' => array( + 'description' => __( 'Tax subtotal.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + 'meta' => array( + 'description' => __( 'Line item meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'label' => array( + 'description' => __( 'Meta label.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + ), + ), + ), + 'tax_lines' => array( + 'description' => __( 'Tax lines data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'rate_code' => array( + 'description' => __( 'Tax rate code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'rate_id' => array( + 'description' => __( 'Tax rate ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'label' => array( + 'description' => __( 'Tax rate label.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'compound' => array( + 'description' => __( 'Show if is a compound tax rate.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'tax_total' => array( + 'description' => __( 'Tax total (not including shipping taxes).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'shipping_tax_total' => array( + 'description' => __( 'Shipping tax total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + 'shipping_lines' => array( + 'description' => __( 'Shipping lines data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'method_title' => array( + 'description' => __( 'Shipping method name.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + ), + 'method_id' => array( + 'description' => __( 'Shipping method ID.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + ), + 'total' => array( + 'description' => __( 'Line total (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'total_tax' => array( + 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'taxes' => array( + 'description' => __( 'Line taxes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Tax rate ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Tax total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + ), + ), + ), + 'fee_lines' => array( + 'description' => __( 'Fee lines data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Fee name.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + ), + 'tax_class' => array( + 'description' => __( 'Tax class of fee.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'tax_status' => array( + 'description' => __( 'Tax status of fee.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'enum' => array( 'taxable', 'none' ), + ), + 'total' => array( + 'description' => __( 'Line total (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'total_tax' => array( + 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'taxes' => array( + 'description' => __( 'Line taxes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Tax rate ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Tax total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'subtotal' => array( + 'description' => __( 'Tax subtotal.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + ), + ), + ), + 'coupon_lines' => array( + 'description' => __( 'Coupons line data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'code' => array( + 'description' => __( 'Coupon code.', 'woocommerce' ), + 'type' => 'mixed', + 'context' => array( 'view', 'edit' ), + ), + 'discount' => array( + 'description' => __( 'Discount total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'discount_tax' => array( + 'description' => __( 'Discount total tax.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + 'refunds' => array( + 'description' => __( 'List of refunds.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Refund ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'reason' => array( + 'description' => __( 'Refund reason.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Refund total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + $params['status'] = array( + 'default' => 'any', + 'description' => __( 'Limit result set to orders assigned a specific status.', 'woocommerce' ), + 'type' => 'string', + 'enum' => array_merge( array( 'any' ), $this->get_order_statuses() ), + 'sanitize_callback' => 'sanitize_key', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['customer'] = array( + 'description' => __( 'Limit result set to orders assigned a specific customer.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['product'] = array( + 'description' => __( 'Limit result set to orders assigned a specific product.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['dp'] = array( + 'default' => wc_get_price_decimals(), + 'description' => __( 'Number of decimal points to use in each resource.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } +} diff --git a/includes/rest-api/Controllers/Version1/class-wc-rest-product-attribute-terms-v1-controller.php b/includes/rest-api/Controllers/Version1/class-wc-rest-product-attribute-terms-v1-controller.php new file mode 100644 index 00000000000..4d115f5ae2a --- /dev/null +++ b/includes/rest-api/Controllers/Version1/class-wc-rest-product-attribute-terms-v1-controller.php @@ -0,0 +1,241 @@ +/terms endpoint. + * + * @author WooThemes + * @category API + * @package Automattic/WooCommerce/RestApi + * @since 3.0.0 + */ + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +/** + * REST API Product Attribute Terms controller class. + * + * @package Automattic/WooCommerce/RestApi + * @extends WC_REST_Terms_Controller + */ +class WC_REST_Product_Attribute_Terms_V1_Controller extends WC_REST_Terms_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + protected $namespace = 'wc/v1'; + + /** + * Route base. + * + * @var string + */ + protected $rest_base = 'products/attributes/(?P[\d]+)/terms'; + + /** + * Register the routes for terms. + */ + public function register_routes() { + register_rest_route( $this->namespace, '/' . $this->rest_base, + array( + 'args' => array( + 'attribute_id' => array( + 'description' => __( 'Unique identifier for the attribute of the terms.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( + 'name' => array( + 'type' => 'string', + 'description' => __( 'Name for the resource.', 'woocommerce' ), + 'required' => true, + ), + ) ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + )); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + ), + 'attribute_id' => array( + 'description' => __( 'Unique identifier for the attribute of the terms.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'type' => 'boolean', + 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( + 'args' => array( + 'attribute_id' => array( + 'description' => __( 'Unique identifier for the attribute of the terms.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'batch_items' ), + 'permission_callback' => array( $this, 'batch_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_batch_schema' ), + ) ); + } + + /** + * Prepare a single product attribute term output for response. + * + * @param WP_Term $item Term object. + * @param WP_REST_Request $request + * @return WP_REST_Response $response + */ + public function prepare_item_for_response( $item, $request ) { + // Get term order. + $menu_order = get_term_meta( $item->term_id, 'order_' . $this->taxonomy, true ); + + $data = array( + 'id' => (int) $item->term_id, + 'name' => $item->name, + 'slug' => $item->slug, + 'description' => $item->description, + 'menu_order' => (int) $menu_order, + 'count' => (int) $item->count, + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $item, $request ) ); + + /** + * Filter a term item returned from the API. + * + * Allows modification of the term data right before it is returned. + * + * @param WP_REST_Response $response The response object. + * @param object $item The original term object. + * @param WP_REST_Request $request Request used to generate the response. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->taxonomy}", $response, $item, $request ); + } + + /** + * Update term meta fields. + * + * @param WP_Term $term + * @param WP_REST_Request $request + * @return bool|WP_Error + */ + protected function update_term_meta_fields( $term, $request ) { + $id = (int) $term->term_id; + + update_term_meta( $id, 'order_' . $this->taxonomy, $request['menu_order'] ); + + return true; + } + + /** + * Get the Attribute Term's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'product_attribute_term', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Term name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'slug' => array( + 'description' => __( 'An alphanumeric identifier for the resource unique to its type.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_title', + ), + ), + 'description' => array( + 'description' => __( 'HTML description of the resource.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'wp_filter_post_kses', + ), + ), + 'menu_order' => array( + 'description' => __( 'Menu order, used to custom sort the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'count' => array( + 'description' => __( 'Number of published products for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/rest-api/Controllers/Version1/class-wc-rest-product-attributes-v1-controller.php b/includes/rest-api/Controllers/Version1/class-wc-rest-product-attributes-v1-controller.php new file mode 100644 index 00000000000..d655200d033 --- /dev/null +++ b/includes/rest-api/Controllers/Version1/class-wc-rest-product-attributes-v1-controller.php @@ -0,0 +1,592 @@ +namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( + 'name' => array( + 'description' => __( 'Name for the resource.', 'woocommerce' ), + 'type' => 'string', + 'required' => true, + ), + ) ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + )); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => true, + 'type' => 'boolean', + 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'batch_items' ), + 'permission_callback' => array( $this, 'batch_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_batch_schema' ), + ) ); + } + + /** + * Check if a given request has access to read the attributes. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'attributes', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to create a attribute. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function create_item_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'attributes', 'create' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you cannot create new resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to read a attribute. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + if ( ! $this->get_taxonomy( $request ) ) { + return new WP_Error( 'woocommerce_rest_taxonomy_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + if ( ! wc_rest_check_manager_permissions( 'attributes', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to update a attribute. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function update_item_permissions_check( $request ) { + if ( ! $this->get_taxonomy( $request ) ) { + return new WP_Error( 'woocommerce_rest_taxonomy_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + if ( ! wc_rest_check_manager_permissions( 'attributes', 'edit' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_update', __( 'Sorry, you cannot update resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to delete a attribute. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function delete_item_permissions_check( $request ) { + if ( ! $this->get_taxonomy( $request ) ) { + return new WP_Error( 'woocommerce_rest_taxonomy_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + if ( ! wc_rest_check_manager_permissions( 'attributes', 'delete' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access batch create, update and delete items. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return bool|WP_Error + */ + public function batch_items_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'attributes', 'batch' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_batch', __( 'Sorry, you are not allowed to batch manipulate this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Get all attributes. + * + * @param WP_REST_Request $request + * @return array + */ + public function get_items( $request ) { + $attributes = wc_get_attribute_taxonomies(); + $data = array(); + foreach ( $attributes as $attribute_obj ) { + $attribute = $this->prepare_item_for_response( $attribute_obj, $request ); + $attribute = $this->prepare_response_for_collection( $attribute ); + $data[] = $attribute; + } + + $response = rest_ensure_response( $data ); + + // This API call always returns all product attributes due to retrieval from the object cache. + $response->header( 'X-WP-Total', count( $data ) ); + $response->header( 'X-WP-TotalPages', 1 ); + + return $response; + } + + /** + * Create a single attribute. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Request|WP_Error + */ + public function create_item( $request ) { + global $wpdb; + + $id = wc_create_attribute( array( + 'name' => $request['name'], + 'slug' => wc_sanitize_taxonomy_name( stripslashes( $request['slug'] ) ), + 'type' => ! empty( $request['type'] ) ? $request['type'] : 'select', + 'order_by' => ! empty( $request['order_by'] ) ? $request['order_by'] : 'menu_order', + 'has_archives' => true === $request['has_archives'], + ) ); + + // Checks for errors. + if ( is_wp_error( $id ) ) { + return new WP_Error( 'woocommerce_rest_cannot_create', $id->get_error_message(), array( 'status' => 400 ) ); + } + + $attribute = $this->get_attribute( $id ); + + if ( is_wp_error( $attribute ) ) { + return $attribute; + } + + $this->update_additional_fields_for_object( $attribute, $request ); + + /** + * Fires after a single product attribute is created or updated via the REST API. + * + * @param stdObject $attribute Inserted attribute object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating attribute, false when updating. + */ + do_action( 'woocommerce_rest_insert_product_attribute', $attribute, $request, true ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $attribute, $request ); + $response = rest_ensure_response( $response ); + $response->set_status( 201 ); + $response->header( 'Location', rest_url( '/' . $this->namespace . '/' . $this->rest_base . '/' . $attribute->attribute_id ) ); + + return $response; + } + + /** + * Get a single attribute. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Request|WP_Error + */ + public function get_item( $request ) { + $attribute = $this->get_attribute( (int) $request['id'] ); + + if ( is_wp_error( $attribute ) ) { + return $attribute; + } + + $response = $this->prepare_item_for_response( $attribute, $request ); + + return rest_ensure_response( $response ); + } + + /** + * Update a single term from a taxonomy. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Request|WP_Error + */ + public function update_item( $request ) { + global $wpdb; + + $id = (int) $request['id']; + $edited = wc_update_attribute( $id, array( + 'name' => $request['name'], + 'slug' => wc_sanitize_taxonomy_name( stripslashes( $request['slug'] ) ), + 'type' => $request['type'], + 'order_by' => $request['order_by'], + 'has_archives' => $request['has_archives'], + ) ); + + // Checks for errors. + if ( is_wp_error( $edited ) ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', $edited->get_error_message(), array( 'status' => 400 ) ); + } + + $attribute = $this->get_attribute( $id ); + + if ( is_wp_error( $attribute ) ) { + return $attribute; + } + + $this->update_additional_fields_for_object( $attribute, $request ); + + /** + * Fires after a single product attribute is created or updated via the REST API. + * + * @param stdObject $attribute Inserted attribute object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating attribute, false when updating. + */ + do_action( 'woocommerce_rest_insert_product_attribute', $attribute, $request, false ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $attribute, $request ); + + return rest_ensure_response( $response ); + } + + /** + * Delete a single attribute. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error + */ + public function delete_item( $request ) { + $force = isset( $request['force'] ) ? (bool) $request['force'] : false; + + // We don't support trashing for this type, error out. + if ( ! $force ) { + return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Resource does not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); + } + + $attribute = $this->get_attribute( (int) $request['id'] ); + + if ( is_wp_error( $attribute ) ) { + return $attribute; + } + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $attribute, $request ); + + $deleted = wc_delete_attribute( $attribute->attribute_id ); + + if ( false === $deleted ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'The resource cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) ); + } + + /** + * Fires after a single attribute is deleted via the REST API. + * + * @param stdObject $attribute The deleted attribute. + * @param WP_REST_Response $response The response data. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( 'woocommerce_rest_delete_product_attribute', $attribute, $response, $request ); + + return $response; + } + + /** + * Prepare a single product attribute output for response. + * + * @param obj $item Term object. + * @param WP_REST_Request $request + * @return WP_REST_Response $response + */ + public function prepare_item_for_response( $item, $request ) { + $data = array( + 'id' => (int) $item->attribute_id, + 'name' => $item->attribute_label, + 'slug' => wc_attribute_taxonomy_name( $item->attribute_name ), + 'type' => $item->attribute_type, + 'order_by' => $item->attribute_orderby, + 'has_archives' => (bool) $item->attribute_public, + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $item ) ); + + /** + * Filter a attribute item returned from the API. + * + * Allows modification of the product attribute data right before it is returned. + * + * @param WP_REST_Response $response The response object. + * @param object $item The original attribute object. + * @param WP_REST_Request $request Request used to generate the response. + */ + return apply_filters( 'woocommerce_rest_prepare_product_attribute', $response, $item, $request ); + } + + /** + * Prepare links for the request. + * + * @param object $attribute Attribute object. + * @return array Links for the given attribute. + */ + protected function prepare_links( $attribute ) { + $base = '/' . $this->namespace . '/' . $this->rest_base; + $links = array( + 'self' => array( + 'href' => rest_url( trailingslashit( $base ) . $attribute->attribute_id ), + ), + 'collection' => array( + 'href' => rest_url( $base ), + ), + ); + + return $links; + } + + /** + * Get the Attribute's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'product_attribute', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Attribute name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'slug' => array( + 'description' => __( 'An alphanumeric identifier for the resource unique to its type.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_title', + ), + ), + 'type' => array( + 'description' => __( 'Type of attribute.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'select', + 'enum' => array_keys( wc_get_attribute_types() ), + 'context' => array( 'view', 'edit' ), + ), + 'order_by' => array( + 'description' => __( 'Default sort order.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'menu_order', + 'enum' => array( 'menu_order', 'name', 'name_num', 'id' ), + 'context' => array( 'view', 'edit' ), + ), + 'has_archives' => array( + 'description' => __( 'Enable/Disable attribute archives.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections + * + * @return array + */ + public function get_collection_params() { + $params = array(); + $params['context'] = $this->get_context_param( array( 'default' => 'view' ) ); + + return $params; + } + + /** + * Get attribute name. + * + * @param WP_REST_Request $request Full details about the request. + * @return string + */ + protected function get_taxonomy( $request ) { + if ( '' !== $this->attribute ) { + return $this->attribute; + } + + if ( $request['id'] ) { + $name = wc_attribute_taxonomy_name_by_id( (int) $request['id'] ); + + $this->attribute = $name; + } + + return $this->attribute; + } + + /** + * Get attribute data. + * + * @param int $id Attribute ID. + * @return stdClass|WP_Error + */ + protected function get_attribute( $id ) { + global $wpdb; + + $attribute = $wpdb->get_row( $wpdb->prepare( " + SELECT * + FROM {$wpdb->prefix}woocommerce_attribute_taxonomies + WHERE attribute_id = %d + ", $id ) ); + + if ( is_wp_error( $attribute ) || is_null( $attribute ) ) { + return new WP_Error( 'woocommerce_rest_attribute_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + return $attribute; + } + + /** + * Validate attribute slug. + * + * @deprecated 3.2.0 + * @param string $slug + * @param bool $new_data + * @return bool|WP_Error + */ + protected function validate_attribute_slug( $slug, $new_data = true ) { + if ( strlen( $slug ) >= 28 ) { + return new WP_Error( 'woocommerce_rest_invalid_product_attribute_slug_too_long', sprintf( __( 'Slug "%s" is too long (28 characters max). Shorten it, please.', 'woocommerce' ), $slug ), array( 'status' => 400 ) ); + } elseif ( wc_check_if_attribute_name_is_reserved( $slug ) ) { + return new WP_Error( 'woocommerce_rest_invalid_product_attribute_slug_reserved_name', sprintf( __( 'Slug "%s" is not allowed because it is a reserved term. Change it, please.', 'woocommerce' ), $slug ), array( 'status' => 400 ) ); + } elseif ( $new_data && taxonomy_exists( wc_attribute_taxonomy_name( $slug ) ) ) { + return new WP_Error( 'woocommerce_rest_invalid_product_attribute_slug_already_exists', sprintf( __( 'Slug "%s" is already in use. Change it, please.', 'woocommerce' ), $slug ), array( 'status' => 400 ) ); + } + + return true; + } + + /** + * Schedule to flush rewrite rules. + * + * @deprecated 3.2.0 + * @since 3.0.0 + */ + protected function flush_rewrite_rules() { + wp_schedule_single_event( time(), 'woocommerce_flush_rewrite_rules' ); + } +} diff --git a/includes/rest-api/Controllers/Version1/class-wc-rest-product-categories-v1-controller.php b/includes/rest-api/Controllers/Version1/class-wc-rest-product-categories-v1-controller.php new file mode 100644 index 00000000000..22d16a0d0bc --- /dev/null +++ b/includes/rest-api/Controllers/Version1/class-wc-rest-product-categories-v1-controller.php @@ -0,0 +1,271 @@ +term_id, 'display_type', true ); + + // Get category order. + $menu_order = get_term_meta( $item->term_id, 'order', true ); + + $data = array( + 'id' => (int) $item->term_id, + 'name' => $item->name, + 'slug' => $item->slug, + 'parent' => (int) $item->parent, + 'description' => $item->description, + 'display' => $display_type ? $display_type : 'default', + 'image' => null, + 'menu_order' => (int) $menu_order, + 'count' => (int) $item->count, + ); + + // Get category image. + $image_id = get_term_meta( $item->term_id, 'thumbnail_id', true ); + if ( $image_id ) { + $attachment = get_post( $image_id ); + + $data['image'] = array( + 'id' => (int) $image_id, + 'date_created' => wc_rest_prepare_date_response( $attachment->post_date_gmt ), + 'date_modified' => wc_rest_prepare_date_response( $attachment->post_modified_gmt ), + 'src' => wp_get_attachment_url( $image_id ), + 'title' => get_the_title( $attachment ), + 'alt' => get_post_meta( $image_id, '_wp_attachment_image_alt', true ), + ); + } + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $item, $request ) ); + + /** + * Filter a term item returned from the API. + * + * Allows modification of the term data right before it is returned. + * + * @param WP_REST_Response $response The response object. + * @param object $item The original term object. + * @param WP_REST_Request $request Request used to generate the response. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->taxonomy}", $response, $item, $request ); + } + + /** + * Update term meta fields. + * + * @param WP_Term $term Term object. + * @param WP_REST_Request $request Request instance. + * @return bool|WP_Error + */ + protected function update_term_meta_fields( $term, $request ) { + $id = (int) $term->term_id; + + if ( isset( $request['display'] ) ) { + update_term_meta( $id, 'display_type', 'default' === $request['display'] ? '' : $request['display'] ); + } + + if ( isset( $request['menu_order'] ) ) { + update_term_meta( $id, 'order', $request['menu_order'] ); + } + + if ( isset( $request['image'] ) ) { + if ( empty( $request['image']['id'] ) && ! empty( $request['image']['src'] ) ) { + $upload = wc_rest_upload_image_from_url( esc_url_raw( $request['image']['src'] ) ); + + if ( is_wp_error( $upload ) ) { + return $upload; + } + + $image_id = wc_rest_set_uploaded_image_as_attachment( $upload ); + } else { + $image_id = isset( $request['image']['id'] ) ? absint( $request['image']['id'] ) : 0; + } + + // Check if image_id is a valid image attachment before updating the term meta. + if ( $image_id && wp_attachment_is_image( $image_id ) ) { + update_term_meta( $id, 'thumbnail_id', $image_id ); + + // Set the image alt. + if ( ! empty( $request['image']['alt'] ) ) { + update_post_meta( $image_id, '_wp_attachment_image_alt', wc_clean( $request['image']['alt'] ) ); + } + + // Set the image title. + if ( ! empty( $request['image']['title'] ) ) { + wp_update_post( array( + 'ID' => $image_id, + 'post_title' => wc_clean( $request['image']['title'] ), + ) ); + } + } else { + delete_term_meta( $id, 'thumbnail_id' ); + } + } + + return true; + } + + /** + * Get the Category schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => $this->taxonomy, + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Category name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'slug' => array( + 'description' => __( 'An alphanumeric identifier for the resource unique to its type.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_title', + ), + ), + 'parent' => array( + 'description' => __( 'The ID for the parent of the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'description' => array( + 'description' => __( 'HTML description of the resource.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'wp_filter_post_kses', + ), + ), + 'display' => array( + 'description' => __( 'Category archive display type.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'default', + 'enum' => array( 'default', 'products', 'subcategories', 'both' ), + 'context' => array( 'view', 'edit' ), + ), + 'image' => array( + 'description' => __( 'Image data.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'id' => array( + 'description' => __( 'Image ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'date_created' => array( + 'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'src' => array( + 'description' => __( 'Image URL.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + ), + 'title' => array( + 'description' => __( 'Image name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'alt' => array( + 'description' => __( 'Image alternative text.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'menu_order' => array( + 'description' => __( 'Menu order, used to custom sort the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'count' => array( + 'description' => __( 'Number of published products for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/rest-api/Controllers/Version1/class-wc-rest-product-reviews-v1-controller.php b/includes/rest-api/Controllers/Version1/class-wc-rest-product-reviews-v1-controller.php new file mode 100644 index 00000000000..f52b7e098ab --- /dev/null +++ b/includes/rest-api/Controllers/Version1/class-wc-rest-product-reviews-v1-controller.php @@ -0,0 +1,578 @@ +/reviews. + * + * @author WooThemes + * @category API + * @package Automattic/WooCommerce/RestApi + * @since 3.0.0 + */ + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +/** + * REST API Product Reviews Controller Class. + * + * @package Automattic/WooCommerce/RestApi + * @extends WC_REST_Controller + */ +class WC_REST_Product_Reviews_V1_Controller extends WC_REST_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + protected $namespace = 'wc/v1'; + + /** + * Route base. + * + * @var string + */ + protected $rest_base = 'products/(?P[\d]+)/reviews'; + + /** + * Register the routes for product reviews. + */ + public function register_routes() { + register_rest_route( $this->namespace, '/' . $this->rest_base, array( + 'args' => array( + 'product_id' => array( + 'description' => __( 'Unique identifier for the variable product.', 'woocommerce' ), + 'type' => 'integer', + ), + 'id' => array( + 'description' => __( 'Unique identifier for the variation.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( + 'review' => array( + 'required' => true, + 'type' => 'string', + 'description' => __( 'Review content.', 'woocommerce' ), + ), + 'name' => array( + 'required' => true, + 'type' => 'string', + 'description' => __( 'Name of the reviewer.', 'woocommerce' ), + ), + 'email' => array( + 'required' => true, + 'type' => 'string', + 'description' => __( 'Email of the reviewer.', 'woocommerce' ), + ), + ) ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + 'args' => array( + 'product_id' => array( + 'description' => __( 'Unique identifier for the variable product.', 'woocommerce' ), + 'type' => 'integer', + ), + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'type' => 'boolean', + 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + } + + /** + * Check whether a given request has permission to read webhook deliveries. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_post_permissions( 'product', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to read a product review. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + $post = get_post( (int) $request['product_id'] ); + + if ( $post && ! wc_rest_check_post_permissions( 'product', 'read', $post->ID ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to create a new product review. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function create_item_permissions_check( $request ) { + $post = get_post( (int) $request['product_id'] ); + if ( $post && ! wc_rest_check_post_permissions( 'product', 'create', $post->ID ) ) { + return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + return true; + } + + /** + * Check if a given request has access to update a product review. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function update_item_permissions_check( $request ) { + $post = get_post( (int) $request['product_id'] ); + if ( $post && ! wc_rest_check_post_permissions( 'product', 'edit', $post->ID ) ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you cannot edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + return true; + } + + /** + * Check if a given request has access to delete a product review. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function delete_item_permissions_check( $request ) { + $post = get_post( (int) $request['product_id'] ); + if ( $post && ! wc_rest_check_post_permissions( 'product', 'delete', $post->ID ) ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you cannot delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + return true; + } + + /** + * Get all reviews from a product. + * + * @param WP_REST_Request $request + * + * @return array|WP_Error + */ + public function get_items( $request ) { + $product_id = (int) $request['product_id']; + + if ( 'product' !== get_post_type( $product_id ) ) { + return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $reviews = get_approved_comments( $product_id ); + $data = array(); + foreach ( $reviews as $review_data ) { + $review = $this->prepare_item_for_response( $review_data, $request ); + $review = $this->prepare_response_for_collection( $review ); + $data[] = $review; + } + + return rest_ensure_response( $data ); + } + + /** + * Get a single product review. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + $id = (int) $request['id']; + $product_id = (int) $request['product_id']; + + if ( 'product' !== get_post_type( $product_id ) ) { + return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $review = get_comment( $id ); + + if ( empty( $id ) || empty( $review ) || intval( $review->comment_post_ID ) !== $product_id ) { + return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $delivery = $this->prepare_item_for_response( $review, $request ); + $response = rest_ensure_response( $delivery ); + + return $response; + } + + + /** + * Create a product review. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + $product_id = (int) $request['product_id']; + + if ( 'product' !== get_post_type( $product_id ) ) { + return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $prepared_review = $this->prepare_item_for_database( $request ); + + /** + * Filter a product review (comment) before it is inserted via the REST API. + * + * Allows modification of the comment right before it is inserted via `wp_insert_comment`. + * + * @param array $prepared_review The prepared comment data for `wp_insert_comment`. + * @param WP_REST_Request $request Request used to insert the comment. + */ + $prepared_review = apply_filters( 'rest_pre_insert_product_review', $prepared_review, $request ); + + $product_review_id = wp_insert_comment( $prepared_review ); + if ( ! $product_review_id ) { + return new WP_Error( 'rest_product_review_failed_create', __( 'Creating product review failed.', 'woocommerce' ), array( 'status' => 500 ) ); + } + + update_comment_meta( $product_review_id, 'rating', ( ! empty( $request['rating'] ) ? $request['rating'] : '0' ) ); + + $product_review = get_comment( $product_review_id ); + $this->update_additional_fields_for_object( $product_review, $request ); + + /** + * Fires after a single item is created or updated via the REST API. + * + * @param WP_Comment $product_review Inserted object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating item, false when updating. + */ + do_action( "woocommerce_rest_insert_product_review", $product_review, $request, true ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $product_review, $request ); + $response = rest_ensure_response( $response ); + $response->set_status( 201 ); + $base = str_replace( '(?P[\d]+)', $product_id, $this->rest_base ); + $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $product_review_id ) ) ); + + return $response; + } + + /** + * Update a single product review. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function update_item( $request ) { + $product_review_id = (int) $request['id']; + $product_id = (int) $request['product_id']; + + if ( 'product' !== get_post_type( $product_id ) ) { + return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $review = get_comment( $product_review_id ); + + if ( empty( $product_review_id ) || empty( $review ) || intval( $review->comment_post_ID ) !== $product_id ) { + return new WP_Error( 'woocommerce_rest_product_review_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $prepared_review = $this->prepare_item_for_database( $request ); + + $updated = wp_update_comment( $prepared_review ); + if ( 0 === $updated ) { + return new WP_Error( 'rest_product_review_failed_edit', __( 'Updating product review failed.', 'woocommerce' ), array( 'status' => 500 ) ); + } + + if ( ! empty( $request['rating'] ) ) { + update_comment_meta( $product_review_id, 'rating', $request['rating'] ); + } + + $product_review = get_comment( $product_review_id ); + $this->update_additional_fields_for_object( $product_review, $request ); + + /** + * Fires after a single item is created or updated via the REST API. + * + * @param WP_Comment $comment Inserted object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating item, false when updating. + */ + do_action( "woocommerce_rest_insert_product_review", $product_review, $request, true ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $product_review, $request ); + + return rest_ensure_response( $response ); + } + + /** + * Delete a product review. + * + * @param WP_REST_Request $request Full details about the request + * + * @return bool|WP_Error|WP_REST_Response + */ + public function delete_item( $request ) { + $product_review_id = absint( is_array( $request['id'] ) ? $request['id']['id'] : $request['id'] ); + $force = isset( $request['force'] ) ? (bool) $request['force'] : false; + + $product_review = get_comment( $product_review_id ); + if ( empty( $product_review_id ) || empty( $product_review->comment_ID ) || empty( $product_review->comment_post_ID ) ) { + return new WP_Error( 'woocommerce_rest_product_review_invalid_id', __( 'Invalid product review ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + /** + * Filter whether a product review is trashable. + * + * Return false to disable trash support for the product review. + * + * @param boolean $supports_trash Whether the object supports trashing. + * @param WP_Post $product_review The object being considered for trashing support. + */ + $supports_trash = apply_filters( 'rest_product_review_trashable', ( EMPTY_TRASH_DAYS > 0 ), $product_review ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $product_review, $request ); + + if ( $force ) { + $result = wp_delete_comment( $product_review_id, true ); + } else { + if ( ! $supports_trash ) { + return new WP_Error( 'rest_trash_not_supported', __( 'The product review does not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); + } + + if ( 'trash' === $product_review->comment_approved ) { + return new WP_Error( 'rest_already_trashed', __( 'The comment has already been trashed.', 'woocommerce' ), array( 'status' => 410 ) ); + } + + $result = wp_trash_comment( $product_review->comment_ID ); + } + + if ( ! $result ) { + return new WP_Error( 'rest_cannot_delete', __( 'The product review cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) ); + } + + /** + * Fires after a product review is deleted via the REST API. + * + * @param object $product_review The deleted item. + * @param WP_REST_Response $response The response data. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( 'rest_delete_product_review', $product_review, $response, $request ); + + return $response; + } + + /** + * Prepare a single product review output for response. + * + * @param WP_Comment $review Product review object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $review, $request ) { + $data = array( + 'id' => (int) $review->comment_ID, + 'date_created' => wc_rest_prepare_date_response( $review->comment_date_gmt ), + 'review' => $review->comment_content, + 'rating' => (int) get_comment_meta( $review->comment_ID, 'rating', true ), + 'name' => $review->comment_author, + 'email' => $review->comment_author_email, + 'verified' => wc_review_is_from_verified_owner( $review->comment_ID ), + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $review, $request ) ); + + /** + * Filter product reviews object returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param WP_Comment $review Product review object used to create response. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'woocommerce_rest_prepare_product_review', $response, $review, $request ); + } + + /** + * Prepare a single product review to be inserted into the database. + * + * @param WP_REST_Request $request Request object. + * @return array|WP_Error $prepared_review + */ + protected function prepare_item_for_database( $request ) { + $prepared_review = array( 'comment_approved' => 1, 'comment_type' => 'review' ); + + if ( isset( $request['id'] ) ) { + $prepared_review['comment_ID'] = (int) $request['id']; + } + + if ( isset( $request['review'] ) ) { + $prepared_review['comment_content'] = $request['review']; + } + + if ( isset( $request['product_id'] ) ) { + $prepared_review['comment_post_ID'] = (int) $request['product_id']; + } + + if ( isset( $request['name'] ) ) { + $prepared_review['comment_author'] = $request['name']; + } + + if ( isset( $request['email'] ) ) { + $prepared_review['comment_author_email'] = $request['email']; + } + + if ( isset( $request['date_created'] ) ) { + $prepared_review['comment_date'] = $request['date_created']; + } + + if ( isset( $request['date_created_gmt'] ) ) { + $prepared_review['comment_date_gmt'] = $request['date_created_gmt']; + } + + return apply_filters( 'rest_preprocess_product_review', $prepared_review, $request ); + } + + /** + * Prepare links for the request. + * + * @param WP_Comment $review Product review object. + * @param WP_REST_Request $request Request object. + * @return array Links for the given product review. + */ + protected function prepare_links( $review, $request ) { + $product_id = (int) $request['product_id']; + $base = str_replace( '(?P[\d]+)', $product_id, $this->rest_base ); + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $review->comment_ID ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ), + ), + 'up' => array( + 'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $product_id ) ), + ), + ); + + return $links; + } + + /** + * Get the Product Review's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'product_review', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'review' => array( + 'description' => __( 'The content of the review.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'date_created' => array( + 'description' => __( "The date the review was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'rating' => array( + 'description' => __( 'Review rating (0 to 5).', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Reviewer name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'email' => array( + 'description' => __( 'Reviewer email.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'verified' => array( + 'description' => __( 'Shows if the reviewer bought the product or not.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + return array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ); + } +} diff --git a/includes/rest-api/Controllers/Version1/class-wc-rest-product-shipping-classes-v1-controller.php b/includes/rest-api/Controllers/Version1/class-wc-rest-product-shipping-classes-v1-controller.php new file mode 100644 index 00000000000..fb2326df2dd --- /dev/null +++ b/includes/rest-api/Controllers/Version1/class-wc-rest-product-shipping-classes-v1-controller.php @@ -0,0 +1,134 @@ + (int) $item->term_id, + 'name' => $item->name, + 'slug' => $item->slug, + 'description' => $item->description, + 'count' => (int) $item->count, + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $item, $request ) ); + + /** + * Filter a term item returned from the API. + * + * Allows modification of the term data right before it is returned. + * + * @param WP_REST_Response $response The response object. + * @param object $item The original term object. + * @param WP_REST_Request $request Request used to generate the response. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->taxonomy}", $response, $item, $request ); + } + + /** + * Get the Shipping Class schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => $this->taxonomy, + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Shipping class name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'slug' => array( + 'description' => __( 'An alphanumeric identifier for the resource unique to its type.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_title', + ), + ), + 'description' => array( + 'description' => __( 'HTML description of the resource.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'wp_filter_post_kses', + ), + ), + 'count' => array( + 'description' => __( 'Number of published products for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/rest-api/Controllers/Version1/class-wc-rest-product-tags-v1-controller.php b/includes/rest-api/Controllers/Version1/class-wc-rest-product-tags-v1-controller.php new file mode 100644 index 00000000000..a43ef8b43f6 --- /dev/null +++ b/includes/rest-api/Controllers/Version1/class-wc-rest-product-tags-v1-controller.php @@ -0,0 +1,134 @@ + (int) $item->term_id, + 'name' => $item->name, + 'slug' => $item->slug, + 'description' => $item->description, + 'count' => (int) $item->count, + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $item, $request ) ); + + /** + * Filter a term item returned from the API. + * + * Allows modification of the term data right before it is returned. + * + * @param WP_REST_Response $response The response object. + * @param object $item The original term object. + * @param WP_REST_Request $request Request used to generate the response. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->taxonomy}", $response, $item, $request ); + } + + /** + * Get the Tag's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => $this->taxonomy, + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Tag name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'slug' => array( + 'description' => __( 'An alphanumeric identifier for the resource unique to its type.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_title', + ), + ), + 'description' => array( + 'description' => __( 'HTML description of the resource.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'wp_filter_post_kses', + ), + ), + 'count' => array( + 'description' => __( 'Number of published products for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/rest-api/Controllers/Version1/class-wc-rest-products-v1-controller.php b/includes/rest-api/Controllers/Version1/class-wc-rest-products-v1-controller.php new file mode 100644 index 00000000000..f0b6c7869c4 --- /dev/null +++ b/includes/rest-api/Controllers/Version1/class-wc-rest-products-v1-controller.php @@ -0,0 +1,2641 @@ +post_type}_query", array( $this, 'query_args' ), 10, 2 ); + add_action( "woocommerce_rest_insert_{$this->post_type}", array( $this, 'clear_transients' ) ); + } + + /** + * Register the routes for products. + */ + public function register_routes() { + register_rest_route( $this->namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), + 'type' => 'boolean', + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'batch_items' ), + 'permission_callback' => array( $this, 'batch_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_batch_schema' ), + ) ); + } + + /** + * Get post types. + * + * @return array + */ + protected function get_post_types() { + return array( 'product', 'product_variation' ); + } + + /** + * Query args. + * + * @param array $args Request args. + * @param WP_REST_Request $request Request data. + * @return array + */ + public function query_args( $args, $request ) { + // Set post_status. + $args['post_status'] = $request['status']; + + // Taxonomy query to filter products by type, category, + // tag, shipping class, and attribute. + $tax_query = array(); + + // Map between taxonomy name and arg's key. + $taxonomies = array( + 'product_cat' => 'category', + 'product_tag' => 'tag', + 'product_shipping_class' => 'shipping_class', + ); + + // Set tax_query for each passed arg. + foreach ( $taxonomies as $taxonomy => $key ) { + if ( ! empty( $request[ $key ] ) && is_array( $request[ $key ] ) ) { + $request[ $key ] = array_filter( $request[ $key ] ); + } + + if ( ! empty( $request[ $key ] ) ) { + $tax_query[] = array( + 'taxonomy' => $taxonomy, + 'field' => 'term_id', + 'terms' => $request[ $key ], + ); + } + } + + // Filter product type by slug. + if ( ! empty( $request['type'] ) ) { + $tax_query[] = array( + 'taxonomy' => 'product_type', + 'field' => 'slug', + 'terms' => $request['type'], + ); + } + + // Filter by attribute and term. + if ( ! empty( $request['attribute'] ) && ! empty( $request['attribute_term'] ) ) { + if ( in_array( $request['attribute'], wc_get_attribute_taxonomy_names(), true ) ) { + $tax_query[] = array( + 'taxonomy' => $request['attribute'], + 'field' => 'term_id', + 'terms' => $request['attribute_term'], + ); + } + } + + if ( ! empty( $tax_query ) ) { + $args['tax_query'] = $tax_query; + } + + // Filter by sku. + if ( ! empty( $request['sku'] ) ) { + $skus = explode( ',', $request['sku'] ); + // Include the current string as a SKU too. + if ( 1 < count( $skus ) ) { + $skus[] = $request['sku']; + } + + $args['meta_query'] = $this->add_meta_query( $args, array( + 'key' => '_sku', + 'value' => $skus, + 'compare' => 'IN', + ) ); + } + + // Apply all WP_Query filters again. + if ( is_array( $request['filter'] ) ) { + $args = array_merge( $args, $request['filter'] ); + unset( $args['filter'] ); + } + + // Force the post_type argument, since it's not a user input variable. + if ( ! empty( $request['sku'] ) ) { + $args['post_type'] = array( 'product', 'product_variation' ); + } else { + $args['post_type'] = $this->post_type; + } + + return $args; + } + + /** + * Get the downloads for a product or product variation. + * + * @param WC_Product|WC_Product_Variation $product Product instance. + * @return array + */ + protected function get_downloads( $product ) { + $downloads = array(); + + if ( $product->is_downloadable() ) { + foreach ( $product->get_downloads() as $file_id => $file ) { + $downloads[] = array( + 'id' => $file_id, // MD5 hash. + 'name' => $file['name'], + 'file' => $file['file'], + ); + } + } + + return $downloads; + } + + /** + * Get taxonomy terms. + * + * @param WC_Product $product Product instance. + * @param string $taxonomy Taxonomy slug. + * @return array + */ + protected function get_taxonomy_terms( $product, $taxonomy = 'cat' ) { + $terms = array(); + + foreach ( wc_get_object_terms( $product->get_id(), 'product_' . $taxonomy ) as $term ) { + $terms[] = array( + 'id' => $term->term_id, + 'name' => $term->name, + 'slug' => $term->slug, + ); + } + + return $terms; + } + + /** + * Get the images for a product or product variation. + * + * @param WC_Product|WC_Product_Variation $product Product instance. + * @return array + */ + protected function get_images( $product ) { + $images = array(); + $attachment_ids = array(); + + // Add featured image. + if ( $product->get_image_id() ) { + $attachment_ids[] = $product->get_image_id(); + } + + // Add gallery images. + $attachment_ids = array_merge( $attachment_ids, $product->get_gallery_image_ids() ); + + // Build image data. + foreach ( $attachment_ids as $position => $attachment_id ) { + $attachment_post = get_post( $attachment_id ); + if ( is_null( $attachment_post ) ) { + continue; + } + + $attachment = wp_get_attachment_image_src( $attachment_id, 'full' ); + if ( ! is_array( $attachment ) ) { + continue; + } + + $images[] = array( + 'id' => (int) $attachment_id, + 'date_created' => wc_rest_prepare_date_response( $attachment_post->post_date_gmt ), + 'date_modified' => wc_rest_prepare_date_response( $attachment_post->post_modified_gmt ), + 'src' => current( $attachment ), + 'name' => get_the_title( $attachment_id ), + 'alt' => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ), + 'position' => (int) $position, + ); + } + + // Set a placeholder image if the product has no images set. + if ( empty( $images ) ) { + $images[] = array( + 'id' => 0, + 'date_created' => wc_rest_prepare_date_response( current_time( 'mysql' ) ), // Default to now. + 'date_modified' => wc_rest_prepare_date_response( current_time( 'mysql' ) ), + 'src' => wc_placeholder_img_src(), + 'name' => __( 'Placeholder', 'woocommerce' ), + 'alt' => __( 'Placeholder', 'woocommerce' ), + 'position' => 0, + ); + } + + return $images; + } + + /** + * Get attribute taxonomy label. + * + * @param string $name Taxonomy name. + * @return string + */ + protected function get_attribute_taxonomy_label( $name ) { + $tax = get_taxonomy( $name ); + $labels = get_taxonomy_labels( $tax ); + + return $labels->singular_name; + } + + /** + * Get default attributes. + * + * @param WC_Product $product Product instance. + * @return array + */ + protected function get_default_attributes( $product ) { + $default = array(); + + if ( $product->is_type( 'variable' ) ) { + foreach ( array_filter( (array) $product->get_default_attributes(), 'strlen' ) as $key => $value ) { + if ( 0 === strpos( $key, 'pa_' ) ) { + $default[] = array( + 'id' => wc_attribute_taxonomy_id_by_name( $key ), + 'name' => $this->get_attribute_taxonomy_label( $key ), + 'option' => $value, + ); + } else { + $default[] = array( + 'id' => 0, + 'name' => wc_attribute_taxonomy_slug( $key ), + 'option' => $value, + ); + } + } + } + + return $default; + } + + /** + * Get attribute options. + * + * @param int $product_id Product ID. + * @param array $attribute Attribute data. + * @return array + */ + protected function get_attribute_options( $product_id, $attribute ) { + if ( isset( $attribute['is_taxonomy'] ) && $attribute['is_taxonomy'] ) { + return wc_get_product_terms( $product_id, $attribute['name'], array( 'fields' => 'names' ) ); + } elseif ( isset( $attribute['value'] ) ) { + return array_map( 'trim', explode( '|', $attribute['value'] ) ); + } + + return array(); + } + + /** + * Get the attributes for a product or product variation. + * + * @param WC_Product|WC_Product_Variation $product Product instance. + * @return array + */ + protected function get_attributes( $product ) { + $attributes = array(); + + if ( $product->is_type( 'variation' ) ) { + // Variation attributes. + foreach ( $product->get_variation_attributes() as $attribute_name => $attribute ) { + $name = str_replace( 'attribute_', '', $attribute_name ); + + if ( ! $attribute ) { + continue; + } + + // Taxonomy-based attributes are prefixed with `pa_`, otherwise simply `attribute_`. + if ( 0 === strpos( $attribute_name, 'attribute_pa_' ) ) { + $option_term = get_term_by( 'slug', $attribute, $name ); + $attributes[] = array( + 'id' => wc_attribute_taxonomy_id_by_name( $name ), + 'name' => $this->get_attribute_taxonomy_label( $name ), + 'option' => $option_term && ! is_wp_error( $option_term ) ? $option_term->name : $attribute, + ); + } else { + $attributes[] = array( + 'id' => 0, + 'name' => $name, + 'option' => $attribute, + ); + } + } + } else { + foreach ( $product->get_attributes() as $attribute ) { + if ( $attribute['is_taxonomy'] ) { + $attributes[] = array( + 'id' => wc_attribute_taxonomy_id_by_name( $attribute['name'] ), + 'name' => $this->get_attribute_taxonomy_label( $attribute['name'] ), + 'position' => (int) $attribute['position'], + 'visible' => (bool) $attribute['is_visible'], + 'variation' => (bool) $attribute['is_variation'], + 'options' => $this->get_attribute_options( $product->get_id(), $attribute ), + ); + } else { + $attributes[] = array( + 'id' => 0, + 'name' => $attribute['name'], + 'position' => (int) $attribute['position'], + 'visible' => (bool) $attribute['is_visible'], + 'variation' => (bool) $attribute['is_variation'], + 'options' => $this->get_attribute_options( $product->get_id(), $attribute ), + ); + } + } + } + + return $attributes; + } + + /** + * Get product menu order. + * + * @deprecated 3.0.0 + * @param WC_Product $product Product instance. + * @return int + */ + protected function get_product_menu_order( $product ) { + return $product->get_menu_order(); + } + + /** + * Get product data. + * + * @param WC_Product $product Product instance. + * @return array + */ + protected function get_product_data( $product ) { + $data = array( + 'id' => $product->get_id(), + 'name' => $product->get_name(), + 'slug' => $product->get_slug(), + 'permalink' => $product->get_permalink(), + 'date_created' => wc_rest_prepare_date_response( $product->get_date_created() ), + 'date_modified' => wc_rest_prepare_date_response( $product->get_date_modified() ), + 'type' => $product->get_type(), + 'status' => $product->get_status(), + 'featured' => $product->is_featured(), + 'catalog_visibility' => $product->get_catalog_visibility(), + 'description' => wpautop( do_shortcode( $product->get_description() ) ), + 'short_description' => apply_filters( 'woocommerce_short_description', $product->get_short_description() ), + 'sku' => $product->get_sku(), + 'price' => $product->get_price(), + 'regular_price' => $product->get_regular_price(), + 'sale_price' => $product->get_sale_price() ? $product->get_sale_price() : '', + 'date_on_sale_from' => $product->get_date_on_sale_from() ? date( 'Y-m-d', $product->get_date_on_sale_from()->getTimestamp() ) : '', + 'date_on_sale_to' => $product->get_date_on_sale_to() ? date( 'Y-m-d', $product->get_date_on_sale_to()->getTimestamp() ) : '', + 'price_html' => $product->get_price_html(), + 'on_sale' => $product->is_on_sale(), + 'purchasable' => $product->is_purchasable(), + 'total_sales' => $product->get_total_sales(), + 'virtual' => $product->is_virtual(), + 'downloadable' => $product->is_downloadable(), + 'downloads' => $this->get_downloads( $product ), + 'download_limit' => $product->get_download_limit(), + 'download_expiry' => $product->get_download_expiry(), + 'download_type' => 'standard', + 'external_url' => $product->is_type( 'external' ) ? $product->get_product_url() : '', + 'button_text' => $product->is_type( 'external' ) ? $product->get_button_text() : '', + 'tax_status' => $product->get_tax_status(), + 'tax_class' => $product->get_tax_class(), + 'manage_stock' => $product->managing_stock(), + 'stock_quantity' => $product->get_stock_quantity(), + 'in_stock' => $product->is_in_stock(), + 'backorders' => $product->get_backorders(), + 'backorders_allowed' => $product->backorders_allowed(), + 'backordered' => $product->is_on_backorder(), + 'sold_individually' => $product->is_sold_individually(), + 'weight' => $product->get_weight(), + 'dimensions' => array( + 'length' => $product->get_length(), + 'width' => $product->get_width(), + 'height' => $product->get_height(), + ), + 'shipping_required' => $product->needs_shipping(), + 'shipping_taxable' => $product->is_shipping_taxable(), + 'shipping_class' => $product->get_shipping_class(), + 'shipping_class_id' => $product->get_shipping_class_id(), + 'reviews_allowed' => $product->get_reviews_allowed(), + 'average_rating' => wc_format_decimal( $product->get_average_rating(), 2 ), + 'rating_count' => $product->get_rating_count(), + 'related_ids' => array_map( 'absint', array_values( wc_get_related_products( $product->get_id() ) ) ), + 'upsell_ids' => array_map( 'absint', $product->get_upsell_ids() ), + 'cross_sell_ids' => array_map( 'absint', $product->get_cross_sell_ids() ), + 'parent_id' => $product->get_parent_id(), + 'purchase_note' => wpautop( do_shortcode( wp_kses_post( $product->get_purchase_note() ) ) ), + 'categories' => $this->get_taxonomy_terms( $product ), + 'tags' => $this->get_taxonomy_terms( $product, 'tag' ), + 'images' => $this->get_images( $product ), + 'attributes' => $this->get_attributes( $product ), + 'default_attributes' => $this->get_default_attributes( $product ), + 'variations' => array(), + 'grouped_products' => array(), + 'menu_order' => $product->get_menu_order(), + ); + + return $data; + } + + /** + * Get an individual variation's data. + * + * @param WC_Product $product Product instance. + * @return array + */ + protected function get_variation_data( $product ) { + $variations = array(); + + foreach ( $product->get_children() as $child_id ) { + $variation = wc_get_product( $child_id ); + if ( ! $variation || ! $variation->exists() ) { + continue; + } + + $variations[] = array( + 'id' => $variation->get_id(), + 'date_created' => wc_rest_prepare_date_response( $variation->get_date_created() ), + 'date_modified' => wc_rest_prepare_date_response( $variation->get_date_modified() ), + 'permalink' => $variation->get_permalink(), + 'sku' => $variation->get_sku(), + 'price' => $variation->get_price(), + 'regular_price' => $variation->get_regular_price(), + 'sale_price' => $variation->get_sale_price(), + 'date_on_sale_from' => $variation->get_date_on_sale_from() ? date( 'Y-m-d', $variation->get_date_on_sale_from()->getTimestamp() ) : '', + 'date_on_sale_to' => $variation->get_date_on_sale_to() ? date( 'Y-m-d', $variation->get_date_on_sale_to()->getTimestamp() ) : '', + 'on_sale' => $variation->is_on_sale(), + 'purchasable' => $variation->is_purchasable(), + 'visible' => $variation->is_visible(), + 'virtual' => $variation->is_virtual(), + 'downloadable' => $variation->is_downloadable(), + 'downloads' => $this->get_downloads( $variation ), + 'download_limit' => '' !== $variation->get_download_limit() ? (int) $variation->get_download_limit() : -1, + 'download_expiry' => '' !== $variation->get_download_expiry() ? (int) $variation->get_download_expiry() : -1, + 'tax_status' => $variation->get_tax_status(), + 'tax_class' => $variation->get_tax_class(), + 'manage_stock' => $variation->managing_stock(), + 'stock_quantity' => $variation->get_stock_quantity(), + 'in_stock' => $variation->is_in_stock(), + 'backorders' => $variation->get_backorders(), + 'backorders_allowed' => $variation->backorders_allowed(), + 'backordered' => $variation->is_on_backorder(), + 'weight' => $variation->get_weight(), + 'dimensions' => array( + 'length' => $variation->get_length(), + 'width' => $variation->get_width(), + 'height' => $variation->get_height(), + ), + 'shipping_class' => $variation->get_shipping_class(), + 'shipping_class_id' => $variation->get_shipping_class_id(), + 'image' => $this->get_images( $variation ), + 'attributes' => $this->get_attributes( $variation ), + ); + } + + return $variations; + } + + /** + * Prepare a single product output for response. + * + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response + */ + public function prepare_item_for_response( $post, $request ) { + $product = wc_get_product( $post ); + $data = $this->get_product_data( $product ); + + // Add variations to variable products. + if ( $product->is_type( 'variable' ) && $product->has_child() ) { + $data['variations'] = $this->get_variation_data( $product ); + } + + // Add grouped products data. + if ( $product->is_type( 'grouped' ) && $product->has_child() ) { + $data['grouped_products'] = $product->get_children(); + } + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $product, $request ) ); + + /** + * Filter the data for a response. + * + * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being + * prepared for the response. + * + * @param WP_REST_Response $response The response object. + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $post, $request ); + } + + /** + * Prepare links for the request. + * + * @param WC_Product $product Product object. + * @param WP_REST_Request $request Request object. + * @return array Links for the given product. + */ + protected function prepare_links( $product, $request ) { + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $product->get_id() ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + + if ( $product->get_parent_id() ) { + $links['up'] = array( + 'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $product->get_parent_id() ) ), + ); + } + + return $links; + } + + /** + * Prepare a single product for create or update. + * + * @param WP_REST_Request $request Request object. + * @return WP_Error|stdClass $data Post object. + */ + protected function prepare_item_for_database( $request ) { + $id = isset( $request['id'] ) ? absint( $request['id'] ) : 0; + + // Type is the most important part here because we need to be using the correct class and methods. + if ( isset( $request['type'] ) ) { + $classname = WC_Product_Factory::get_classname_from_product_type( $request['type'] ); + + if ( ! class_exists( $classname ) ) { + $classname = 'WC_Product_Simple'; + } + + $product = new $classname( $id ); + } elseif ( isset( $request['id'] ) ) { + $product = wc_get_product( $id ); + } else { + $product = new WC_Product_Simple(); + } + + // Post title. + if ( isset( $request['name'] ) ) { + $product->set_name( wp_filter_post_kses( $request['name'] ) ); + } + + // Post content. + if ( isset( $request['description'] ) ) { + $product->set_description( wp_filter_post_kses( $request['description'] ) ); + } + + // Post excerpt. + if ( isset( $request['short_description'] ) ) { + $product->set_short_description( wp_filter_post_kses( $request['short_description'] ) ); + } + + // Post status. + if ( isset( $request['status'] ) ) { + $product->set_status( get_post_status_object( $request['status'] ) ? $request['status'] : 'draft' ); + } + + // Post slug. + if ( isset( $request['slug'] ) ) { + $product->set_slug( $request['slug'] ); + } + + // Menu order. + if ( isset( $request['menu_order'] ) ) { + $product->set_menu_order( $request['menu_order'] ); + } + + // Comment status. + if ( isset( $request['reviews_allowed'] ) ) { + $product->set_reviews_allowed( $request['reviews_allowed'] ); + } + + /** + * Filter the query_vars used in `get_items` for the constructed query. + * + * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being + * prepared for insertion. + * + * @param WC_Product $product An object representing a single item prepared + * for inserting or updating the database. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}", $product, $request ); + } + + /** + * Create a single product. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + if ( ! empty( $request['id'] ) ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) ); + } + + $product_id = 0; + + try { + $product_id = $this->save_product( $request ); + $post = get_post( $product_id ); + $this->update_additional_fields_for_object( $post, $request ); + $this->update_post_meta_fields( $post, $request ); + + /** + * Fires after a single item is created or updated via the REST API. + * + * @param WP_Post $post Post data. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating item, false when updating. + */ + do_action( 'woocommerce_rest_insert_product', $post, $request, true ); + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $post, $request ); + $response = rest_ensure_response( $response ); + $response->set_status( 201 ); + $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $post->ID ) ) ); + + return $response; + } catch ( WC_Data_Exception $e ) { + $this->delete_post( $product_id ); + return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); + } catch ( WC_REST_Exception $e ) { + $this->delete_post( $product_id ); + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Update a single product. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function update_item( $request ) { + $post_id = (int) $request['id']; + + if ( empty( $post_id ) || get_post_type( $post_id ) !== $this->post_type ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + try { + $product_id = $this->save_product( $request ); + $post = get_post( $product_id ); + $this->update_additional_fields_for_object( $post, $request ); + $this->update_post_meta_fields( $post, $request ); + + /** + * Fires after a single item is created or updated via the REST API. + * + * @param WP_Post $post Post data. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating item, false when updating. + */ + do_action( 'woocommerce_rest_insert_product', $post, $request, false ); + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $post, $request ); + + return rest_ensure_response( $response ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); + } catch ( WC_REST_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Saves a product to the database. + * + * @param WP_REST_Request $request Full details about the request. + * @return int + */ + public function save_product( $request ) { + $product = $this->prepare_item_for_database( $request ); + return $product->save(); + } + + /** + * Save product images. + * + * @deprecated 3.0.0 + * @param int $product_id + * @param array $images + * @throws WC_REST_Exception + */ + protected function save_product_images( $product_id, $images ) { + $product = wc_get_product( $product_id ); + + return set_product_images( $product, $images ); + } + + /** + * Set product images. + * + * @throws WC_REST_Exception REST API exceptions. + * @param WC_Product $product Product instance. + * @param array $images Images data. + * @return WC_Product + */ + protected function set_product_images( $product, $images ) { + if ( is_array( $images ) ) { + $gallery = array(); + + foreach ( $images as $image ) { + $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; + + if ( 0 === $attachment_id && isset( $image['src'] ) ) { + $upload = wc_rest_upload_image_from_url( esc_url_raw( $image['src'] ) ); + + if ( is_wp_error( $upload ) ) { + if ( ! apply_filters( 'woocommerce_rest_suppress_image_upload_error', false, $upload, $product->get_id(), $images ) ) { + throw new WC_REST_Exception( 'woocommerce_product_image_upload_error', $upload->get_error_message(), 400 ); + } else { + continue; + } + } + + $attachment_id = wc_rest_set_uploaded_image_as_attachment( $upload, $product->get_id() ); + } + + if ( ! wp_attachment_is_image( $attachment_id ) ) { + throw new WC_REST_Exception( 'woocommerce_product_invalid_image_id', sprintf( __( '#%s is an invalid image ID.', 'woocommerce' ), $attachment_id ), 400 ); + } + + if ( isset( $image['position'] ) && 0 === absint( $image['position'] ) ) { + $product->set_image_id( $attachment_id ); + } else { + $gallery[] = $attachment_id; + } + + // Set the image alt if present. + if ( ! empty( $image['alt'] ) ) { + update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) ); + } + + // Set the image name if present. + if ( ! empty( $image['name'] ) ) { + wp_update_post( array( 'ID' => $attachment_id, 'post_title' => $image['name'] ) ); + } + } + + if ( ! empty( $gallery ) ) { + $product->set_gallery_image_ids( $gallery ); + } + } else { + $product->set_image_id( '' ); + $product->set_gallery_image_ids( array() ); + } + + return $product; + } + + /** + * Save product shipping data. + * + * @param WC_Product $product Product instance. + * @param array $data Shipping data. + * @return WC_Product + */ + protected function save_product_shipping_data( $product, $data ) { + // Virtual. + if ( isset( $data['virtual'] ) && true === $data['virtual'] ) { + $product->set_weight( '' ); + $product->set_height( '' ); + $product->set_length( '' ); + $product->set_width( '' ); + } else { + if ( isset( $data['weight'] ) ) { + $product->set_weight( $data['weight'] ); + } + + // Height. + if ( isset( $data['dimensions']['height'] ) ) { + $product->set_height( $data['dimensions']['height'] ); + } + + // Width. + if ( isset( $data['dimensions']['width'] ) ) { + $product->set_width( $data['dimensions']['width'] ); + } + + // Length. + if ( isset( $data['dimensions']['length'] ) ) { + $product->set_length( $data['dimensions']['length'] ); + } + } + + // Shipping class. + if ( isset( $data['shipping_class'] ) ) { + $data_store = $product->get_data_store(); + $shipping_class_id = $data_store->get_shipping_class_id_by_slug( wc_clean( $data['shipping_class'] ) ); + $product->set_shipping_class_id( $shipping_class_id ); + } + + return $product; + } + + /** + * Save downloadable files. + * + * @param WC_Product $product Product instance. + * @param array $downloads Downloads data. + * @param int $deprecated Deprecated since 3.0. + * @return WC_Product + */ + protected function save_downloadable_files( $product, $downloads, $deprecated = 0 ) { + if ( $deprecated ) { + wc_deprecated_argument( 'variation_id', '3.0', 'save_downloadable_files() not requires a variation_id anymore.' ); + } + + $files = array(); + foreach ( $downloads as $key => $file ) { + if ( empty( $file['file'] ) ) { + continue; + } + + $download = new WC_Product_Download(); + $download->set_id( ! empty( $file['id'] ) ? $file['id'] : wp_generate_uuid4() ); + $download->set_name( $file['name'] ? $file['name'] : wc_get_filename_from_url( $file['file'] ) ); + $download->set_file( apply_filters( 'woocommerce_file_download_path', $file['file'], $product, $key ) ); + $files[] = $download; + } + $product->set_downloads( $files ); + + return $product; + } + + /** + * Save taxonomy terms. + * + * @param WC_Product $product Product instance. + * @param array $terms Terms data. + * @param string $taxonomy Taxonomy name. + * @return WC_Product + */ + protected function save_taxonomy_terms( $product, $terms, $taxonomy = 'cat' ) { + $term_ids = wp_list_pluck( $terms, 'id' ); + + if ( 'cat' === $taxonomy ) { + $product->set_category_ids( $term_ids ); + } elseif ( 'tag' === $taxonomy ) { + $product->set_tag_ids( $term_ids ); + } + + return $product; + } + + /** + * Save default attributes. + * + * @since 3.0.0 + * + * @param WC_Product $product Product instance. + * @param WP_REST_Request $request Request data. + * @return WC_Product + */ + protected function save_default_attributes( $product, $request ) { + if ( isset( $request['default_attributes'] ) && is_array( $request['default_attributes'] ) ) { + $attributes = $product->get_attributes(); + $default_attributes = array(); + + foreach ( $request['default_attributes'] as $attribute ) { + $attribute_id = 0; + $attribute_name = ''; + + // Check ID for global attributes or name for product attributes. + if ( ! empty( $attribute['id'] ) ) { + $attribute_id = absint( $attribute['id'] ); + $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); + } elseif ( ! empty( $attribute['name'] ) ) { + $attribute_name = sanitize_title( $attribute['name'] ); + } + + if ( ! $attribute_id && ! $attribute_name ) { + continue; + } + + if ( isset( $attributes[ $attribute_name ] ) ) { + $_attribute = $attributes[ $attribute_name ]; + + if ( $_attribute['is_variation'] ) { + $value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : ''; + + if ( ! empty( $_attribute['is_taxonomy'] ) ) { + // If dealing with a taxonomy, we need to get the slug from the name posted to the API. + $term = get_term_by( 'name', $value, $attribute_name ); + + if ( $term && ! is_wp_error( $term ) ) { + $value = $term->slug; + } else { + $value = sanitize_title( $value ); + } + } + + if ( $value ) { + $default_attributes[ $attribute_name ] = $value; + } + } + } + } + + $product->set_default_attributes( $default_attributes ); + } + + return $product; + } + + /** + * Save product meta. + * + * @deprecated 3.0.0 + * @param WC_Product $product + * @param WP_REST_Request $request + * @return bool + * @throws WC_REST_Exception + */ + protected function save_product_meta( $product, $request ) { + $product = $this->set_product_meta( $product, $request ); + $product->save(); + + return true; + } + + /** + * Set product meta. + * + * @throws WC_REST_Exception REST API exceptions. + * @param WC_Product $product Product instance. + * @param WP_REST_Request $request Request data. + * @return WC_Product + */ + protected function set_product_meta( $product, $request ) { + // Virtual. + if ( isset( $request['virtual'] ) ) { + $product->set_virtual( $request['virtual'] ); + } + + // Tax status. + if ( isset( $request['tax_status'] ) ) { + $product->set_tax_status( $request['tax_status'] ); + } + + // Tax Class. + if ( isset( $request['tax_class'] ) ) { + $product->set_tax_class( $request['tax_class'] ); + } + + // Catalog Visibility. + if ( isset( $request['catalog_visibility'] ) ) { + $product->set_catalog_visibility( $request['catalog_visibility'] ); + } + + // Purchase Note. + if ( isset( $request['purchase_note'] ) ) { + $product->set_purchase_note( wp_kses_post( wp_unslash( $request['purchase_note'] ) ) ); + } + + // Featured Product. + if ( isset( $request['featured'] ) ) { + $product->set_featured( $request['featured'] ); + } + + // Shipping data. + $product = $this->save_product_shipping_data( $product, $request ); + + // SKU. + if ( isset( $request['sku'] ) ) { + $product->set_sku( wc_clean( $request['sku'] ) ); + } + + // Attributes. + if ( isset( $request['attributes'] ) ) { + $attributes = array(); + + foreach ( $request['attributes'] as $attribute ) { + $attribute_id = 0; + $attribute_name = ''; + + // Check ID for global attributes or name for product attributes. + if ( ! empty( $attribute['id'] ) ) { + $attribute_id = absint( $attribute['id'] ); + $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); + } elseif ( ! empty( $attribute['name'] ) ) { + $attribute_name = wc_clean( $attribute['name'] ); + } + + if ( ! $attribute_id && ! $attribute_name ) { + continue; + } + + if ( $attribute_id ) { + + if ( isset( $attribute['options'] ) ) { + $options = $attribute['options']; + + if ( ! is_array( $attribute['options'] ) ) { + // Text based attributes - Posted values are term names. + $options = explode( WC_DELIMITER, $options ); + } + + $values = array_map( 'wc_sanitize_term_text_based', $options ); + $values = array_filter( $values, 'strlen' ); + } else { + $values = array(); + } + + if ( ! empty( $values ) ) { + // Add attribute to array, but don't set values. + $attribute_object = new WC_Product_Attribute(); + $attribute_object->set_id( $attribute_id ); + $attribute_object->set_name( $attribute_name ); + $attribute_object->set_options( $values ); + $attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' ); + $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); + $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); + $attributes[] = $attribute_object; + } + } elseif ( isset( $attribute['options'] ) ) { + // Custom attribute - Add attribute to array and set the values. + if ( is_array( $attribute['options'] ) ) { + $values = $attribute['options']; + } else { + $values = explode( WC_DELIMITER, $attribute['options'] ); + } + $attribute_object = new WC_Product_Attribute(); + $attribute_object->set_name( $attribute_name ); + $attribute_object->set_options( $values ); + $attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' ); + $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); + $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); + $attributes[] = $attribute_object; + } + } + $product->set_attributes( $attributes ); + } + + // Sales and prices. + if ( in_array( $product->get_type(), array( 'variable', 'grouped' ), true ) ) { + $product->set_regular_price( '' ); + $product->set_sale_price( '' ); + $product->set_date_on_sale_to( '' ); + $product->set_date_on_sale_from( '' ); + $product->set_price( '' ); + } else { + // Regular Price. + if ( isset( $request['regular_price'] ) ) { + $product->set_regular_price( $request['regular_price'] ); + } + + // Sale Price. + if ( isset( $request['sale_price'] ) ) { + $product->set_sale_price( $request['sale_price'] ); + } + + if ( isset( $request['date_on_sale_from'] ) ) { + $product->set_date_on_sale_from( $request['date_on_sale_from'] ); + } + + if ( isset( $request['date_on_sale_to'] ) ) { + $product->set_date_on_sale_to( $request['date_on_sale_to'] ); + } + } + + // Product parent ID for groups. + if ( isset( $request['parent_id'] ) ) { + $product->set_parent_id( $request['parent_id'] ); + } + + // Sold individually. + if ( isset( $request['sold_individually'] ) ) { + $product->set_sold_individually( $request['sold_individually'] ); + } + + // Stock status. + if ( isset( $request['in_stock'] ) ) { + $stock_status = true === $request['in_stock'] ? 'instock' : 'outofstock'; + } else { + $stock_status = $product->get_stock_status(); + } + + // Stock data. + if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) { + // Manage stock. + if ( isset( $request['manage_stock'] ) ) { + $product->set_manage_stock( $request['manage_stock'] ); + } + + // Backorders. + if ( isset( $request['backorders'] ) ) { + $product->set_backorders( $request['backorders'] ); + } + + if ( $product->is_type( 'grouped' ) ) { + $product->set_manage_stock( 'no' ); + $product->set_backorders( 'no' ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( $stock_status ); + } elseif ( $product->is_type( 'external' ) ) { + $product->set_manage_stock( 'no' ); + $product->set_backorders( 'no' ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( 'instock' ); + } elseif ( $product->get_manage_stock() ) { + // Stock status is always determined by children so sync later. + if ( ! $product->is_type( 'variable' ) ) { + $product->set_stock_status( $stock_status ); + } + + // Stock quantity. + if ( isset( $request['stock_quantity'] ) ) { + $product->set_stock_quantity( wc_stock_amount( $request['stock_quantity'] ) ); + } elseif ( isset( $request['inventory_delta'] ) ) { + $stock_quantity = wc_stock_amount( $product->get_stock_quantity() ); + $stock_quantity += wc_stock_amount( $request['inventory_delta'] ); + $product->set_stock_quantity( wc_stock_amount( $stock_quantity ) ); + } + } else { + // Don't manage stock. + $product->set_manage_stock( 'no' ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( $stock_status ); + } + } elseif ( ! $product->is_type( 'variable' ) ) { + $product->set_stock_status( $stock_status ); + } + + // Upsells. + if ( isset( $request['upsell_ids'] ) ) { + $upsells = array(); + $ids = $request['upsell_ids']; + + if ( ! empty( $ids ) ) { + foreach ( $ids as $id ) { + if ( $id && $id > 0 ) { + $upsells[] = $id; + } + } + } + + $product->set_upsell_ids( $upsells ); + } + + // Cross sells. + if ( isset( $request['cross_sell_ids'] ) ) { + $crosssells = array(); + $ids = $request['cross_sell_ids']; + + if ( ! empty( $ids ) ) { + foreach ( $ids as $id ) { + if ( $id && $id > 0 ) { + $crosssells[] = $id; + } + } + } + + $product->set_cross_sell_ids( $crosssells ); + } + + // Product categories. + if ( isset( $request['categories'] ) && is_array( $request['categories'] ) ) { + $product = $this->save_taxonomy_terms( $product, $request['categories'] ); + } + + // Product tags. + if ( isset( $request['tags'] ) && is_array( $request['tags'] ) ) { + $product = $this->save_taxonomy_terms( $product, $request['tags'], 'tag' ); + } + + // Downloadable. + if ( isset( $request['downloadable'] ) ) { + $product->set_downloadable( $request['downloadable'] ); + } + + // Downloadable options. + if ( $product->get_downloadable() ) { + + // Downloadable files. + if ( isset( $request['downloads'] ) && is_array( $request['downloads'] ) ) { + $product = $this->save_downloadable_files( $product, $request['downloads'] ); + } + + // Download limit. + if ( isset( $request['download_limit'] ) ) { + $product->set_download_limit( $request['download_limit'] ); + } + + // Download expiry. + if ( isset( $request['download_expiry'] ) ) { + $product->set_download_expiry( $request['download_expiry'] ); + } + } + + // Product url and button text for external products. + if ( $product->is_type( 'external' ) ) { + if ( isset( $request['external_url'] ) ) { + $product->set_product_url( $request['external_url'] ); + } + + if ( isset( $request['button_text'] ) ) { + $product->set_button_text( $request['button_text'] ); + } + } + + // Save default attributes for variable products. + if ( $product->is_type( 'variable' ) ) { + $product = $this->save_default_attributes( $product, $request ); + } + + return $product; + } + + /** + * Save variations. + * + * @throws WC_REST_Exception REST API exceptions. + * @param WC_Product $product Product instance. + * @param WP_REST_Request $request Request data. + * @return bool + */ + protected function save_variations_data( $product, $request ) { + foreach ( $request['variations'] as $menu_order => $data ) { + $variation = new WC_Product_Variation( isset( $data['id'] ) ? absint( $data['id'] ) : 0 ); + + // Create initial name and status. + if ( ! $variation->get_slug() ) { + /* translators: 1: variation id 2: product name */ + $variation->set_name( sprintf( __( 'Variation #%1$s of %2$s', 'woocommerce' ), $variation->get_id(), $product->get_name() ) ); + $variation->set_status( isset( $data['visible'] ) && false === $data['visible'] ? 'private' : 'publish' ); + } + + // Parent ID. + $variation->set_parent_id( $product->get_id() ); + + // Menu order. + $variation->set_menu_order( $menu_order ); + + // Status. + if ( isset( $data['visible'] ) ) { + $variation->set_status( false === $data['visible'] ? 'private' : 'publish' ); + } + + // SKU. + if ( isset( $data['sku'] ) ) { + $variation->set_sku( wc_clean( $data['sku'] ) ); + } + + // Thumbnail. + if ( isset( $data['image'] ) && is_array( $data['image'] ) ) { + $image = $data['image']; + $image = current( $image ); + if ( is_array( $image ) ) { + $image['position'] = 0; + } + + $variation = $this->set_product_images( $variation, array( $image ) ); + } + + // Virtual variation. + if ( isset( $data['virtual'] ) ) { + $variation->set_virtual( $data['virtual'] ); + } + + // Downloadable variation. + if ( isset( $data['downloadable'] ) ) { + $variation->set_downloadable( $data['downloadable'] ); + } + + // Downloads. + if ( $variation->get_downloadable() ) { + // Downloadable files. + if ( isset( $data['downloads'] ) && is_array( $data['downloads'] ) ) { + $variation = $this->save_downloadable_files( $variation, $data['downloads'] ); + } + + // Download limit. + if ( isset( $data['download_limit'] ) ) { + $variation->set_download_limit( $data['download_limit'] ); + } + + // Download expiry. + if ( isset( $data['download_expiry'] ) ) { + $variation->set_download_expiry( $data['download_expiry'] ); + } + } + + // Shipping data. + $variation = $this->save_product_shipping_data( $variation, $data ); + + // Stock handling. + if ( isset( $data['manage_stock'] ) ) { + $variation->set_manage_stock( $data['manage_stock'] ); + } + + if ( isset( $data['in_stock'] ) ) { + $variation->set_stock_status( true === $data['in_stock'] ? 'instock' : 'outofstock' ); + } + + if ( isset( $data['backorders'] ) ) { + $variation->set_backorders( $data['backorders'] ); + } + + if ( $variation->get_manage_stock() ) { + if ( isset( $data['stock_quantity'] ) ) { + $variation->set_stock_quantity( $data['stock_quantity'] ); + } elseif ( isset( $data['inventory_delta'] ) ) { + $stock_quantity = wc_stock_amount( $variation->get_stock_quantity() ); + $stock_quantity += wc_stock_amount( $data['inventory_delta'] ); + $variation->set_stock_quantity( $stock_quantity ); + } + } else { + $variation->set_backorders( 'no' ); + $variation->set_stock_quantity( '' ); + } + + // Regular Price. + if ( isset( $data['regular_price'] ) ) { + $variation->set_regular_price( $data['regular_price'] ); + } + + // Sale Price. + if ( isset( $data['sale_price'] ) ) { + $variation->set_sale_price( $data['sale_price'] ); + } + + if ( isset( $data['date_on_sale_from'] ) ) { + $variation->set_date_on_sale_from( $data['date_on_sale_from'] ); + } + + if ( isset( $data['date_on_sale_to'] ) ) { + $variation->set_date_on_sale_to( $data['date_on_sale_to'] ); + } + + // Tax class. + if ( isset( $data['tax_class'] ) ) { + $variation->set_tax_class( $data['tax_class'] ); + } + + // Description. + if ( isset( $data['description'] ) ) { + $variation->set_description( wp_kses_post( $data['description'] ) ); + } + + // Update taxonomies. + if ( isset( $data['attributes'] ) ) { + $attributes = array(); + $parent_attributes = $product->get_attributes(); + + foreach ( $data['attributes'] as $attribute ) { + $attribute_id = 0; + $attribute_name = ''; + + // Check ID for global attributes or name for product attributes. + if ( ! empty( $attribute['id'] ) ) { + $attribute_id = absint( $attribute['id'] ); + $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); + } elseif ( ! empty( $attribute['name'] ) ) { + $attribute_name = sanitize_title( $attribute['name'] ); + } + + if ( ! $attribute_id && ! $attribute_name ) { + continue; + } + + if ( ! isset( $parent_attributes[ $attribute_name ] ) || ! $parent_attributes[ $attribute_name ]->get_variation() ) { + continue; + } + + $attribute_key = sanitize_title( $parent_attributes[ $attribute_name ]->get_name() ); + $attribute_value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : ''; + + if ( $parent_attributes[ $attribute_name ]->is_taxonomy() ) { + // If dealing with a taxonomy, we need to get the slug from the name posted to the API. + $term = get_term_by( 'name', $attribute_value, $attribute_name ); + + if ( $term && ! is_wp_error( $term ) ) { + $attribute_value = $term->slug; + } else { + $attribute_value = sanitize_title( $attribute_value ); + } + } + + $attributes[ $attribute_key ] = $attribute_value; + } + + $variation->set_attributes( $attributes ); + } + + $variation->save(); + + do_action( 'woocommerce_rest_save_product_variation', $variation->get_id(), $menu_order, $data ); + } + + return true; + } + + /** + * Add post meta fields. + * + * @param WP_Post $post Post data. + * @param WP_REST_Request $request Request data. + * @return bool|WP_Error + */ + protected function add_post_meta_fields( $post, $request ) { + return $this->update_post_meta_fields( $post, $request ); + } + + /** + * Update post meta fields. + * + * @param WP_Post $post Post data. + * @param WP_REST_Request $request Request data. + * @return bool|WP_Error + */ + protected function update_post_meta_fields( $post, $request ) { + $product = wc_get_product( $post ); + + // Check for featured/gallery images, upload it and set it. + if ( isset( $request['images'] ) ) { + $product = $this->set_product_images( $product, $request['images'] ); + } + + // Save product meta fields. + $product = $this->set_product_meta( $product, $request ); + + // Save the product data. + $product->save(); + + // Save variations. + if ( $product->is_type( 'variable' ) ) { + if ( isset( $request['variations'] ) && is_array( $request['variations'] ) ) { + $this->save_variations_data( $product, $request ); + } + } + + // Clear caches here so in sync with any new variations/children. + wc_delete_product_transients( $product->get_id() ); + wp_cache_delete( 'product-' . $product->get_id(), 'products' ); + + return true; + } + + /** + * Clear cache/transients. + * + * @param WP_Post $post Post data. + */ + public function clear_transients( $post ) { + wc_delete_product_transients( $post->ID ); + } + + /** + * Delete post. + * + * @param int|WP_Post $id Post ID or WP_Post instance. + */ + protected function delete_post( $id ) { + if ( ! empty( $id->ID ) ) { + $id = $id->ID; + } elseif ( ! is_numeric( $id ) || 0 >= $id ) { + return; + } + + // Delete product attachments. + $attachments = get_posts( array( + 'post_parent' => $id, + 'post_status' => 'any', + 'post_type' => 'attachment', + ) ); + + foreach ( (array) $attachments as $attachment ) { + wp_delete_attachment( $attachment->ID, true ); + } + + // Delete product. + $product = wc_get_product( $id ); + $product->delete( true ); + } + + /** + * Delete a single item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error + */ + public function delete_item( $request ) { + $id = (int) $request['id']; + $force = (bool) $request['force']; + $post = get_post( $id ); + $product = wc_get_product( $id ); + + if ( ! empty( $post->post_type ) && 'product_variation' === $post->post_type && 'product' === $this->post_type ) { + return new WP_Error( "woocommerce_rest_invalid_{$this->post_type}_id", __( 'To manipulate product variations you should use the /products/<product_id>/variations/<id> endpoint.', 'woocommerce' ), array( 'status' => 404 ) ); + } elseif ( empty( $id ) || empty( $post->ID ) || $post->post_type !== $this->post_type ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid post ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $supports_trash = EMPTY_TRASH_DAYS > 0; + + /** + * Filter whether an item is trashable. + * + * Return false to disable trash support for the item. + * + * @param boolean $supports_trash Whether the item type support trashing. + * @param WP_Post $post The Post object being considered for trashing support. + */ + $supports_trash = apply_filters( "woocommerce_rest_{$this->post_type}_trashable", $supports_trash, $post ); + + if ( ! wc_rest_check_post_permissions( $this->post_type, 'delete', $post->ID ) ) { + /* translators: %s: post type */ + return new WP_Error( "woocommerce_rest_user_cannot_delete_{$this->post_type}", sprintf( __( 'Sorry, you are not allowed to delete %s.', 'woocommerce' ), $this->post_type ), array( 'status' => rest_authorization_required_code() ) ); + } + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $post, $request ); + + // If we're forcing, then delete permanently. + if ( $force ) { + if ( $product->is_type( 'variable' ) ) { + foreach ( $product->get_children() as $child_id ) { + $child = wc_get_product( $child_id ); + if ( ! empty( $child ) ) { + $child->delete( true ); + } + } + } else { + // For other product types, if the product has children, remove the relationship. + foreach ( $product->get_children() as $child_id ) { + $child = wc_get_product( $child_id ); + if ( ! empty( $child ) ) { + $child->set_parent_id( 0 ); + $child->save(); + } + } + } + + $product->delete( true ); + $result = ! ( $product->get_id() > 0 ); + } else { + // If we don't support trashing for this type, error out. + if ( ! $supports_trash ) { + /* translators: %s: post type */ + return new WP_Error( 'woocommerce_rest_trash_not_supported', sprintf( __( 'The %s does not support trashing.', 'woocommerce' ), $this->post_type ), array( 'status' => 501 ) ); + } + + // Otherwise, only trash if we haven't already. + if ( 'trash' === $post->post_status ) { + /* translators: %s: post type */ + return new WP_Error( 'woocommerce_rest_already_trashed', sprintf( __( 'The %s has already been deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 410 ) ); + } + + // (Note that internally this falls through to `wp_delete_post` if + // the trash is disabled.) + $product->delete(); + $result = 'trash' === $product->get_status(); + } + + if ( ! $result ) { + /* translators: %s: post type */ + return new WP_Error( 'woocommerce_rest_cannot_delete', sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 500 ) ); + } + + // Delete parent product transients. + if ( $parent_id = wp_get_post_parent_id( $id ) ) { + wc_delete_product_transients( $parent_id ); + } + + /** + * Fires after a single item is deleted or trashed via the REST API. + * + * @param object $post The deleted or trashed item. + * @param WP_REST_Response $response The response data. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( "woocommerce_rest_delete_{$this->post_type}", $post, $response, $request ); + + return $response; + } + + /** + * Get the Product's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $weight_unit = get_option( 'woocommerce_weight_unit' ); + $dimension_unit = get_option( 'woocommerce_dimension_unit' ); + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => $this->post_type, + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Product name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'slug' => array( + 'description' => __( 'Product slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'permalink' => array( + 'description' => __( 'Product URL.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( "The date the product was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the product was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'type' => array( + 'description' => __( 'Product type.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'simple', + 'enum' => array_keys( wc_get_product_types() ), + 'context' => array( 'view', 'edit' ), + ), + 'status' => array( + 'description' => __( 'Product status (post status).', 'woocommerce' ), + 'type' => 'string', + 'default' => 'publish', + 'enum' => array_merge( array_keys( get_post_statuses() ), array( 'future' ) ), + 'context' => array( 'view', 'edit' ), + ), + 'featured' => array( + 'description' => __( 'Featured product.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'catalog_visibility' => array( + 'description' => __( 'Catalog visibility.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'visible', + 'enum' => array( 'visible', 'catalog', 'search', 'hidden' ), + 'context' => array( 'view', 'edit' ), + ), + 'description' => array( + 'description' => __( 'Product description.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'short_description' => array( + 'description' => __( 'Product short description.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'sku' => array( + 'description' => __( 'Unique identifier.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'price' => array( + 'description' => __( 'Current product price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'regular_price' => array( + 'description' => __( 'Product regular price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'sale_price' => array( + 'description' => __( 'Product sale price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_from' => array( + 'description' => __( 'Start date of sale price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_to' => array( + 'description' => __( 'End date of sale price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'price_html' => array( + 'description' => __( 'Price formatted in HTML.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'on_sale' => array( + 'description' => __( 'Shows if the product is on sale.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'purchasable' => array( + 'description' => __( 'Shows if the product can be bought.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total_sales' => array( + 'description' => __( 'Amount of sales.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'virtual' => array( + 'description' => __( 'If the product is virtual.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'downloadable' => array( + 'description' => __( 'If the product is downloadable.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'downloads' => array( + 'description' => __( 'List of downloadable files.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'File ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'File name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'file' => array( + 'description' => __( 'File URL.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'download_limit' => array( + 'description' => __( 'Number of times downloadable files can be downloaded after purchase.', 'woocommerce' ), + 'type' => 'integer', + 'default' => -1, + 'context' => array( 'view', 'edit' ), + ), + 'download_expiry' => array( + 'description' => __( 'Number of days until access to downloadable files expires.', 'woocommerce' ), + 'type' => 'integer', + 'default' => -1, + 'context' => array( 'view', 'edit' ), + ), + 'download_type' => array( + 'description' => __( 'Download type, this controls the schema on the front-end.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'standard', + 'enum' => array( 'standard' ), + 'context' => array( 'view', 'edit' ), + ), + 'external_url' => array( + 'description' => __( 'Product external URL. Only for external products.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + ), + 'button_text' => array( + 'description' => __( 'Product external button text. Only for external products.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'tax_status' => array( + 'description' => __( 'Tax status.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'taxable', + 'enum' => array( 'taxable', 'shipping', 'none' ), + 'context' => array( 'view', 'edit' ), + ), + 'tax_class' => array( + 'description' => __( 'Tax class.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'manage_stock' => array( + 'description' => __( 'Stock management at product level.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'stock_quantity' => array( + 'description' => __( 'Stock quantity.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'in_stock' => array( + 'description' => __( 'Controls whether or not the product is listed as "in stock" or "out of stock" on the frontend.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => true, + 'context' => array( 'view', 'edit' ), + ), + 'backorders' => array( + 'description' => __( 'If managing stock, this controls if backorders are allowed.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'no', + 'enum' => array( 'no', 'notify', 'yes' ), + 'context' => array( 'view', 'edit' ), + ), + 'backorders_allowed' => array( + 'description' => __( 'Shows if backorders are allowed.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'backordered' => array( + 'description' => __( 'Shows if the product is on backordered.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'sold_individually' => array( + 'description' => __( 'Allow one item to be bought in a single order.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'weight' => array( + /* translators: %s: weight unit */ + 'description' => sprintf( __( 'Product weight (%s).', 'woocommerce' ), $weight_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'dimensions' => array( + 'description' => __( 'Product dimensions.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'length' => array( + /* translators: %s: dimension unit */ + 'description' => sprintf( __( 'Product length (%s).', 'woocommerce' ), $dimension_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'width' => array( + /* translators: %s: dimension unit */ + 'description' => sprintf( __( 'Product width (%s).', 'woocommerce' ), $dimension_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'height' => array( + /* translators: %s: dimension unit */ + 'description' => sprintf( __( 'Product height (%s).', 'woocommerce' ), $dimension_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'shipping_required' => array( + 'description' => __( 'Shows if the product need to be shipped.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'shipping_taxable' => array( + 'description' => __( 'Shows whether or not the product shipping is taxable.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'shipping_class' => array( + 'description' => __( 'Shipping class slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'shipping_class_id' => array( + 'description' => __( 'Shipping class ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'reviews_allowed' => array( + 'description' => __( 'Allow reviews.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => true, + 'context' => array( 'view', 'edit' ), + ), + 'average_rating' => array( + 'description' => __( 'Reviews average rating.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'rating_count' => array( + 'description' => __( 'Amount of reviews that the product have.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'related_ids' => array( + 'description' => __( 'List of related products IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'upsell_ids' => array( + 'description' => __( 'List of upsell products IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + ), + 'cross_sell_ids' => array( + 'description' => __( 'List of cross-sell products IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + ), + 'parent_id' => array( + 'description' => __( 'Product parent ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'purchase_note' => array( + 'description' => __( 'Optional note to send the customer after purchase.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'categories' => array( + 'description' => __( 'List of categories.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Category ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Category name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'slug' => array( + 'description' => __( 'Category slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + 'tags' => array( + 'description' => __( 'List of tags.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Tag ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Tag name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'slug' => array( + 'description' => __( 'Tag slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + 'images' => array( + 'description' => __( 'List of images.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Image ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'date_created' => array( + 'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'src' => array( + 'description' => __( 'Image URL.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Image name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'alt' => array( + 'description' => __( 'Image alternative text.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'position' => array( + 'description' => __( 'Image position. 0 means that the image is featured.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'attributes' => array( + 'description' => __( 'List of attributes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Attribute ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Attribute name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'position' => array( + 'description' => __( 'Attribute position.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'visible' => array( + 'description' => __( "Define if the attribute is visible on the \"Additional information\" tab in the product's page.", 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'variation' => array( + 'description' => __( 'Define if the attribute can be used as variation.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'options' => array( + 'description' => __( 'List of available term names of the attribute.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'default_attributes' => array( + 'description' => __( 'Defaults variation attributes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Attribute ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Attribute name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'option' => array( + 'description' => __( 'Selected attribute term name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'variations' => array( + 'description' => __( 'List of variations.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Variation ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( "The date the variation was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the variation was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'permalink' => array( + 'description' => __( 'Variation URL.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'sku' => array( + 'description' => __( 'Unique identifier.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'price' => array( + 'description' => __( 'Current variation price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'regular_price' => array( + 'description' => __( 'Variation regular price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'sale_price' => array( + 'description' => __( 'Variation sale price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_from' => array( + 'description' => __( 'Start date of sale price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_to' => array( + 'description' => __( 'End date of sale price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'on_sale' => array( + 'description' => __( 'Shows if the variation is on sale.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'purchasable' => array( + 'description' => __( 'Shows if the variation can be bought.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'visible' => array( + 'description' => __( 'If the variation is visible.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + ), + 'virtual' => array( + 'description' => __( 'If the variation is virtual.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'downloadable' => array( + 'description' => __( 'If the variation is downloadable.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'downloads' => array( + 'description' => __( 'List of downloadable files.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'File ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'File name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'file' => array( + 'description' => __( 'File URL.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'download_limit' => array( + 'description' => __( 'Number of times downloadable files can be downloaded after purchase.', 'woocommerce' ), + 'type' => 'integer', + 'default' => null, + 'context' => array( 'view', 'edit' ), + ), + 'download_expiry' => array( + 'description' => __( 'Number of days until access to downloadable files expires.', 'woocommerce' ), + 'type' => 'integer', + 'default' => null, + 'context' => array( 'view', 'edit' ), + ), + 'tax_status' => array( + 'description' => __( 'Tax status.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'taxable', + 'enum' => array( 'taxable', 'shipping', 'none' ), + 'context' => array( 'view', 'edit' ), + ), + 'tax_class' => array( + 'description' => __( 'Tax class.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'manage_stock' => array( + 'description' => __( 'Stock management at variation level.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'stock_quantity' => array( + 'description' => __( 'Stock quantity.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'in_stock' => array( + 'description' => __( 'Controls whether or not the variation is listed as "in stock" or "out of stock" on the frontend.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => true, + 'context' => array( 'view', 'edit' ), + ), + 'backorders' => array( + 'description' => __( 'If managing stock, this controls if backorders are allowed.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'no', + 'enum' => array( 'no', 'notify', 'yes' ), + 'context' => array( 'view', 'edit' ), + ), + 'backorders_allowed' => array( + 'description' => __( 'Shows if backorders are allowed.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'backordered' => array( + 'description' => __( 'Shows if the variation is on backordered.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'weight' => array( + /* translators: %s: weight unit */ + 'description' => sprintf( __( 'Variation weight (%s).', 'woocommerce' ), $weight_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'dimensions' => array( + 'description' => __( 'Variation dimensions.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'length' => array( + /* translators: %s: dimension unit */ + 'description' => sprintf( __( 'Variation length (%s).', 'woocommerce' ), $dimension_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'width' => array( + /* translators: %s: dimension unit */ + 'description' => sprintf( __( 'Variation width (%s).', 'woocommerce' ), $dimension_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'height' => array( + /* translators: %s: dimension unit */ + 'description' => sprintf( __( 'Variation height (%s).', 'woocommerce' ), $dimension_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'shipping_class' => array( + 'description' => __( 'Shipping class slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'shipping_class_id' => array( + 'description' => __( 'Shipping class ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'image' => array( + 'description' => __( 'Variation image data.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'id' => array( + 'description' => __( 'Image ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'date_created' => array( + 'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'src' => array( + 'description' => __( 'Image URL.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Image name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'alt' => array( + 'description' => __( 'Image alternative text.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'position' => array( + 'description' => __( 'Image position. 0 means that the image is featured.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'attributes' => array( + 'description' => __( 'List of attributes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Attribute ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Attribute name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'option' => array( + 'description' => __( 'Selected attribute term name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + ), + ), + ), + 'grouped_products' => array( + 'description' => __( 'List of grouped products ID.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'menu_order' => array( + 'description' => __( 'Menu order, used to custom sort products.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections of attachments. + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + $params['slug'] = array( + 'description' => __( 'Limit result set to products with a specific slug.', 'woocommerce' ), + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['status'] = array( + 'default' => 'any', + 'description' => __( 'Limit result set to products assigned a specific status.', 'woocommerce' ), + 'type' => 'string', + 'enum' => array_merge( array( 'any', 'future' ), array_keys( get_post_statuses() ) ), + 'sanitize_callback' => 'sanitize_key', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['type'] = array( + 'description' => __( 'Limit result set to products assigned a specific type.', 'woocommerce' ), + 'type' => 'string', + 'enum' => array_keys( wc_get_product_types() ), + 'sanitize_callback' => 'sanitize_key', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['category'] = array( + 'description' => __( 'Limit result set to products assigned a specific category ID.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'wp_parse_id_list', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['tag'] = array( + 'description' => __( 'Limit result set to products assigned a specific tag ID.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'wp_parse_id_list', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['shipping_class'] = array( + 'description' => __( 'Limit result set to products assigned a specific shipping class ID.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'wp_parse_id_list', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['attribute'] = array( + 'description' => __( 'Limit result set to products with a specific attribute.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['attribute_term'] = array( + 'description' => __( 'Limit result set to products with a specific attribute term ID (required an assigned attribute).', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'wp_parse_id_list', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['sku'] = array( + 'description' => __( 'Limit result set to products with a specific SKU.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } +} diff --git a/includes/rest-api/Controllers/Version1/class-wc-rest-report-sales-v1-controller.php b/includes/rest-api/Controllers/Version1/class-wc-rest-report-sales-v1-controller.php new file mode 100644 index 00000000000..d880c86e7e5 --- /dev/null +++ b/includes/rest-api/Controllers/Version1/class-wc-rest-report-sales-v1-controller.php @@ -0,0 +1,397 @@ +namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + } + + /** + * Check whether a given request has permission to read report. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'reports', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Get sales reports. + * + * @param WP_REST_Request $request + * @return array|WP_Error + */ + public function get_items( $request ) { + $data = array(); + $item = $this->prepare_item_for_response( null, $request ); + $data[] = $this->prepare_response_for_collection( $item ); + + return rest_ensure_response( $data ); + } + + /** + * Prepare a report sales object for serialization. + * + * @param null $_ + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $_, $request ) { + // Set date filtering. + $filter = array( + 'period' => $request['period'], + 'date_min' => $request['date_min'], + 'date_max' => $request['date_max'], + ); + $this->setup_report( $filter ); + + // New customers. + $users_query = new WP_User_Query( + array( + 'fields' => array( 'user_registered' ), + 'role' => 'customer', + ) + ); + + $customers = $users_query->get_results(); + + foreach ( $customers as $key => $customer ) { + if ( strtotime( $customer->user_registered ) < $this->report->start_date || strtotime( $customer->user_registered ) > $this->report->end_date ) { + unset( $customers[ $key ] ); + } + } + + $total_customers = count( $customers ); + $report_data = $this->report->get_report_data(); + $period_totals = array(); + + // Setup period totals by ensuring each period in the interval has data. + for ( $i = 0; $i <= $this->report->chart_interval; $i++ ) { + + switch ( $this->report->chart_groupby ) { + case 'day' : + $time = date( 'Y-m-d', strtotime( "+{$i} DAY", $this->report->start_date ) ); + break; + default : + $time = date( 'Y-m', strtotime( "+{$i} MONTH", $this->report->start_date ) ); + break; + } + + // Set the customer signups for each period. + $customer_count = 0; + foreach ( $customers as $customer ) { + if ( date( ( 'day' == $this->report->chart_groupby ) ? 'Y-m-d' : 'Y-m', strtotime( $customer->user_registered ) ) == $time ) { + $customer_count++; + } + } + + $period_totals[ $time ] = array( + 'sales' => wc_format_decimal( 0.00, 2 ), + 'orders' => 0, + 'items' => 0, + 'tax' => wc_format_decimal( 0.00, 2 ), + 'shipping' => wc_format_decimal( 0.00, 2 ), + 'discount' => wc_format_decimal( 0.00, 2 ), + 'customers' => $customer_count, + ); + } + + // add total sales, total order count, total tax and total shipping for each period + foreach ( $report_data->orders as $order ) { + $time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order->post_date ) ) : date( 'Y-m', strtotime( $order->post_date ) ); + + if ( ! isset( $period_totals[ $time ] ) ) { + continue; + } + + $period_totals[ $time ]['sales'] = wc_format_decimal( $order->total_sales, 2 ); + $period_totals[ $time ]['tax'] = wc_format_decimal( $order->total_tax + $order->total_shipping_tax, 2 ); + $period_totals[ $time ]['shipping'] = wc_format_decimal( $order->total_shipping, 2 ); + } + + foreach ( $report_data->order_counts as $order ) { + $time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order->post_date ) ) : date( 'Y-m', strtotime( $order->post_date ) ); + + if ( ! isset( $period_totals[ $time ] ) ) { + continue; + } + + $period_totals[ $time ]['orders'] = (int) $order->count; + } + + // Add total order items for each period. + foreach ( $report_data->order_items as $order_item ) { + $time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $order_item->post_date ) ) : date( 'Y-m', strtotime( $order_item->post_date ) ); + + if ( ! isset( $period_totals[ $time ] ) ) { + continue; + } + + $period_totals[ $time ]['items'] = (int) $order_item->order_item_count; + } + + // Add total discount for each period. + foreach ( $report_data->coupons as $discount ) { + $time = ( 'day' === $this->report->chart_groupby ) ? date( 'Y-m-d', strtotime( $discount->post_date ) ) : date( 'Y-m', strtotime( $discount->post_date ) ); + + if ( ! isset( $period_totals[ $time ] ) ) { + continue; + } + + $period_totals[ $time ]['discount'] = wc_format_decimal( $discount->discount_amount, 2 ); + } + + $sales_data = array( + 'total_sales' => $report_data->total_sales, + 'net_sales' => $report_data->net_sales, + 'average_sales' => $report_data->average_sales, + 'total_orders' => $report_data->total_orders, + 'total_items' => $report_data->total_items, + 'total_tax' => wc_format_decimal( $report_data->total_tax + $report_data->total_shipping_tax, 2 ), + 'total_shipping' => $report_data->total_shipping, + 'total_refunds' => $report_data->total_refunds, + 'total_discount' => $report_data->total_coupons, + 'totals_grouped_by' => $this->report->chart_groupby, + 'totals' => $period_totals, + 'total_customers' => $total_customers, + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $sales_data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + $response->add_links( array( + 'about' => array( + 'href' => rest_url( sprintf( '%s/reports', $this->namespace ) ), + ), + ) ); + + /** + * Filter a report sales returned from the API. + * + * Allows modification of the report sales data right before it is returned. + * + * @param WP_REST_Response $response The response object. + * @param stdClass $data The original report object. + * @param WP_REST_Request $request Request used to generate the response. + */ + return apply_filters( 'woocommerce_rest_prepare_report_sales', $response, (object) $sales_data, $request ); + } + + /** + * Setup the report object and parse any date filtering. + * + * @param array $filter date filtering + */ + protected function setup_report( $filter ) { + include_once( WC()->plugin_path() . '/includes/admin/reports/class-wc-admin-report.php' ); + include_once( WC()->plugin_path() . '/includes/admin/reports/class-wc-report-sales-by-date.php' ); + + $this->report = new WC_Report_Sales_By_Date(); + + if ( empty( $filter['period'] ) ) { + // Custom date range. + $filter['period'] = 'custom'; + + if ( ! empty( $filter['date_min'] ) || ! empty( $filter['date_max'] ) ) { + + // Overwrite _GET to make use of WC_Admin_Report::calculate_current_range() for custom date ranges. + $_GET['start_date'] = $filter['date_min']; + $_GET['end_date'] = isset( $filter['date_max'] ) ? $filter['date_max'] : null; + + } else { + + // Default custom range to today. + $_GET['start_date'] = $_GET['end_date'] = date( 'Y-m-d', current_time( 'timestamp' ) ); + } + } else { + $filter['period'] = empty( $filter['period'] ) ? 'week' : $filter['period']; + + // Change "week" period to "7day". + if ( 'week' === $filter['period'] ) { + $filter['period'] = '7day'; + } + } + + $this->report->calculate_current_range( $filter['period'] ); + } + + /** + * Get the Report's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'sales_report', + 'type' => 'object', + 'properties' => array( + 'total_sales' => array( + 'description' => __( 'Gross sales in the period.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'net_sales' => array( + 'description' => __( 'Net sales in the period.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'average_sales' => array( + 'description' => __( 'Average net daily sales.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'total_orders' => array( + 'description' => __( 'Total of orders placed.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'total_items' => array( + 'description' => __( 'Total of items purchased.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'total_tax' => array( + 'description' => __( 'Total charged for taxes.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'total_shipping' => array( + 'description' => __( 'Total charged for shipping.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'total_refunds' => array( + 'description' => __( 'Total of refunded orders.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'total_discount' => array( + 'description' => __( 'Total of coupons used.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'totals_grouped_by' => array( + 'description' => __( 'Group type.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'totals' => array( + 'description' => __( 'Totals.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'array', + ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + return array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + 'period' => array( + 'description' => __( 'Report period.', 'woocommerce' ), + 'type' => 'string', + 'enum' => array( 'week', 'month', 'last_month', 'year' ), + 'validate_callback' => 'rest_validate_request_arg', + 'sanitize_callback' => 'sanitize_text_field', + ), + 'date_min' => array( + /* translators: %s: date format */ + 'description' => sprintf( __( 'Return sales for a specific start date, the date need to be in the %s format.', 'woocommerce' ), 'YYYY-MM-DD' ), + 'type' => 'string', + 'format' => 'date', + 'validate_callback' => 'wc_rest_validate_reports_request_arg', + 'sanitize_callback' => 'sanitize_text_field', + ), + 'date_max' => array( + /* translators: %s: date format */ + 'description' => sprintf( __( 'Return sales for a specific end date, the date need to be in the %s format.', 'woocommerce' ), 'YYYY-MM-DD' ), + 'type' => 'string', + 'format' => 'date', + 'validate_callback' => 'wc_rest_validate_reports_request_arg', + 'sanitize_callback' => 'sanitize_text_field', + ), + ); + } +} diff --git a/includes/rest-api/Controllers/Version1/class-wc-rest-report-top-sellers-v1-controller.php b/includes/rest-api/Controllers/Version1/class-wc-rest-report-top-sellers-v1-controller.php new file mode 100644 index 00000000000..22e928e05b8 --- /dev/null +++ b/includes/rest-api/Controllers/Version1/class-wc-rest-report-top-sellers-v1-controller.php @@ -0,0 +1,174 @@ + $request['period'], + 'date_min' => $request['date_min'], + 'date_max' => $request['date_max'], + ); + $this->setup_report( $filter ); + + $report_data = $this->report->get_order_report_data( array( + 'data' => array( + '_product_id' => array( + 'type' => 'order_item_meta', + 'order_item_type' => 'line_item', + 'function' => '', + 'name' => 'product_id', + ), + '_qty' => array( + 'type' => 'order_item_meta', + 'order_item_type' => 'line_item', + 'function' => 'SUM', + 'name' => 'order_item_qty', + ), + ), + 'order_by' => 'order_item_qty DESC', + 'group_by' => 'product_id', + 'limit' => isset( $filter['limit'] ) ? absint( $filter['limit'] ) : 12, + 'query_type' => 'get_results', + 'filter_range' => true, + ) ); + + $top_sellers = array(); + + foreach ( $report_data as $item ) { + $product = wc_get_product( $item->product_id ); + + if ( $product ) { + $top_sellers[] = array( + 'name' => $product->get_name(), + 'product_id' => (int) $item->product_id, + 'quantity' => wc_stock_amount( $item->order_item_qty ), + ); + } + } + + $data = array(); + foreach ( $top_sellers as $top_seller ) { + $item = $this->prepare_item_for_response( (object) $top_seller, $request ); + $data[] = $this->prepare_response_for_collection( $item ); + } + + return rest_ensure_response( $data ); + } + + /** + * Prepare a report sales object for serialization. + * + * @param stdClass $top_seller + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $top_seller, $request ) { + $data = array( + 'name' => $top_seller->name, + 'product_id' => $top_seller->product_id, + 'quantity' => $top_seller->quantity, + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + $response->add_links( array( + 'about' => array( + 'href' => rest_url( sprintf( '%s/reports', $this->namespace ) ), + ), + 'product' => array( + 'href' => rest_url( sprintf( '/%s/products/%s', $this->namespace, $top_seller->product_id ) ), + ), + ) ); + + /** + * Filter a report top sellers returned from the API. + * + * Allows modification of the report top sellers data right before it is returned. + * + * @param WP_REST_Response $response The response object. + * @param stdClass $top_seller The original report object. + * @param WP_REST_Request $request Request used to generate the response. + */ + return apply_filters( 'woocommerce_rest_prepare_report_top_sellers', $response, $top_seller, $request ); + } + + /** + * Get the Report's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'top_sellers_report', + 'type' => 'object', + 'properties' => array( + 'name' => array( + 'description' => __( 'Product name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'product_id' => array( + 'description' => __( 'Product ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'quantity' => array( + 'description' => __( 'Total number of purchases.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/rest-api/Controllers/Version1/class-wc-rest-reports-v1-controller.php b/includes/rest-api/Controllers/Version1/class-wc-rest-reports-v1-controller.php new file mode 100644 index 00000000000..c35b9e6329b --- /dev/null +++ b/includes/rest-api/Controllers/Version1/class-wc-rest-reports-v1-controller.php @@ -0,0 +1,184 @@ +namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + } + + /** + * Check whether a given request has permission to read reports. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'reports', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Get reports list. + * + * @since 3.5.0 + * @return array + */ + protected function get_reports() { + return array( + array( + 'slug' => 'sales', + 'description' => __( 'List of sales reports.', 'woocommerce' ), + ), + array( + 'slug' => 'top_sellers', + 'description' => __( 'List of top sellers products.', 'woocommerce' ), + ), + ); + } + + /** + * Get all reports. + * + * @param WP_REST_Request $request + * @return array|WP_Error + */ + public function get_items( $request ) { + $data = array(); + $reports = $this->get_reports(); + + foreach ( $reports as $report ) { + $item = $this->prepare_item_for_response( (object) $report, $request ); + $data[] = $this->prepare_response_for_collection( $item ); + } + + return rest_ensure_response( $data ); + } + + /** + * Prepare a report object for serialization. + * + * @param stdClass $report Report data. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $report, $request ) { + $data = array( + 'slug' => $report->slug, + 'description' => $report->description, + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + $response->add_links( array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $report->slug ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ), + ), + ) ); + + /** + * Filter a report returned from the API. + * + * Allows modification of the report data right before it is returned. + * + * @param WP_REST_Response $response The response object. + * @param object $report The original report object. + * @param WP_REST_Request $request Request used to generate the response. + */ + return apply_filters( 'woocommerce_rest_prepare_report', $response, $report, $request ); + } + + /** + * Get the Report's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'report', + 'type' => 'object', + 'properties' => array( + 'slug' => array( + 'description' => __( 'An alphanumeric identifier for the resource.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'description' => array( + 'description' => __( 'A human-readable description of the resource.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + return array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ); + } +} diff --git a/includes/rest-api/Controllers/Version1/class-wc-rest-tax-classes-v1-controller.php b/includes/rest-api/Controllers/Version1/class-wc-rest-tax-classes-v1-controller.php new file mode 100644 index 00000000000..b71cc27d75e --- /dev/null +++ b/includes/rest-api/Controllers/Version1/class-wc-rest-tax-classes-v1-controller.php @@ -0,0 +1,321 @@ +namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P\w[\w\s\-]*)', array( + 'args' => array( + 'slug' => array( + 'description' => __( 'Unique slug for the resource.', 'woocommerce' ), + 'type' => 'string', + ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'type' => 'boolean', + 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + } + + /** + * Check whether a given request has permission to read tax classes. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access create tax classes. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return bool|WP_Error + */ + public function create_item_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'create' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access delete a tax. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return bool|WP_Error + */ + public function delete_item_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'delete' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Get all tax classes. + * + * @param WP_REST_Request $request + * @return array + */ + public function get_items( $request ) { + $tax_classes = array(); + + // Add standard class. + $tax_classes[] = array( + 'slug' => 'standard', + 'name' => __( 'Standard rate', 'woocommerce' ), + ); + + $classes = WC_Tax::get_tax_classes(); + + foreach ( $classes as $class ) { + $tax_classes[] = array( + 'slug' => sanitize_title( $class ), + 'name' => $class, + ); + } + + $data = array(); + foreach ( $tax_classes as $tax_class ) { + $class = $this->prepare_item_for_response( $tax_class, $request ); + $class = $this->prepare_response_for_collection( $class ); + $data[] = $class; + } + + return rest_ensure_response( $data ); + } + + /** + * Create a single tax class. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + $tax_class = WC_Tax::create_tax_class( $request['name'] ); + + if ( is_wp_error( $tax_class ) ) { + return new WP_Error( 'woocommerce_rest_' . $tax_class->get_error_code(), $tax_class->get_error_message(), array( 'status' => 400 ) ); + } + + $this->update_additional_fields_for_object( $tax_class, $request ); + + /** + * Fires after a tax class is created or updated via the REST API. + * + * @param stdClass $tax_class Data used to create the tax class. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating tax class, false when updating tax class. + */ + do_action( 'woocommerce_rest_insert_tax_class', (object) $tax_class, $request, true ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $tax_class, $request ); + $response = rest_ensure_response( $response ); + $response->set_status( 201 ); + $response->header( 'Location', rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $tax_class['slug'] ) ) ); + + return $response; + } + + /** + * Delete a single tax class. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function delete_item( $request ) { + global $wpdb; + + $force = isset( $request['force'] ) ? (bool) $request['force'] : false; + + // We don't support trashing for this type, error out. + if ( ! $force ) { + return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Taxes do not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); + } + + $tax_class = WC_Tax::get_tax_class_by( 'slug', sanitize_title( $request['slug'] ) ); + $deleted = WC_Tax::delete_tax_class_by( 'slug', sanitize_title( $request['slug'] ) ); + + if ( ! $deleted ) { + return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource id.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + if ( is_wp_error( $deleted ) ) { + return new WP_Error( 'woocommerce_rest_' . $deleted->get_error_code(), $deleted->get_error_message(), array( 'status' => 400 ) ); + } + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $tax_class, $request ); + + /** + * Fires after a tax class is deleted via the REST API. + * + * @param stdClass $tax_class The tax data. + * @param WP_REST_Response $response The response returned from the API. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( 'woocommerce_rest_delete_tax', (object) $tax_class, $response, $request ); + + return $response; + } + + /** + * Prepare a single tax class output for response. + * + * @param array $tax_class Tax class data. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $tax_class, $request ) { + $data = $tax_class; + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links() ); + + /** + * Filter tax object returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param stdClass $tax_class Tax object used to create response. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'woocommerce_rest_prepare_tax', $response, (object) $tax_class, $request ); + } + + /** + * Prepare links for the request. + * + * @return array Links for the given tax class. + */ + protected function prepare_links() { + $links = array( + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + + return $links; + } + + /** + * Get the Tax Classes schema, conforming to JSON Schema + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'tax_class', + 'type' => 'object', + 'properties' => array( + 'slug' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Tax class name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'required' => true, + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + return array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ); + } +} diff --git a/includes/rest-api/Controllers/Version1/class-wc-rest-taxes-v1-controller.php b/includes/rest-api/Controllers/Version1/class-wc-rest-taxes-v1-controller.php new file mode 100644 index 00000000000..bbbc1869314 --- /dev/null +++ b/includes/rest-api/Controllers/Version1/class-wc-rest-taxes-v1-controller.php @@ -0,0 +1,709 @@ +namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'type' => 'boolean', + 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'batch_items' ), + 'permission_callback' => array( $this, 'batch_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_batch_schema' ), + ) ); + } + + /** + * Check whether a given request has permission to read taxes. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access create taxes. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return bool|WP_Error + */ + public function create_item_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'create' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to read a tax. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access update a tax. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return bool|WP_Error + */ + public function update_item_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'edit' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access delete a tax. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return bool|WP_Error + */ + public function delete_item_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'delete' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access batch create, update and delete items. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return bool|WP_Error + */ + public function batch_items_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'batch' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_batch', __( 'Sorry, you are not allowed to batch manipulate this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Get all taxes. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + global $wpdb; + + $prepared_args = array(); + $prepared_args['order'] = $request['order']; + $prepared_args['number'] = $request['per_page']; + if ( ! empty( $request['offset'] ) ) { + $prepared_args['offset'] = $request['offset']; + } else { + $prepared_args['offset'] = ( $request['page'] - 1 ) * $prepared_args['number']; + } + $orderby_possibles = array( + 'id' => 'tax_rate_id', + 'order' => 'tax_rate_order', + ); + $prepared_args['orderby'] = $orderby_possibles[ $request['orderby'] ]; + $prepared_args['class'] = $request['class']; + + /** + * Filter arguments, before passing to $wpdb->get_results(), when querying taxes via the REST API. + * + * @param array $prepared_args Array of arguments for $wpdb->get_results(). + * @param WP_REST_Request $request The current request. + */ + $prepared_args = apply_filters( 'woocommerce_rest_tax_query', $prepared_args, $request ); + + $query = " + SELECT * + FROM {$wpdb->prefix}woocommerce_tax_rates + WHERE 1 = 1 + "; + + // Filter by tax class. + if ( ! empty( $prepared_args['class'] ) ) { + $class = 'standard' !== $prepared_args['class'] ? sanitize_title( $prepared_args['class'] ) : ''; + $query .= " AND tax_rate_class = '$class'"; + } + + // Order tax rates. + $order_by = sprintf( ' ORDER BY %s', sanitize_key( $prepared_args['orderby'] ) ); + + // Pagination. + $pagination = sprintf( ' LIMIT %d, %d', $prepared_args['offset'], $prepared_args['number'] ); + + // Query taxes. + $results = $wpdb->get_results( $query . $order_by . $pagination ); + + $taxes = array(); + foreach ( $results as $tax ) { + $data = $this->prepare_item_for_response( $tax, $request ); + $taxes[] = $this->prepare_response_for_collection( $data ); + } + + $response = rest_ensure_response( $taxes ); + + // Store pagination values for headers then unset for count query. + $per_page = (int) $prepared_args['number']; + $page = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 ); + + // Query only for ids. + $wpdb->get_results( str_replace( 'SELECT *', 'SELECT tax_rate_id', $query ) ); + + // Calculate totals. + $total_taxes = (int) $wpdb->num_rows; + $response->header( 'X-WP-Total', (int) $total_taxes ); + $max_pages = ceil( $total_taxes / $per_page ); + $response->header( 'X-WP-TotalPages', (int) $max_pages ); + + $base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) ); + if ( $page > 1 ) { + $prev_page = $page - 1; + if ( $prev_page > $max_pages ) { + $prev_page = $max_pages; + } + $prev_link = add_query_arg( 'page', $prev_page, $base ); + $response->link_header( 'prev', $prev_link ); + } + if ( $max_pages > $page ) { + $next_page = $page + 1; + $next_link = add_query_arg( 'page', $next_page, $base ); + $response->link_header( 'next', $next_link ); + } + + return $response; + } + + /** + * Take tax data from the request and return the updated or newly created rate. + * + * @param WP_REST_Request $request Full details about the request. + * @param stdClass|null $current Existing tax object. + * @return object + */ + protected function create_or_update_tax( $request, $current = null ) { + $id = absint( isset( $request['id'] ) ? $request['id'] : 0 ); + $data = array(); + $fields = array( + 'tax_rate_country', + 'tax_rate_state', + 'tax_rate', + 'tax_rate_name', + 'tax_rate_priority', + 'tax_rate_compound', + 'tax_rate_shipping', + 'tax_rate_order', + 'tax_rate_class', + ); + + foreach ( $fields as $field ) { + // Keys via API differ from the stored names returned by _get_tax_rate. + $key = 'tax_rate' === $field ? 'rate' : str_replace( 'tax_rate_', '', $field ); + + // Remove data that was not posted. + if ( ! isset( $request[ $key ] ) ) { + continue; + } + + // Test new data against current data. + if ( $current && $current->$field === $request[ $key ] ) { + continue; + } + + // Add to data array. + switch ( $key ) { + case 'tax_rate_priority' : + case 'tax_rate_compound' : + case 'tax_rate_shipping' : + case 'tax_rate_order' : + $data[ $field ] = absint( $request[ $key ] ); + break; + case 'tax_rate_class' : + $data[ $field ] = 'standard' !== $request['tax_rate_class'] ? $request['tax_rate_class'] : ''; + break; + default : + $data[ $field ] = wc_clean( $request[ $key ] ); + break; + } + } + + if ( $id ) { + WC_Tax::_update_tax_rate( $id, $data ); + } else { + $id = WC_Tax::_insert_tax_rate( $data ); + } + + // Add locales. + if ( ! empty( $request['postcode'] ) ) { + WC_Tax::_update_tax_rate_postcodes( $id, wc_clean( $request['postcode'] ) ); + } + if ( ! empty( $request['city'] ) ) { + WC_Tax::_update_tax_rate_cities( $id, wc_clean( $request['city'] ) ); + } + + return WC_Tax::_get_tax_rate( $id, OBJECT ); + } + + /** + * Create a single tax. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + if ( ! empty( $request['id'] ) ) { + return new WP_Error( 'woocommerce_rest_tax_exists', __( 'Cannot create existing resource.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $tax = $this->create_or_update_tax( $request ); + + $this->update_additional_fields_for_object( $tax, $request ); + + /** + * Fires after a tax is created or updated via the REST API. + * + * @param stdClass $tax Data used to create the tax. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating tax, false when updating tax. + */ + do_action( 'woocommerce_rest_insert_tax', $tax, $request, true ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $tax, $request ); + $response = rest_ensure_response( $response ); + $response->set_status( 201 ); + $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $tax->tax_rate_id ) ) ); + + return $response; + } + + /** + * Get a single tax. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + $id = (int) $request['id']; + $tax_obj = WC_Tax::_get_tax_rate( $id, OBJECT ); + + if ( empty( $id ) || empty( $tax_obj ) ) { + return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $tax = $this->prepare_item_for_response( $tax_obj, $request ); + $response = rest_ensure_response( $tax ); + + return $response; + } + + /** + * Update a single tax. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function update_item( $request ) { + $id = (int) $request['id']; + $tax_obj = WC_Tax::_get_tax_rate( $id, OBJECT ); + + if ( empty( $id ) || empty( $tax_obj ) ) { + return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $tax = $this->create_or_update_tax( $request, $tax_obj ); + + $this->update_additional_fields_for_object( $tax, $request ); + + /** + * Fires after a tax is created or updated via the REST API. + * + * @param stdClass $tax Data used to create the tax. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating tax, false when updating tax. + */ + do_action( 'woocommerce_rest_insert_tax', $tax, $request, false ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $tax, $request ); + $response = rest_ensure_response( $response ); + + return $response; + } + + /** + * Delete a single tax. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function delete_item( $request ) { + global $wpdb; + + $id = (int) $request['id']; + $force = isset( $request['force'] ) ? (bool) $request['force'] : false; + + // We don't support trashing for this type, error out. + if ( ! $force ) { + return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Taxes do not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); + } + + $tax = WC_Tax::_get_tax_rate( $id, OBJECT ); + + if ( empty( $id ) || empty( $tax ) ) { + return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $tax, $request ); + + WC_Tax::_delete_tax_rate( $id ); + + if ( 0 === $wpdb->rows_affected ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'The resource cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) ); + } + + /** + * Fires after a tax is deleted via the REST API. + * + * @param stdClass $tax The tax data. + * @param WP_REST_Response $response The response returned from the API. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( 'woocommerce_rest_delete_tax', $tax, $response, $request ); + + return $response; + } + + /** + * Prepare a single tax output for response. + * + * @param stdClass $tax Tax object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $tax, $request ) { + global $wpdb; + + $id = (int) $tax->tax_rate_id; + $data = array( + 'id' => $id, + 'country' => $tax->tax_rate_country, + 'state' => $tax->tax_rate_state, + 'postcode' => '', + 'city' => '', + 'rate' => $tax->tax_rate, + 'name' => $tax->tax_rate_name, + 'priority' => (int) $tax->tax_rate_priority, + 'compound' => (bool) $tax->tax_rate_compound, + 'shipping' => (bool) $tax->tax_rate_shipping, + 'order' => (int) $tax->tax_rate_order, + 'class' => $tax->tax_rate_class ? $tax->tax_rate_class : 'standard', + ); + + // Get locales from a tax rate. + $locales = $wpdb->get_results( $wpdb->prepare( " + SELECT location_code, location_type + FROM {$wpdb->prefix}woocommerce_tax_rate_locations + WHERE tax_rate_id = %d + ", $id ) ); + + if ( ! is_wp_error( $tax ) && ! is_null( $tax ) ) { + foreach ( $locales as $locale ) { + $data[ $locale->location_type ] = $locale->location_code; + } + } + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $tax ) ); + + /** + * Filter tax object returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param stdClass $tax Tax object used to create response. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'woocommerce_rest_prepare_tax', $response, $tax, $request ); + } + + /** + * Prepare links for the request. + * + * @param stdClass $tax Tax object. + * @return array Links for the given tax. + */ + protected function prepare_links( $tax ) { + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $tax->tax_rate_id ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + + return $links; + } + + /** + * Get the Taxes schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'tax', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'country' => array( + 'description' => __( 'Country ISO 3166 code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'state' => array( + 'description' => __( 'State code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'postcode' => array( + 'description' => __( 'Postcode / ZIP.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'city' => array( + 'description' => __( 'City name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'rate' => array( + 'description' => __( 'Tax rate.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Tax rate name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'priority' => array( + 'description' => __( 'Tax priority.', 'woocommerce' ), + 'type' => 'integer', + 'default' => 1, + 'context' => array( 'view', 'edit' ), + ), + 'compound' => array( + 'description' => __( 'Whether or not this is a compound rate.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'shipping' => array( + 'description' => __( 'Whether or not this tax rate also gets applied to shipping.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => true, + 'context' => array( 'view', 'edit' ), + ), + 'order' => array( + 'description' => __( 'Indicates the order that will appear in queries.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'class' => array( + 'description' => __( 'Tax class.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'standard', + 'enum' => array_merge( array( 'standard' ), WC_Tax::get_tax_class_slugs() ), + 'context' => array( 'view', 'edit' ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + $params = array(); + $params['context'] = $this->get_context_param(); + $params['context']['default'] = 'view'; + + $params['page'] = array( + 'description' => __( 'Current page of the collection.', 'woocommerce' ), + 'type' => 'integer', + 'default' => 1, + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + 'minimum' => 1, + ); + $params['per_page'] = array( + 'description' => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ), + 'type' => 'integer', + 'default' => 10, + 'minimum' => 1, + 'maximum' => 100, + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['offset'] = array( + 'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['order'] = array( + 'default' => 'asc', + 'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ), + 'enum' => array( 'asc', 'desc' ), + 'sanitize_callback' => 'sanitize_key', + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['orderby'] = array( + 'default' => 'order', + 'description' => __( 'Sort collection by object attribute.', 'woocommerce' ), + 'enum' => array( + 'id', + 'order', + ), + 'sanitize_callback' => 'sanitize_key', + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['class'] = array( + 'description' => __( 'Sort by tax class.', 'woocommerce' ), + 'enum' => array_merge( array( 'standard' ), WC_Tax::get_tax_class_slugs() ), + 'sanitize_callback' => 'sanitize_title', + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } +} diff --git a/includes/rest-api/Controllers/Version1/class-wc-rest-webhook-deliveries-v1-controller.php b/includes/rest-api/Controllers/Version1/class-wc-rest-webhook-deliveries-v1-controller.php new file mode 100644 index 00000000000..033a2c797c8 --- /dev/null +++ b/includes/rest-api/Controllers/Version1/class-wc-rest-webhook-deliveries-v1-controller.php @@ -0,0 +1,314 @@ +/deliveries endpoint. + * + * @author WooThemes + * @category API + * @package Automattic/WooCommerce/RestApi + * @since 3.0.0 + */ + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +/** + * REST API Webhook Deliveries controller class. + * + * @deprecated 3.3.0 Webhooks deliveries logs now uses logging system. + * @package Automattic/WooCommerce/RestApi + * @extends WC_REST_Controller + */ +class WC_REST_Webhook_Deliveries_V1_Controller extends WC_REST_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + protected $namespace = 'wc/v1'; + + /** + * Route base. + * + * @var string + */ + protected $rest_base = 'webhooks/(?P[\d]+)/deliveries'; + + /** + * Register the routes for webhook deliveries. + */ + public function register_routes() { + register_rest_route( $this->namespace, '/' . $this->rest_base, array( + 'args' => array( + 'webhook_id' => array( + 'description' => __( 'Unique identifier for the webhook.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + 'args' => array( + 'webhook_id' => array( + 'description' => __( 'Unique identifier for the webhook.', 'woocommerce' ), + 'type' => 'integer', + ), + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + } + + /** + * Check whether a given request has permission to read taxes. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'webhooks', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to read a tax. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'webhooks', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Get all webhook deliveries. + * + * @param WP_REST_Request $request + * + * @return array|WP_Error + */ + public function get_items( $request ) { + $webhook = wc_get_webhook( (int) $request['webhook_id'] ); + + if ( empty( $webhook ) || is_null( $webhook ) ) { + return new WP_Error( 'woocommerce_rest_webhook_invalid_id', __( 'Invalid webhook ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $logs = array(); + $data = array(); + foreach ( $logs as $log ) { + $delivery = $this->prepare_item_for_response( (object) $log, $request ); + $delivery = $this->prepare_response_for_collection( $delivery ); + $data[] = $delivery; + } + + return rest_ensure_response( $data ); + } + + /** + * Get a single webhook delivery. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + $id = (int) $request['id']; + $webhook = wc_get_webhook( (int) $request['webhook_id'] ); + + if ( empty( $webhook ) || is_null( $webhook ) ) { + return new WP_Error( 'woocommerce_rest_webhook_invalid_id', __( 'Invalid webhook ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $log = array(); + + if ( empty( $id ) || empty( $log ) ) { + return new WP_Error( 'woocommerce_rest_invalid_id', __( 'Invalid resource ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $delivery = $this->prepare_item_for_response( (object) $log, $request ); + $response = rest_ensure_response( $delivery ); + + return $response; + } + + /** + * Prepare a single webhook delivery output for response. + * + * @param stdClass $log Delivery log object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $log, $request ) { + $data = (array) $log; + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $log ) ); + + /** + * Filter webhook delivery object returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param stdClass $log Delivery log object used to create response. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'woocommerce_rest_prepare_webhook_delivery', $response, $log, $request ); + } + + /** + * Prepare links for the request. + * + * @param stdClass $log Delivery log object. + * @return array Links for the given webhook delivery. + */ + protected function prepare_links( $log ) { + $webhook_id = (int) $log->request_headers['X-WC-Webhook-ID']; + $base = str_replace( '(?P[\d]+)', $webhook_id, $this->rest_base ); + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $log->id ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ), + ), + 'up' => array( + 'href' => rest_url( sprintf( '/%s/webhooks/%d', $this->namespace, $webhook_id ) ), + ), + ); + + return $links; + } + + /** + * Get the Webhook's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'webhook_delivery', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'duration' => array( + 'description' => __( 'The delivery duration, in seconds.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'summary' => array( + 'description' => __( 'A friendly summary of the response including the HTTP response code, message, and body.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'request_url' => array( + 'description' => __( 'The URL where the webhook was delivered.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'request_headers' => array( + 'description' => __( 'Request headers.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view' ), + 'readonly' => true, + 'items' => array( + 'type' => 'string', + ), + ), + 'request_body' => array( + 'description' => __( 'Request body.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'response_code' => array( + 'description' => __( 'The HTTP response code from the receiving server.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'response_message' => array( + 'description' => __( 'The HTTP response message from the receiving server.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'response_headers' => array( + 'description' => __( 'Array of the response headers from the receiving server.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view' ), + 'readonly' => true, + 'items' => array( + 'type' => 'string', + ), + ), + 'response_body' => array( + 'description' => __( 'The response body from the receiving server.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( "The date the webhook delivery was logged, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + return array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ); + } +} diff --git a/includes/rest-api/Controllers/Version1/class-wc-rest-webhooks-v1-controller.php b/includes/rest-api/Controllers/Version1/class-wc-rest-webhooks-v1-controller.php new file mode 100644 index 00000000000..9ad7e982b25 --- /dev/null +++ b/includes/rest-api/Controllers/Version1/class-wc-rest-webhooks-v1-controller.php @@ -0,0 +1,763 @@ +namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => array_merge( $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( + 'topic' => array( + 'required' => true, + 'type' => 'string', + 'description' => __( 'Webhook topic.', 'woocommerce' ), + ), + 'delivery_url' => array( + 'required' => true, + 'type' => 'string', + 'description' => __( 'Webhook delivery URL.', 'woocommerce' ), + ), + ) ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'type' => 'boolean', + 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) ); + + register_rest_route( $this->namespace, '/' . $this->rest_base . '/batch', array( + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'batch_items' ), + 'permission_callback' => array( $this, 'batch_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_batch_schema' ), + ) ); + } + + /** + * Check whether a given request has permission to read webhooks. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'webhooks', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access create webhooks. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return bool|WP_Error + */ + public function create_item_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'webhooks', 'create' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to read a webhook. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'webhooks', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access update a webhook. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return bool|WP_Error + */ + public function update_item_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'webhooks', 'edit' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access delete a webhook. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return bool|WP_Error + */ + public function delete_item_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'webhooks', 'delete' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access batch create, update and delete items. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return bool|WP_Error + */ + public function batch_items_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'webhooks', 'batch' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_batch', __( 'Sorry, you are not allowed to batch manipulate this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Get the default REST API version. + * + * @since 3.0.0 + * @return string + */ + protected function get_default_api_version() { + return 'wp_api_v1'; + } + + /** + * Get all webhooks. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + $args = array(); + $args['order'] = $request['order']; + $args['orderby'] = $request['orderby']; + $args['status'] = 'all' === $request['status'] ? '' : $request['status']; + $args['include'] = implode( ',', $request['include'] ); + $args['exclude'] = implode( ',', $request['exclude'] ); + $args['limit'] = $request['per_page']; + $args['search'] = $request['search']; + $args['before'] = $request['before']; + $args['after'] = $request['after']; + + if ( empty( $request['offset'] ) ) { + $args['offset'] = 1 < $request['page'] ? ( $request['page'] - 1 ) * $args['limit'] : 0; + } + + /** + * Filter arguments, before passing to WC_Webhook_Data_Store->search_webhooks, when querying webhooks via the REST API. + * + * @param array $args Array of arguments for $wpdb->get_results(). + * @param WP_REST_Request $request The current request. + */ + $prepared_args = apply_filters( 'woocommerce_rest_webhook_query', $args, $request ); + unset( $prepared_args['page'] ); + $prepared_args['paginate'] = true; + + // Get the webhooks. + $webhooks = array(); + $data_store = WC_Data_Store::load( 'webhook' ); + $results = $data_store->search_webhooks( $prepared_args ); + $webhook_ids = $results->webhooks; + + foreach ( $webhook_ids as $webhook_id ) { + $data = $this->prepare_item_for_response( $webhook_id, $request ); + $webhooks[] = $this->prepare_response_for_collection( $data ); + } + + $response = rest_ensure_response( $webhooks ); + $per_page = (int) $prepared_args['limit']; + $page = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 ); + $total_webhooks = $results->total; + $max_pages = $results->max_num_pages; + $base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) ); + + $response->header( 'X-WP-Total', $total_webhooks ); + $response->header( 'X-WP-TotalPages', $max_pages ); + + if ( $page > 1 ) { + $prev_page = $page - 1; + if ( $prev_page > $max_pages ) { + $prev_page = $max_pages; + } + $prev_link = add_query_arg( 'page', $prev_page, $base ); + $response->link_header( 'prev', $prev_link ); + } + if ( $max_pages > $page ) { + $next_page = $page + 1; + $next_link = add_query_arg( 'page', $next_page, $base ); + $response->link_header( 'next', $next_link ); + } + + return $response; + } + + /** + * Get a single item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + $id = (int) $request['id']; + + if ( empty( $id ) ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $data = $this->prepare_item_for_response( $id, $request ); + $response = rest_ensure_response( $data ); + + return $response; + } + + /** + * Create a single webhook. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + if ( ! empty( $request['id'] ) ) { + /* translators: %s: post type */ + return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) ); + } + + // Validate topic. + if ( empty( $request['topic'] ) || ! wc_is_webhook_valid_topic( strtolower( $request['topic'] ) ) ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_topic", __( 'Webhook topic is required and must be valid.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + // Validate delivery URL. + if ( empty( $request['delivery_url'] ) || ! wc_is_valid_url( $request['delivery_url'] ) ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_delivery_url", __( 'Webhook delivery URL must be a valid URL starting with http:// or https://.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $post = $this->prepare_item_for_database( $request ); + if ( is_wp_error( $post ) ) { + return $post; + } + + $webhook = new WC_Webhook(); + $webhook->set_name( $post->post_title ); + $webhook->set_user_id( $post->post_author ); + $webhook->set_status( 'publish' === $post->post_status ? 'active' : 'disabled' ); + $webhook->set_topic( $request['topic'] ); + $webhook->set_delivery_url( $request['delivery_url'] ); + $webhook->set_secret( ! empty( $request['secret'] ) ? $request['secret'] : wp_generate_password( 50, true, true ) ); + $webhook->set_api_version( $this->get_default_api_version() ); + $webhook->save(); + + $this->update_additional_fields_for_object( $webhook, $request ); + + /** + * Fires after a single item is created or updated via the REST API. + * + * @param WC_Webhook $webhook Webhook data. + * @param WP_REST_Request $request Request object. + * @param bool $creating True when creating item, false when updating. + */ + do_action( "woocommerce_rest_insert_webhook_object", $webhook, $request, true ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $webhook->get_id(), $request ); + $response = rest_ensure_response( $response ); + $response->set_status( 201 ); + $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $webhook->get_id() ) ) ); + + // Send ping. + $webhook->deliver_ping(); + + return $response; + } + + /** + * Update a single webhook. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function update_item( $request ) { + $id = (int) $request['id']; + $webhook = wc_get_webhook( $id ); + + if ( empty( $webhook ) || is_null( $webhook ) ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + // Update topic. + if ( ! empty( $request['topic'] ) ) { + if ( wc_is_webhook_valid_topic( strtolower( $request['topic'] ) ) ) { + $webhook->set_topic( $request['topic'] ); + } else { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_topic", __( 'Webhook topic must be valid.', 'woocommerce' ), array( 'status' => 400 ) ); + } + } + + // Update delivery URL. + if ( ! empty( $request['delivery_url'] ) ) { + if ( wc_is_valid_url( $request['delivery_url'] ) ) { + $webhook->set_delivery_url( $request['delivery_url'] ); + } else { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_delivery_url", __( 'Webhook delivery URL must be a valid URL starting with http:// or https://.', 'woocommerce' ), array( 'status' => 400 ) ); + } + } + + // Update secret. + if ( ! empty( $request['secret'] ) ) { + $webhook->set_secret( $request['secret'] ); + } + + // Update status. + if ( ! empty( $request['status'] ) ) { + if ( wc_is_webhook_valid_status( strtolower( $request['status'] ) ) ) { + $webhook->set_status( $request['status'] ); + } else { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_status", __( 'Webhook status must be valid.', 'woocommerce' ), array( 'status' => 400 ) ); + } + } + + $post = $this->prepare_item_for_database( $request ); + if ( is_wp_error( $post ) ) { + return $post; + } + + if ( isset( $post->post_title ) ) { + $webhook->set_name( $post->post_title ); + } + + $webhook->save(); + + $this->update_additional_fields_for_object( $webhook, $request ); + + /** + * Fires after a single item is created or updated via the REST API. + * + * @param WC_Webhook $webhook Webhook data. + * @param WP_REST_Request $request Request object. + * @param bool $creating True when creating item, false when updating. + */ + do_action( "woocommerce_rest_insert_webhook_object", $webhook, $request, false ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $webhook->get_id(), $request ); + + return rest_ensure_response( $response ); + } + + /** + * Delete a single webhook. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error + */ + public function delete_item( $request ) { + $id = (int) $request['id']; + $force = isset( $request['force'] ) ? (bool) $request['force'] : false; + + // We don't support trashing for this type, error out. + if ( ! $force ) { + return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Webhooks do not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); + } + + $webhook = wc_get_webhook( $id ); + + if ( empty( $webhook ) || is_null( $webhook ) ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $webhook, $request ); + $result = $webhook->delete( true ); + + if ( ! $result ) { + /* translators: %s: post type */ + return new WP_Error( 'woocommerce_rest_cannot_delete', sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 500 ) ); + } + + /** + * Fires after a single item is deleted or trashed via the REST API. + * + * @param WC_Webhook $webhook The deleted or trashed item. + * @param WP_REST_Response $response The response data. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( "woocommerce_rest_delete_webhook_object", $webhook, $response, $request ); + + return $response; + } + + /** + * Prepare a single webhook for create or update. + * + * @param WP_REST_Request $request Request object. + * @return WP_Error|stdClass $data Post object. + */ + protected function prepare_item_for_database( $request ) { + $data = new stdClass; + + // Post ID. + if ( isset( $request['id'] ) ) { + $data->ID = absint( $request['id'] ); + } + + // Validate required POST fields. + if ( 'POST' === $request->get_method() && empty( $data->ID ) ) { + $data->post_title = ! empty( $request['name'] ) ? $request['name'] : sprintf( __( 'Webhook created on %s', 'woocommerce' ), strftime( _x( '%b %d, %Y @ %I:%M %p', 'Webhook created on date parsed by strftime', 'woocommerce' ) ) ); // @codingStandardsIgnoreLine + + // Post author. + $data->post_author = get_current_user_id(); + + // Post password. + $data->post_password = 'webhook_' . wp_generate_password(); + + // Post status. + $data->post_status = 'publish'; + } else { + + // Allow edit post title. + if ( ! empty( $request['name'] ) ) { + $data->post_title = $request['name']; + } + } + + // Comment status. + $data->comment_status = 'closed'; + + // Ping status. + $data->ping_status = 'closed'; + + /** + * Filter the query_vars used in `get_items` for the constructed query. + * + * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being + * prepared for insertion. + * + * @param stdClass $data An object representing a single item prepared + * for inserting or updating the database. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}", $data, $request ); + } + + /** + * Prepare a single webhook output for response. + * + * @param int $id Webhook ID or object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $id, $request ) { + $webhook = wc_get_webhook( $id ); + + if ( empty( $webhook ) || is_null( $webhook ) ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $data = array( + 'id' => $webhook->get_id(), + 'name' => $webhook->get_name(), + 'status' => $webhook->get_status(), + 'topic' => $webhook->get_topic(), + 'resource' => $webhook->get_resource(), + 'event' => $webhook->get_event(), + 'hooks' => $webhook->get_hooks(), + 'delivery_url' => $webhook->get_delivery_url(), + 'date_created' => wc_rest_prepare_date_response( $webhook->get_date_created() ), + 'date_modified' => wc_rest_prepare_date_response( $webhook->get_date_modified() ), + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $webhook->get_id() ) ); + + /** + * Filter webhook object returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param WC_Webhook $webhook Webhook object used to create response. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $webhook, $request ); + } + + /** + * Prepare links for the request. + * + * @param int $id Webhook ID. + * @return array + */ + protected function prepare_links( $id ) { + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $id ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + + return $links; + } + + /** + * Get the Webhook's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'webhook', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'A friendly name for the webhook.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'status' => array( + 'description' => __( 'Webhook status.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'active', + 'enum' => array_keys( wc_get_webhook_statuses() ), + 'context' => array( 'view', 'edit' ), + ), + 'topic' => array( + 'description' => __( 'Webhook topic.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'resource' => array( + 'description' => __( 'Webhook resource.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'event' => array( + 'description' => __( 'Webhook event.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'hooks' => array( + 'description' => __( 'WooCommerce action names associated with the webhook.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'string', + ), + ), + 'delivery_url' => array( + 'description' => __( 'The URL where the webhook payload is delivered.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'secret' => array( + 'description' => __( "Secret key used to generate a hash of the delivered webhook and provided in the request headers. This will default to a MD5 hash from the current user's ID|username if not provided.", 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'edit' ), + ), + 'date_created' => array( + 'description' => __( "The date the webhook was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the webhook was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections of attachments. + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + $params['context']['default'] = 'view'; + + $params['after'] = array( + 'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'date-time', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['before'] = array( + 'description' => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'date-time', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['exclude'] = array( + 'description' => __( 'Ensure result set excludes specific IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'default' => array(), + 'sanitize_callback' => 'wp_parse_id_list', + ); + $params['include'] = array( + 'description' => __( 'Limit result set to specific ids.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'default' => array(), + 'sanitize_callback' => 'wp_parse_id_list', + ); + $params['offset'] = array( + 'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['order'] = array( + 'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'desc', + 'enum' => array( 'asc', 'desc' ), + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['orderby'] = array( + 'description' => __( 'Sort collection by object attribute.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'date', + 'enum' => array( + 'date', + 'id', + 'title', + ), + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['status'] = array( + 'default' => 'all', + 'description' => __( 'Limit result set to webhooks assigned a specific status.', 'woocommerce' ), + 'type' => 'string', + 'enum' => array( 'all', 'active', 'paused', 'disabled' ), + 'sanitize_callback' => 'sanitize_key', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } +} diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-coupons-v2-controller.php b/includes/rest-api/Controllers/Version2/class-wc-rest-coupons-v2-controller.php new file mode 100644 index 00000000000..07bda40697c --- /dev/null +++ b/includes/rest-api/Controllers/Version2/class-wc-rest-coupons-v2-controller.php @@ -0,0 +1,542 @@ +namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => array_merge( + $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( + 'code' => array( + 'description' => __( 'Coupon code.', 'woocommerce' ), + 'required' => true, + 'type' => 'string', + ), + ) + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + register_rest_route( + $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'type' => 'boolean', + 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + register_rest_route( + $this->namespace, '/' . $this->rest_base . '/batch', array( + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'batch_items' ), + 'permission_callback' => array( $this, 'batch_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_batch_schema' ), + ) + ); + } + + /** + * Get object. + * + * @since 3.0.0 + * @param int $id Object ID. + * @return WC_Data + */ + protected function get_object( $id ) { + return new WC_Coupon( $id ); + } + + /** + * Get formatted item data. + * + * @since 3.0.0 + * @param WC_Data $object WC_Data instance. + * @return array + */ + protected function get_formatted_item_data( $object ) { + $data = $object->get_data(); + + $format_decimal = array( 'amount', 'minimum_amount', 'maximum_amount' ); + $format_date = array( 'date_created', 'date_modified', 'date_expires' ); + $format_null = array( 'usage_limit', 'usage_limit_per_user', 'limit_usage_to_x_items' ); + + // Format decimal values. + foreach ( $format_decimal as $key ) { + $data[ $key ] = wc_format_decimal( $data[ $key ], 2 ); + } + + // Format date values. + foreach ( $format_date as $key ) { + $datetime = $data[ $key ]; + $data[ $key ] = wc_rest_prepare_date_response( $datetime, false ); + $data[ $key . '_gmt' ] = wc_rest_prepare_date_response( $datetime ); + } + + // Format null values. + foreach ( $format_null as $key ) { + $data[ $key ] = $data[ $key ] ? $data[ $key ] : null; + } + + return array( + 'id' => $object->get_id(), + 'code' => $data['code'], + 'amount' => $data['amount'], + 'date_created' => $data['date_created'], + 'date_created_gmt' => $data['date_created_gmt'], + 'date_modified' => $data['date_modified'], + 'date_modified_gmt' => $data['date_modified_gmt'], + 'discount_type' => $data['discount_type'], + 'description' => $data['description'], + 'date_expires' => $data['date_expires'], + 'date_expires_gmt' => $data['date_expires_gmt'], + 'usage_count' => $data['usage_count'], + 'individual_use' => $data['individual_use'], + 'product_ids' => $data['product_ids'], + 'excluded_product_ids' => $data['excluded_product_ids'], + 'usage_limit' => $data['usage_limit'], + 'usage_limit_per_user' => $data['usage_limit_per_user'], + 'limit_usage_to_x_items' => $data['limit_usage_to_x_items'], + 'free_shipping' => $data['free_shipping'], + 'product_categories' => $data['product_categories'], + 'excluded_product_categories' => $data['excluded_product_categories'], + 'exclude_sale_items' => $data['exclude_sale_items'], + 'minimum_amount' => $data['minimum_amount'], + 'maximum_amount' => $data['maximum_amount'], + 'email_restrictions' => $data['email_restrictions'], + 'used_by' => $data['used_by'], + 'meta_data' => $data['meta_data'], + ); + } + + /** + * Prepare a single coupon output for response. + * + * @since 3.0.0 + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response + */ + public function prepare_object_for_response( $object, $request ) { + $data = $this->get_formatted_item_data( $object ); + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + $response = rest_ensure_response( $data ); + $response->add_links( $this->prepare_links( $object, $request ) ); + + /** + * Filter the data for a response. + * + * The dynamic portion of the hook name, $this->post_type, + * refers to object type being prepared for the response. + * + * @param WP_REST_Response $response The response object. + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->post_type}_object", $response, $object, $request ); + } + + /** + * Prepare objects query. + * + * @since 3.0.0 + * @param WP_REST_Request $request Full details about the request. + * @return array + */ + protected function prepare_objects_query( $request ) { + $args = parent::prepare_objects_query( $request ); + + if ( ! empty( $request['code'] ) ) { + $id = wc_get_coupon_id_by_code( $request['code'] ); + $args['post__in'] = array( $id ); + } + + // Get only ids. + $args['fields'] = 'ids'; + + return $args; + } + + /** + * Only return writable props from schema. + * + * @param array $schema Schema. + * @return bool + */ + protected function filter_writable_props( $schema ) { + return empty( $schema['readonly'] ); + } + + /** + * Prepare a single coupon for create or update. + * + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + * @return WP_Error|WC_Data + */ + protected function prepare_object_for_database( $request, $creating = false ) { + $id = isset( $request['id'] ) ? absint( $request['id'] ) : 0; + $coupon = new WC_Coupon( $id ); + $schema = $this->get_item_schema(); + $data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) ); + + // Validate required POST fields. + if ( $creating && empty( $request['code'] ) ) { + return new WP_Error( 'woocommerce_rest_empty_coupon_code', sprintf( __( 'The coupon code cannot be empty.', 'woocommerce' ), 'code' ), array( 'status' => 400 ) ); + } + + // Handle all writable props. + foreach ( $data_keys as $key ) { + $value = $request[ $key ]; + + if ( ! is_null( $value ) ) { + switch ( $key ) { + case 'code': + $coupon_code = wc_format_coupon_code( $value ); + $id = $coupon->get_id() ? $coupon->get_id() : 0; + $id_from_code = wc_get_coupon_id_by_code( $coupon_code, $id ); + + if ( $id_from_code ) { + return new WP_Error( 'woocommerce_rest_coupon_code_already_exists', __( 'The coupon code already exists', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $coupon->set_code( $coupon_code ); + break; + case 'meta_data': + if ( is_array( $value ) ) { + foreach ( $value as $meta ) { + $coupon->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); + } + } + break; + case 'description': + $coupon->set_description( wp_filter_post_kses( $value ) ); + break; + default: + if ( is_callable( array( $coupon, "set_{$key}" ) ) ) { + $coupon->{"set_{$key}"}( $value ); + } + break; + } + } + } + + /** + * Filters an object before it is inserted via the REST API. + * + * The dynamic portion of the hook name, `$this->post_type`, + * refers to the object type slug. + * + * @param WC_Data $coupon Object object. + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + */ + return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $coupon, $request, $creating ); + } + + /** + * Get the Coupon's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => $this->post_type, + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the object.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'code' => array( + 'description' => __( 'Coupon code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'amount' => array( + 'description' => __( 'The amount of discount. Should always be numeric, even if setting a percentage.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'date_created' => array( + 'description' => __( "The date the coupon was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created_gmt' => array( + 'description' => __( 'The date the coupon was created, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the coupon was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified_gmt' => array( + 'description' => __( 'The date the coupon was last modified, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'discount_type' => array( + 'description' => __( 'Determines the type of discount that will be applied.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'fixed_cart', + 'enum' => array_keys( wc_get_coupon_types() ), + 'context' => array( 'view', 'edit' ), + ), + 'description' => array( + 'description' => __( 'Coupon description.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'date_expires' => array( + 'description' => __( "The date the coupon expires, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'date_expires_gmt' => array( + 'description' => __( 'The date the coupon expires, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'usage_count' => array( + 'description' => __( 'Number of times the coupon has been used already.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'individual_use' => array( + 'description' => __( 'If true, the coupon can only be used individually. Other applied coupons will be removed from the cart.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'product_ids' => array( + 'description' => __( 'List of product IDs the coupon can be used on.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + ), + 'excluded_product_ids' => array( + 'description' => __( 'List of product IDs the coupon cannot be used on.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + ), + 'usage_limit' => array( + 'description' => __( 'How many times the coupon can be used in total.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'usage_limit_per_user' => array( + 'description' => __( 'How many times the coupon can be used per customer.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'limit_usage_to_x_items' => array( + 'description' => __( 'Max number of items in the cart the coupon can be applied to.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'free_shipping' => array( + 'description' => __( 'If true and if the free shipping method requires a coupon, this coupon will enable free shipping.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'product_categories' => array( + 'description' => __( 'List of category IDs the coupon applies to.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + ), + 'excluded_product_categories' => array( + 'description' => __( 'List of category IDs the coupon does not apply to.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + ), + 'exclude_sale_items' => array( + 'description' => __( 'If true, this coupon will not be applied to items that have sale prices.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'minimum_amount' => array( + 'description' => __( 'Minimum order amount that needs to be in the cart before coupon applies.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'maximum_amount' => array( + 'description' => __( 'Maximum order amount allowed when using the coupon.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'email_restrictions' => array( + 'description' => __( 'List of email addresses that can use this coupon.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + 'context' => array( 'view', 'edit' ), + ), + 'used_by' => array( + 'description' => __( 'List of user IDs (or guest email addresses) that have used the coupon.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'meta_data' => array( + 'description' => __( 'Meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Meta ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => array( 'string', 'null' ), + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + ), + ); + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections of attachments. + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + $params['code'] = array( + 'description' => __( 'Limit result set to resources with a specific code.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } +} diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-customer-downloads-v2-controller.php b/includes/rest-api/Controllers/Version2/class-wc-rest-customer-downloads-v2-controller.php new file mode 100644 index 00000000000..e403442f484 --- /dev/null +++ b/includes/rest-api/Controllers/Version2/class-wc-rest-customer-downloads-v2-controller.php @@ -0,0 +1,165 @@ +/downloads endpoint. + * + * @package Automattic/WooCommerce/RestApi + * @since 2.6.0 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * REST API Customers controller class. + * + * @package Automattic/WooCommerce/RestApi + * @extends WC_REST_Customer_Downloads_V1_Controller + */ +class WC_REST_Customer_Downloads_V2_Controller extends WC_REST_Customer_Downloads_V1_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + protected $namespace = 'wc/v2'; + + /** + * Prepare a single download output for response. + * + * @param stdClass $download Download object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $download, $request ) { + $data = array( + 'download_id' => $download->download_id, + 'download_url' => $download->download_url, + 'product_id' => $download->product_id, + 'product_name' => $download->product_name, + 'download_name' => $download->download_name, + 'order_id' => $download->order_id, + 'order_key' => $download->order_key, + 'downloads_remaining' => '' === $download->downloads_remaining ? 'unlimited' : $download->downloads_remaining, + 'access_expires' => $download->access_expires ? wc_rest_prepare_date_response( $download->access_expires ) : 'never', + 'access_expires_gmt' => $download->access_expires ? wc_rest_prepare_date_response( get_gmt_from_date( $download->access_expires ) ) : 'never', + 'file' => $download->file, + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $download, $request ) ); + + /** + * Filter customer download data returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param stdClass $download Download object used to create response. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'woocommerce_rest_prepare_customer_download', $response, $download, $request ); + } + + /** + * Get the Customer Download's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'customer_download', + 'type' => 'object', + 'properties' => array( + 'download_id' => array( + 'description' => __( 'Download ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'download_url' => array( + 'description' => __( 'Download file URL.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'product_id' => array( + 'description' => __( 'Downloadable product ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'product_name' => array( + 'description' => __( 'Product name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'download_name' => array( + 'description' => __( 'Downloadable file name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'order_id' => array( + 'description' => __( 'Order ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'order_key' => array( + 'description' => __( 'Order key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'downloads_remaining' => array( + 'description' => __( 'Number of downloads remaining.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'access_expires' => array( + 'description' => __( "The date when download access expires, in the site's timezone.", 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'access_expires_gmt' => array( + 'description' => __( 'The date when download access expires, as GMT.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'file' => array( + 'description' => __( 'File details.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view' ), + 'readonly' => true, + 'properties' => array( + 'name' => array( + 'description' => __( 'File name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'file' => array( + 'description' => __( 'File URL.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-customers-v2-controller.php b/includes/rest-api/Controllers/Version2/class-wc-rest-customers-v2-controller.php new file mode 100644 index 00000000000..50fff8f641a --- /dev/null +++ b/includes/rest-api/Controllers/Version2/class-wc-rest-customers-v2-controller.php @@ -0,0 +1,364 @@ +get_data(); + $format_date = array( 'date_created', 'date_modified' ); + + // Format date values. + foreach ( $format_date as $key ) { + $datetime = 'date_created' === $key ? get_date_from_gmt( gmdate( 'Y-m-d H:i:s', $data[ $key ]->getTimestamp() ) ) : $data[ $key ]; + $data[ $key ] = wc_rest_prepare_date_response( $datetime, false ); + $data[ $key . '_gmt' ] = wc_rest_prepare_date_response( $datetime ); + } + + return array( + 'id' => $object->get_id(), + 'date_created' => $data['date_created'], + 'date_created_gmt' => $data['date_created_gmt'], + 'date_modified' => $data['date_modified'], + 'date_modified_gmt' => $data['date_modified_gmt'], + 'email' => $data['email'], + 'first_name' => $data['first_name'], + 'last_name' => $data['last_name'], + 'role' => $data['role'], + 'username' => $data['username'], + 'billing' => $data['billing'], + 'shipping' => $data['shipping'], + 'is_paying_customer' => $data['is_paying_customer'], + 'orders_count' => $object->get_order_count(), + 'total_spent' => $object->get_total_spent(), + 'avatar_url' => $object->get_avatar_url(), + 'meta_data' => $data['meta_data'], + ); + } + + /** + * Prepare a single customer output for response. + * + * @param WP_User $user_data User object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $user_data, $request ) { + $customer = new WC_Customer( $user_data->ID ); + $data = $this->get_formatted_item_data( $customer ); + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + $response = rest_ensure_response( $data ); + $response->add_links( $this->prepare_links( $user_data ) ); + + /** + * Filter customer data returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param WP_User $user_data User object used to create response. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'woocommerce_rest_prepare_customer', $response, $user_data, $request ); + } + + /** + * Update customer meta fields. + * + * @param WC_Customer $customer Customer data. + * @param WP_REST_Request $request Request data. + */ + protected function update_customer_meta_fields( $customer, $request ) { + parent::update_customer_meta_fields( $customer, $request ); + + // Meta data. + if ( isset( $request['meta_data'] ) ) { + if ( is_array( $request['meta_data'] ) ) { + foreach ( $request['meta_data'] as $meta ) { + $customer->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); + } + } + } + } + + /** + * Get the Customer's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'customer', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( "The date the customer was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created_gmt' => array( + 'description' => __( 'The date the customer was created, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the customer was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified_gmt' => array( + 'description' => __( 'The date the customer was last modified, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'email' => array( + 'description' => __( 'The email address for the customer.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'email', + 'context' => array( 'view', 'edit' ), + ), + 'first_name' => array( + 'description' => __( 'Customer first name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'last_name' => array( + 'description' => __( 'Customer last name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'role' => array( + 'description' => __( 'Customer role.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'username' => array( + 'description' => __( 'Customer login name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_user', + ), + ), + 'password' => array( + 'description' => __( 'Customer password.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'edit' ), + ), + 'billing' => array( + 'description' => __( 'List of billing address data.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'first_name' => array( + 'description' => __( 'First name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'last_name' => array( + 'description' => __( 'Last name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'company' => array( + 'description' => __( 'Company name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_1' => array( + 'description' => __( 'Address line 1', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_2' => array( + 'description' => __( 'Address line 2', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'city' => array( + 'description' => __( 'City name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'state' => array( + 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'postcode' => array( + 'description' => __( 'Postal code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'country' => array( + 'description' => __( 'ISO code of the country.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'email' => array( + 'description' => __( 'Email address.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'email', + 'context' => array( 'view', 'edit' ), + ), + 'phone' => array( + 'description' => __( 'Phone number.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'shipping' => array( + 'description' => __( 'List of shipping address data.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'first_name' => array( + 'description' => __( 'First name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'last_name' => array( + 'description' => __( 'Last name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'company' => array( + 'description' => __( 'Company name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_1' => array( + 'description' => __( 'Address line 1', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_2' => array( + 'description' => __( 'Address line 2', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'city' => array( + 'description' => __( 'City name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'state' => array( + 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'postcode' => array( + 'description' => __( 'Postal code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'country' => array( + 'description' => __( 'ISO code of the country.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'is_paying_customer' => array( + 'description' => __( 'Is the customer a paying customer?', 'woocommerce' ), + 'type' => 'bool', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'orders_count' => array( + 'description' => __( 'Quantity of orders made by the customer.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total_spent' => array( + 'description' => __( 'Total amount spent.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'avatar_url' => array( + 'description' => __( 'Avatar URL.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'meta_data' => array( + 'description' => __( 'Meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Meta ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => array( 'string', 'null' ), + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-network-orders-v2-controller.php b/includes/rest-api/Controllers/Version2/class-wc-rest-network-orders-v2-controller.php new file mode 100644 index 00000000000..45d0f8b483d --- /dev/null +++ b/includes/rest-api/Controllers/Version2/class-wc-rest-network-orders-v2-controller.php @@ -0,0 +1,174 @@ +namespace, + '/' . $this->rest_base . '/network', + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'network_orders' ), + 'permission_callback' => array( $this, 'network_orders_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + } + + /** + * Retrieves the item's schema for display / public consumption purposes. + * + * @return array Public item schema data. + */ + public function get_public_item_schema() { + $schema = parent::get_public_item_schema(); + + $schema['properties']['blog'] = array( + 'description' => __( 'Blog id of the record on the multisite.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view' ), + 'readonly' => true, + ); + $schema['properties']['edit_url'] = array( + 'description' => __( 'URL to edit the order', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ); + $schema['properties']['customer'][] = array( + 'description' => __( 'Name of the customer for the order', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ); + $schema['properties']['status_name'][] = array( + 'description' => __( 'Order Status', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ); + $schema['properties']['formatted_total'][] = array( + 'description' => __( 'Order total formatted for locale', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ); + + return $schema; + } + + /** + * Does a permissions check for the proper requested blog + * + * @param WP_REST_Request $request Full details about the request. + * + * @return bool $permission + */ + public function network_orders_permissions_check( $request ) { + $blog_id = $request->get_param( 'blog_id' ); + $blog_id = ! empty( $blog_id ) ? $blog_id : get_current_blog_id(); + + switch_to_blog( $blog_id ); + + $permission = $this->get_items_permissions_check( $request ); + + restore_current_blog(); + + return $permission; + } + + /** + * Get a collection of orders from the requested blog id + * + * @param WP_REST_Request $request Full details about the request. + * + * @return WP_REST_Response + */ + public function network_orders( $request ) { + $blog_id = $request->get_param( 'blog_id' ); + $blog_id = ! empty( $blog_id ) ? $blog_id : get_current_blog_id(); + $active_plugins = get_blog_option( $blog_id, 'active_plugins', array() ); + $network_active_plugins = array_keys( get_site_option( 'active_sitewide_plugins', array() ) ); + + $plugins = array_merge( $active_plugins, $network_active_plugins ); + $wc_active = false; + foreach ( $plugins as $plugin ) { + if ( substr_compare( $plugin, '/woocommerce.php', strlen( $plugin ) - strlen( '/woocommerce.php' ), strlen( '/woocommerce.php' ) ) === 0 ) { + $wc_active = true; + } + } + + // If WooCommerce not active for site, return an empty response. + if ( ! $wc_active ) { + $response = rest_ensure_response( array() ); + return $response; + } + + switch_to_blog( $blog_id ); + add_filter( 'woocommerce_rest_orders_prepare_object_query', array( $this, 'network_orders_filter_args' ) ); + $items = $this->get_items( $request ); + remove_filter( 'woocommerce_rest_orders_prepare_object_query', array( $this, 'network_orders_filter_args' ) ); + + foreach ( $items->data as &$current_order ) { + $order = wc_get_order( $current_order['id'] ); + + $current_order['blog'] = get_blog_details( get_current_blog_id() ); + $current_order['edit_url'] = get_admin_url( $blog_id, 'post.php?post=' . absint( $order->get_id() ) . '&action=edit' ); + /* translators: 1: first name 2: last name */ + $current_order['customer'] = trim( sprintf( _x( '%1$s %2$s', 'full name', 'woocommerce' ), $order->get_billing_first_name(), $order->get_billing_last_name() ) ); + $current_order['status_name'] = wc_get_order_status_name( $order->get_status() ); + $current_order['formatted_total'] = $order->get_formatted_order_total(); + } + + restore_current_blog(); + + return $items; + } + + /** + * Filters the post statuses to on hold and processing for the network order query. + * + * @param array $args Query args. + * + * @return array + */ + public function network_orders_filter_args( $args ) { + $args['post_status'] = array( + 'wc-on-hold', + 'wc-processing', + ); + + return $args; + } +} diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-order-notes-v2-controller.php b/includes/rest-api/Controllers/Version2/class-wc-rest-order-notes-v2-controller.php new file mode 100644 index 00000000000..f3d269c963f --- /dev/null +++ b/includes/rest-api/Controllers/Version2/class-wc-rest-order-notes-v2-controller.php @@ -0,0 +1,182 @@ +/notes endpoint. + * + * @package Automattic/WooCommerce/RestApi + * @since 2.6.0 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * REST API Order Notes controller class. + * + * @package Automattic/WooCommerce/RestApi + * @extends WC_REST_Order_Notes_V1_Controller + */ +class WC_REST_Order_Notes_V2_Controller extends WC_REST_Order_Notes_V1_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + protected $namespace = 'wc/v2'; + + /** + * Get order notes from an order. + * + * @param WP_REST_Request $request Request data. + * + * @return array|WP_Error + */ + public function get_items( $request ) { + $order = wc_get_order( (int) $request['order_id'] ); + + if ( ! $order || $this->post_type !== $order->get_type() ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid order ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $args = array( + 'post_id' => $order->get_id(), + 'approve' => 'approve', + 'type' => 'order_note', + ); + + // Allow filter by order note type. + if ( 'customer' === $request['type'] ) { + $args['meta_query'] = array( // WPCS: slow query ok. + array( + 'key' => 'is_customer_note', + 'value' => 1, + 'compare' => '=', + ), + ); + } elseif ( 'internal' === $request['type'] ) { + $args['meta_query'] = array( // WPCS: slow query ok. + array( + 'key' => 'is_customer_note', + 'compare' => 'NOT EXISTS', + ), + ); + } + + remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 ); + + $notes = get_comments( $args ); + + add_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ), 10, 1 ); + + $data = array(); + foreach ( $notes as $note ) { + $order_note = $this->prepare_item_for_response( $note, $request ); + $order_note = $this->prepare_response_for_collection( $order_note ); + $data[] = $order_note; + } + + return rest_ensure_response( $data ); + } + + /** + * Prepare a single order note output for response. + * + * @param WP_Comment $note Order note object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $note, $request ) { + $data = array( + 'id' => (int) $note->comment_ID, + 'date_created' => wc_rest_prepare_date_response( $note->comment_date ), + 'date_created_gmt' => wc_rest_prepare_date_response( $note->comment_date_gmt ), + 'note' => $note->comment_content, + 'customer_note' => (bool) get_comment_meta( $note->comment_ID, 'is_customer_note', true ), + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $note ) ); + + /** + * Filter order note object returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param WP_Comment $note Order note object used to create response. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'woocommerce_rest_prepare_order_note', $response, $note, $request ); + } + + /** + * Get the Order Notes schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'order_note', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( "The date the order note was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created_gmt' => array( + 'description' => __( 'The date the order note was created, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'note' => array( + 'description' => __( 'Order note content.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'customer_note' => array( + 'description' => __( 'If true, the note will be shown to customers and they will be notified. If false, the note will be for admin reference only.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + $params = array(); + $params['context'] = $this->get_context_param( array( 'default' => 'view' ) ); + $params['type'] = array( + 'default' => 'any', + 'description' => __( 'Limit result to customers or internal notes.', 'woocommerce' ), + 'type' => 'string', + 'enum' => array( 'any', 'customer', 'internal' ), + 'sanitize_callback' => 'sanitize_key', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } +} diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-order-refunds-v2-controller.php b/includes/rest-api/Controllers/Version2/class-wc-rest-order-refunds-v2-controller.php new file mode 100644 index 00000000000..dd7f2d3d3f6 --- /dev/null +++ b/includes/rest-api/Controllers/Version2/class-wc-rest-order-refunds-v2-controller.php @@ -0,0 +1,584 @@ +/refunds endpoint. + * + * @package Automattic/WooCommerce/RestApi + * @since 2.6.0 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * REST API Order Refunds controller class. + * + * @package Automattic/WooCommerce/RestApi + * @extends WC_REST_Orders_V2_Controller + */ +class WC_REST_Order_Refunds_V2_Controller extends WC_REST_Orders_V2_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + protected $namespace = 'wc/v2'; + + /** + * Route base. + * + * @var string + */ + protected $rest_base = 'orders/(?P[\d]+)/refunds'; + + /** + * Post type. + * + * @var string + */ + protected $post_type = 'shop_order_refund'; + + /** + * Stores the request. + * + * @var array + */ + protected $request = array(); + + /** + * Order refunds actions. + */ + public function __construct() { + add_filter( "woocommerce_rest_{$this->post_type}_object_trashable", '__return_false' ); + } + + /** + * Register the routes for order refunds. + */ + public function register_routes() { + register_rest_route( + $this->namespace, '/' . $this->rest_base, array( + 'args' => array( + 'order_id' => array( + 'description' => __( 'The order ID.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + register_rest_route( + $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + 'args' => array( + 'order_id' => array( + 'description' => __( 'The order ID.', 'woocommerce' ), + 'type' => 'integer', + ), + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => true, + 'type' => 'boolean', + 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Get object. + * + * @since 3.0.0 + * @param int $id Object ID. + * @return WC_Data + */ + protected function get_object( $id ) { + return wc_get_order( $id ); + } + + /** + * Get formatted item data. + * + * @since 3.0.0 + * @param WC_Data $object WC_Data instance. + * @return array + */ + protected function get_formatted_item_data( $object ) { + $data = $object->get_data(); + $format_decimal = array( 'amount' ); + $format_date = array( 'date_created' ); + $format_line_items = array( 'line_items' ); + + // Format decimal values. + foreach ( $format_decimal as $key ) { + $data[ $key ] = wc_format_decimal( $data[ $key ], $this->request['dp'] ); + } + + // Format date values. + foreach ( $format_date as $key ) { + $datetime = $data[ $key ]; + $data[ $key ] = wc_rest_prepare_date_response( $datetime, false ); + $data[ $key . '_gmt' ] = wc_rest_prepare_date_response( $datetime ); + } + + // Format line items. + foreach ( $format_line_items as $key ) { + $data[ $key ] = array_values( array_map( array( $this, 'get_order_item_data' ), $data[ $key ] ) ); + } + + return array( + 'id' => $object->get_id(), + 'date_created' => $data['date_created'], + 'date_created_gmt' => $data['date_created_gmt'], + 'amount' => $data['amount'], + 'reason' => $data['reason'], + 'refunded_by' => $data['refunded_by'], + 'refunded_payment' => $data['refunded_payment'], + 'meta_data' => $data['meta_data'], + 'line_items' => $data['line_items'], + ); + } + + /** + * Prepare a single order output for response. + * + * @since 3.0.0 + * + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + * + * @return WP_Error|WP_REST_Response + */ + public function prepare_object_for_response( $object, $request ) { + $this->request = $request; + $this->request['dp'] = is_null( $this->request['dp'] ) ? wc_get_price_decimals() : absint( $this->request['dp'] ); + $order = wc_get_order( (int) $request['order_id'] ); + + if ( ! $order ) { + return new WP_Error( 'woocommerce_rest_invalid_order_id', __( 'Invalid order ID.', 'woocommerce' ), 404 ); + } + + if ( ! $object || $object->get_parent_id() !== $order->get_id() ) { + return new WP_Error( 'woocommerce_rest_invalid_order_refund_id', __( 'Invalid order refund ID.', 'woocommerce' ), 404 ); + } + + $data = $this->get_formatted_item_data( $object ); + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $object, $request ) ); + + /** + * Filter the data for a response. + * + * The dynamic portion of the hook name, $this->post_type, + * refers to object type being prepared for the response. + * + * @param WP_REST_Response $response The response object. + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->post_type}_object", $response, $object, $request ); + } + + /** + * Prepare links for the request. + * + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + * @return array Links for the given post. + */ + protected function prepare_links( $object, $request ) { + $base = str_replace( '(?P[\d]+)', $object->get_parent_id(), $this->rest_base ); + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $object->get_id() ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ), + ), + 'up' => array( + 'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $object->get_parent_id() ) ), + ), + ); + + return $links; + } + + /** + * Prepare objects query. + * + * @since 3.0.0 + * @param WP_REST_Request $request Full details about the request. + * @return array + */ + protected function prepare_objects_query( $request ) { + $args = parent::prepare_objects_query( $request ); + + $args['post_status'] = array_keys( wc_get_order_statuses() ); + $args['post_parent__in'] = array( absint( $request['order_id'] ) ); + + return $args; + } + + /** + * Prepares one object for create or update operation. + * + * @since 3.0.0 + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + * @return WP_Error|WC_Data The prepared item, or WP_Error object on failure. + */ + protected function prepare_object_for_database( $request, $creating = false ) { + $order = wc_get_order( (int) $request['order_id'] ); + + if ( ! $order ) { + return new WP_Error( 'woocommerce_rest_invalid_order_id', __( 'Invalid order ID.', 'woocommerce' ), 404 ); + } + + if ( 0 > $request['amount'] ) { + return new WP_Error( 'woocommerce_rest_invalid_order_refund', __( 'Refund amount must be greater than zero.', 'woocommerce' ), 400 ); + } + + // Create the refund. + $refund = wc_create_refund( + array( + 'order_id' => $order->get_id(), + 'amount' => $request['amount'], + 'reason' => empty( $request['reason'] ) ? null : $request['reason'], + 'refund_payment' => is_bool( $request['api_refund'] ) ? $request['api_refund'] : true, + 'restock_items' => true, + ) + ); + + if ( is_wp_error( $refund ) ) { + return new WP_Error( 'woocommerce_rest_cannot_create_order_refund', $refund->get_error_message(), 500 ); + } + + if ( ! $refund ) { + return new WP_Error( 'woocommerce_rest_cannot_create_order_refund', __( 'Cannot create order refund, please try again.', 'woocommerce' ), 500 ); + } + + if ( ! empty( $request['meta_data'] ) && is_array( $request['meta_data'] ) ) { + foreach ( $request['meta_data'] as $meta ) { + $refund->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); + } + $refund->save_meta_data(); + } + + /** + * Filters an object before it is inserted via the REST API. + * + * The dynamic portion of the hook name, `$this->post_type`, + * refers to the object type slug. + * + * @param WC_Data $coupon Object object. + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + */ + return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $refund, $request, $creating ); + } + + /** + * Save an object data. + * + * @since 3.0.0 + * @param WP_REST_Request $request Full details about the request. + * @param bool $creating If is creating a new object. + * @return WC_Data|WP_Error + */ + protected function save_object( $request, $creating = false ) { + try { + $object = $this->prepare_object_for_database( $request, $creating ); + + if ( is_wp_error( $object ) ) { + return $object; + } + + return $this->get_object( $object->get_id() ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); + } catch ( WC_REST_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Get the Order's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => $this->post_type, + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( "The date the order refund was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created_gmt' => array( + 'description' => __( 'The date the order refund was created, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'amount' => array( + 'description' => __( 'Refund amount.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'reason' => array( + 'description' => __( 'Reason for refund.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'refunded_by' => array( + 'description' => __( 'User ID of user who created the refund.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'refunded_payment' => array( + 'description' => __( 'If the payment was refunded via the API.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'meta_data' => array( + 'description' => __( 'Meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Meta ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => array( 'string', 'null' ), + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'line_items' => array( + 'description' => __( 'Line items data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Product name.', 'woocommerce' ), + 'type' => array( 'string', 'null' ), + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'product_id' => array( + 'description' => __( 'Product ID.', 'woocommerce' ), + 'type' => array( 'integer', 'null' ), + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'variation_id' => array( + 'description' => __( 'Variation ID, if applicable.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'quantity' => array( + 'description' => __( 'Quantity ordered.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'tax_class' => array( + 'description' => __( 'Tax class of product.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'subtotal' => array( + 'description' => __( 'Line subtotal (before discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'subtotal_tax' => array( + 'description' => __( 'Line subtotal tax (before discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Line total (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total_tax' => array( + 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'taxes' => array( + 'description' => __( 'Line taxes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Tax rate ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Tax total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'subtotal' => array( + 'description' => __( 'Tax subtotal.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + 'meta_data' => array( + 'description' => __( 'Meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Meta ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => array( 'string', 'null' ), + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + 'sku' => array( + 'description' => __( 'Product SKU.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'price' => array( + 'description' => __( 'Product price.', 'woocommerce' ), + 'type' => 'number', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + 'api_refund' => array( + 'description' => __( 'When true, the payment gateway API is used to generate the refund.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'edit' ), + 'default' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + unset( $params['status'], $params['customer'], $params['product'] ); + + return $params; + } +} diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php b/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php new file mode 100644 index 00000000000..75b366627de --- /dev/null +++ b/includes/rest-api/Controllers/Version2/class-wc-rest-orders-v2-controller.php @@ -0,0 +1,1711 @@ +namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P[\d]+)', + array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'type' => 'boolean', + 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/batch', + array( + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'batch_items' ), + 'permission_callback' => array( $this, 'batch_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_batch_schema' ), + ) + ); + } + + /** + * Get object. Return false if object is not of required type. + * + * @since 3.0.0 + * @param int $id Object ID. + * @return WC_Data|bool + */ + protected function get_object( $id ) { + $order = wc_get_order( $id ); + // In case id is a refund's id (or it's not an order at all), don't expose it via /orders/ path. + if ( ! $order || 'shop_order_refund' === $order->get_type() ) { + return false; + } + + return $order; + } + + /** + * Expands an order item to get its data. + * + * @param WC_Order_item $item Order item data. + * @return array + */ + protected function get_order_item_data( $item ) { + $data = $item->get_data(); + $format_decimal = array( 'subtotal', 'subtotal_tax', 'total', 'total_tax', 'tax_total', 'shipping_tax_total' ); + + // Format decimal values. + foreach ( $format_decimal as $key ) { + if ( isset( $data[ $key ] ) ) { + $data[ $key ] = wc_format_decimal( $data[ $key ], $this->request['dp'] ); + } + } + + // Add SKU and PRICE to products. + if ( is_callable( array( $item, 'get_product' ) ) ) { + $data['sku'] = $item->get_product() ? $item->get_product()->get_sku() : null; + $data['price'] = $item->get_quantity() ? $item->get_total() / $item->get_quantity() : 0; + } + + // Format taxes. + if ( ! empty( $data['taxes']['total'] ) ) { + $taxes = array(); + + foreach ( $data['taxes']['total'] as $tax_rate_id => $tax ) { + $taxes[] = array( + 'id' => $tax_rate_id, + 'total' => $tax, + 'subtotal' => isset( $data['taxes']['subtotal'][ $tax_rate_id ] ) ? $data['taxes']['subtotal'][ $tax_rate_id ] : '', + ); + } + $data['taxes'] = $taxes; + } elseif ( isset( $data['taxes'] ) ) { + $data['taxes'] = array(); + } + + // Remove names for coupons, taxes and shipping. + if ( isset( $data['code'] ) || isset( $data['rate_code'] ) || isset( $data['method_title'] ) ) { + unset( $data['name'] ); + } + + // Remove props we don't want to expose. + unset( $data['order_id'] ); + unset( $data['type'] ); + + return $data; + } + + /** + * Get formatted item data. + * + * @since 3.0.0 + * @param WC_Data $object WC_Data instance. + * @return array + */ + protected function get_formatted_item_data( $object ) { + $data = $object->get_data(); + $format_decimal = array( 'discount_total', 'discount_tax', 'shipping_total', 'shipping_tax', 'shipping_total', 'shipping_tax', 'cart_tax', 'total', 'total_tax' ); + $format_date = array( 'date_created', 'date_modified', 'date_completed', 'date_paid' ); + $format_line_items = array( 'line_items', 'tax_lines', 'shipping_lines', 'fee_lines', 'coupon_lines' ); + + // Format decimal values. + foreach ( $format_decimal as $key ) { + $data[ $key ] = wc_format_decimal( $data[ $key ], $this->request['dp'] ); + } + + // Format date values. + foreach ( $format_date as $key ) { + $datetime = $data[ $key ]; + $data[ $key ] = wc_rest_prepare_date_response( $datetime, false ); + $data[ $key . '_gmt' ] = wc_rest_prepare_date_response( $datetime ); + } + + // Format the order status. + $data['status'] = 'wc-' === substr( $data['status'], 0, 3 ) ? substr( $data['status'], 3 ) : $data['status']; + + // Format line items. + foreach ( $format_line_items as $key ) { + $data[ $key ] = array_values( array_map( array( $this, 'get_order_item_data' ), $data[ $key ] ) ); + } + + // Refunds. + $data['refunds'] = array(); + foreach ( $object->get_refunds() as $refund ) { + $data['refunds'][] = array( + 'id' => $refund->get_id(), + 'reason' => $refund->get_reason() ? $refund->get_reason() : '', + 'total' => '-' . wc_format_decimal( $refund->get_amount(), $this->request['dp'] ), + ); + } + + return array( + 'id' => $object->get_id(), + 'parent_id' => $data['parent_id'], + 'number' => $data['number'], + 'order_key' => $data['order_key'], + 'created_via' => $data['created_via'], + 'version' => $data['version'], + 'status' => $data['status'], + 'currency' => $data['currency'], + 'date_created' => $data['date_created'], + 'date_created_gmt' => $data['date_created_gmt'], + 'date_modified' => $data['date_modified'], + 'date_modified_gmt' => $data['date_modified_gmt'], + 'discount_total' => $data['discount_total'], + 'discount_tax' => $data['discount_tax'], + 'shipping_total' => $data['shipping_total'], + 'shipping_tax' => $data['shipping_tax'], + 'cart_tax' => $data['cart_tax'], + 'total' => $data['total'], + 'total_tax' => $data['total_tax'], + 'prices_include_tax' => $data['prices_include_tax'], + 'customer_id' => $data['customer_id'], + 'customer_ip_address' => $data['customer_ip_address'], + 'customer_user_agent' => $data['customer_user_agent'], + 'customer_note' => $data['customer_note'], + 'billing' => $data['billing'], + 'shipping' => $data['shipping'], + 'payment_method' => $data['payment_method'], + 'payment_method_title' => $data['payment_method_title'], + 'transaction_id' => $data['transaction_id'], + 'date_paid' => $data['date_paid'], + 'date_paid_gmt' => $data['date_paid_gmt'], + 'date_completed' => $data['date_completed'], + 'date_completed_gmt' => $data['date_completed_gmt'], + 'cart_hash' => $data['cart_hash'], + 'meta_data' => $data['meta_data'], + 'line_items' => $data['line_items'], + 'tax_lines' => $data['tax_lines'], + 'shipping_lines' => $data['shipping_lines'], + 'fee_lines' => $data['fee_lines'], + 'coupon_lines' => $data['coupon_lines'], + 'refunds' => $data['refunds'], + ); + } + + /** + * Prepare a single order output for response. + * + * @since 3.0.0 + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response + */ + public function prepare_object_for_response( $object, $request ) { + $this->request = $request; + $this->request['dp'] = is_null( $this->request['dp'] ) ? wc_get_price_decimals() : absint( $this->request['dp'] ); + $data = $this->get_formatted_item_data( $object ); + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + $response = rest_ensure_response( $data ); + $response->add_links( $this->prepare_links( $object, $request ) ); + + /** + * Filter the data for a response. + * + * The dynamic portion of the hook name, $this->post_type, + * refers to object type being prepared for the response. + * + * @param WP_REST_Response $response The response object. + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->post_type}_object", $response, $object, $request ); + } + + /** + * Prepare links for the request. + * + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + * @return array Links for the given post. + */ + protected function prepare_links( $object, $request ) { + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $object->get_id() ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + + if ( 0 !== (int) $object->get_customer_id() ) { + $links['customer'] = array( + 'href' => rest_url( sprintf( '/%s/customers/%d', $this->namespace, $object->get_customer_id() ) ), + ); + } + + if ( 0 !== (int) $object->get_parent_id() ) { + $links['up'] = array( + 'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $object->get_parent_id() ) ), + ); + } + + return $links; + } + + /** + * Prepare objects query. + * + * @since 3.0.0 + * @param WP_REST_Request $request Full details about the request. + * @return array + */ + protected function prepare_objects_query( $request ) { + global $wpdb; + + $args = parent::prepare_objects_query( $request ); + + // Set post_status. + if ( in_array( $request['status'], $this->get_order_statuses(), true ) ) { + $args['post_status'] = 'wc-' . $request['status']; + } elseif ( 'any' === $request['status'] ) { + $args['post_status'] = 'any'; + } else { + $args['post_status'] = $request['status']; + } + + if ( isset( $request['customer'] ) ) { + if ( ! empty( $args['meta_query'] ) ) { + $args['meta_query'] = array(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + } + + $args['meta_query'][] = array( + 'key' => '_customer_user', + 'value' => $request['customer'], + 'type' => 'NUMERIC', + ); + } + + // Search by product. + if ( ! empty( $request['product'] ) ) { + $order_ids = $wpdb->get_col( + $wpdb->prepare( + "SELECT order_id + FROM {$wpdb->prefix}woocommerce_order_items + WHERE order_item_id IN ( SELECT order_item_id FROM {$wpdb->prefix}woocommerce_order_itemmeta WHERE meta_key = '_product_id' AND meta_value = %d ) + AND order_item_type = 'line_item'", + $request['product'] + ) + ); + + // Force WP_Query return empty if don't found any order. + $order_ids = ! empty( $order_ids ) ? $order_ids : array( 0 ); + + $args['post__in'] = $order_ids; + } + + // Search. + if ( ! empty( $args['s'] ) ) { + $order_ids = wc_order_search( $args['s'] ); + + if ( ! empty( $order_ids ) ) { + unset( $args['s'] ); + $args['post__in'] = array_merge( $order_ids, array( 0 ) ); + } + } + + /** + * Filter the query arguments for a request. + * + * Enables adding extra arguments or setting defaults for an order collection request. + * + * @param array $args Key value array of query var to query value. + * @param WP_REST_Request $request The request used. + */ + $args = apply_filters( 'woocommerce_rest_orders_prepare_object_query', $args, $request ); + + return $args; + } + + /** + * Only return writable props from schema. + * + * @param array $schema Schema. + * @return bool + */ + protected function filter_writable_props( $schema ) { + return empty( $schema['readonly'] ); + } + + /** + * Prepare a single order for create or update. + * + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + * @return WP_Error|WC_Data + */ + protected function prepare_object_for_database( $request, $creating = false ) { + $id = isset( $request['id'] ) ? absint( $request['id'] ) : 0; + $order = new WC_Order( $id ); + $schema = $this->get_item_schema(); + $data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) ); + + // Handle all writable props. + foreach ( $data_keys as $key ) { + $value = $request[ $key ]; + + if ( ! is_null( $value ) ) { + switch ( $key ) { + case 'status': + // Status change should be done later so transitions have new data. + break; + case 'billing': + case 'shipping': + $this->update_address( $order, $value, $key ); + break; + case 'line_items': + case 'shipping_lines': + case 'fee_lines': + case 'coupon_lines': + if ( is_array( $value ) ) { + foreach ( $value as $item ) { + if ( is_array( $item ) ) { + if ( $this->item_is_null( $item ) || ( isset( $item['quantity'] ) && 0 === $item['quantity'] ) ) { + $order->remove_item( $item['id'] ); + } else { + $this->set_item( $order, $key, $item ); + } + } + } + } + break; + case 'meta_data': + if ( is_array( $value ) ) { + foreach ( $value as $meta ) { + $order->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); + } + } + break; + default: + if ( is_callable( array( $order, "set_{$key}" ) ) ) { + $order->{"set_{$key}"}( $value ); + } + break; + } + } + } + + /** + * Filters an object before it is inserted via the REST API. + * + * The dynamic portion of the hook name, `$this->post_type`, + * refers to the object type slug. + * + * @param WC_Data $order Object object. + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + */ + return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $order, $request, $creating ); + } + + /** + * Save an object data. + * + * @since 3.0.0 + * @throws WC_REST_Exception But all errors are validated before returning any data. + * @param WP_REST_Request $request Full details about the request. + * @param bool $creating If is creating a new object. + * @return WC_Data|WP_Error + */ + protected function save_object( $request, $creating = false ) { + try { + $object = $this->prepare_object_for_database( $request, $creating ); + + if ( is_wp_error( $object ) ) { + return $object; + } + + // Make sure gateways are loaded so hooks from gateways fire on save/create. + WC()->payment_gateways(); + + if ( ! is_null( $request['customer_id'] ) && 0 !== $request['customer_id'] ) { + // Make sure customer exists. + if ( false === get_user_by( 'id', $request['customer_id'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_customer_id', __( 'Customer ID is invalid.', 'woocommerce' ), 400 ); + } + + // Make sure customer is part of blog. + if ( is_multisite() && ! is_user_member_of_blog( $request['customer_id'] ) ) { + add_user_to_blog( get_current_blog_id(), $request['customer_id'], 'customer' ); + } + } + + if ( $creating ) { + $object->set_created_via( 'rest-api' ); + $object->set_prices_include_tax( 'yes' === get_option( 'woocommerce_prices_include_tax' ) ); + $object->calculate_totals(); + } else { + // If items have changed, recalculate order totals. + if ( isset( $request['billing'] ) || isset( $request['shipping'] ) || isset( $request['line_items'] ) || isset( $request['shipping_lines'] ) || isset( $request['fee_lines'] ) || isset( $request['coupon_lines'] ) ) { + $object->calculate_totals( true ); + } + } + + // Set status. + if ( ! empty( $request['status'] ) ) { + $object->set_status( $request['status'] ); + } + + $object->save(); + + // Actions for after the order is saved. + if ( true === $request['set_paid'] ) { + if ( $creating || $object->needs_payment() ) { + $object->payment_complete( $request['transaction_id'] ); + } + } + + return $this->get_object( $object->get_id() ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); + } catch ( WC_REST_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Update address. + * + * @param WC_Order $order Order data. + * @param array $posted Posted data. + * @param string $type Address type. + */ + protected function update_address( $order, $posted, $type = 'billing' ) { + foreach ( $posted as $key => $value ) { + if ( is_callable( array( $order, "set_{$type}_{$key}" ) ) ) { + $order->{"set_{$type}_{$key}"}( $value ); + } + } + } + + /** + * Gets the product ID from the SKU or posted ID. + * + * @throws WC_REST_Exception When SKU or ID is not valid. + * @param array $posted Request data. + * @param string $action 'create' to add line item or 'update' to update it. + * @return int + */ + protected function get_product_id( $posted, $action = 'create' ) { + if ( ! empty( $posted['sku'] ) ) { + $product_id = (int) wc_get_product_id_by_sku( $posted['sku'] ); + } elseif ( ! empty( $posted['product_id'] ) && empty( $posted['variation_id'] ) ) { + $product_id = (int) $posted['product_id']; + } elseif ( ! empty( $posted['variation_id'] ) ) { + $product_id = (int) $posted['variation_id']; + } elseif ( 'update' === $action ) { + $product_id = 0; + } else { + throw new WC_REST_Exception( 'woocommerce_rest_required_product_reference', __( 'Product ID or SKU is required.', 'woocommerce' ), 400 ); + } + return $product_id; + } + + /** + * Maybe set an item prop if the value was posted. + * + * @param WC_Order_Item $item Order item. + * @param string $prop Order property. + * @param array $posted Request data. + */ + protected function maybe_set_item_prop( $item, $prop, $posted ) { + if ( isset( $posted[ $prop ] ) ) { + $item->{"set_$prop"}( $posted[ $prop ] ); + } + } + + /** + * Maybe set item props if the values were posted. + * + * @param WC_Order_Item $item Order item data. + * @param string[] $props Properties. + * @param array $posted Request data. + */ + protected function maybe_set_item_props( $item, $props, $posted ) { + foreach ( $props as $prop ) { + $this->maybe_set_item_prop( $item, $prop, $posted ); + } + } + + /** + * Maybe set item meta if posted. + * + * @param WC_Order_Item $item Order item data. + * @param array $posted Request data. + */ + protected function maybe_set_item_meta_data( $item, $posted ) { + if ( ! empty( $posted['meta_data'] ) && is_array( $posted['meta_data'] ) ) { + foreach ( $posted['meta_data'] as $meta ) { + if ( isset( $meta['key'] ) ) { + $value = isset( $meta['value'] ) ? $meta['value'] : null; + $item->update_meta_data( $meta['key'], $value, isset( $meta['id'] ) ? $meta['id'] : '' ); + } + } + } + } + + /** + * Create or update a line item. + * + * @param array $posted Line item data. + * @param string $action 'create' to add line item or 'update' to update it. + * @param object $item Passed when updating an item. Null during creation. + * @return WC_Order_Item_Product + * @throws WC_REST_Exception Invalid data, server error. + */ + protected function prepare_line_items( $posted, $action = 'create', $item = null ) { + $item = is_null( $item ) ? new WC_Order_Item_Product( ! empty( $posted['id'] ) ? $posted['id'] : '' ) : $item; + $product = wc_get_product( $this->get_product_id( $posted, $action ) ); + + if ( $product && $product !== $item->get_product() ) { + $item->set_product( $product ); + + if ( 'create' === $action ) { + $quantity = isset( $posted['quantity'] ) ? $posted['quantity'] : 1; + $total = wc_get_price_excluding_tax( $product, array( 'qty' => $quantity ) ); + $item->set_total( $total ); + $item->set_subtotal( $total ); + } + } + + $this->maybe_set_item_props( $item, array( 'name', 'quantity', 'total', 'subtotal', 'tax_class' ), $posted ); + $this->maybe_set_item_meta_data( $item, $posted ); + + return $item; + } + + /** + * Create or update an order shipping method. + * + * @param array $posted $shipping Item data. + * @param string $action 'create' to add shipping or 'update' to update it. + * @param object $item Passed when updating an item. Null during creation. + * @return WC_Order_Item_Shipping + * @throws WC_REST_Exception Invalid data, server error. + */ + protected function prepare_shipping_lines( $posted, $action = 'create', $item = null ) { + $item = is_null( $item ) ? new WC_Order_Item_Shipping( ! empty( $posted['id'] ) ? $posted['id'] : '' ) : $item; + + if ( 'create' === $action ) { + if ( empty( $posted['method_id'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_shipping_item', __( 'Shipping method ID is required.', 'woocommerce' ), 400 ); + } + } + + $this->maybe_set_item_props( $item, array( 'method_id', 'method_title', 'total', 'instance_id' ), $posted ); + $this->maybe_set_item_meta_data( $item, $posted ); + + return $item; + } + + /** + * Create or update an order fee. + * + * @param array $posted Item data. + * @param string $action 'create' to add fee or 'update' to update it. + * @param object $item Passed when updating an item. Null during creation. + * @return WC_Order_Item_Fee + * @throws WC_REST_Exception Invalid data, server error. + */ + protected function prepare_fee_lines( $posted, $action = 'create', $item = null ) { + $item = is_null( $item ) ? new WC_Order_Item_Fee( ! empty( $posted['id'] ) ? $posted['id'] : '' ) : $item; + + if ( 'create' === $action ) { + if ( empty( $posted['name'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_fee_item', __( 'Fee name is required.', 'woocommerce' ), 400 ); + } + } + + $this->maybe_set_item_props( $item, array( 'name', 'tax_class', 'tax_status', 'total' ), $posted ); + $this->maybe_set_item_meta_data( $item, $posted ); + + return $item; + } + + /** + * Create or update an order coupon. + * + * @param array $posted Item data. + * @param string $action 'create' to add coupon or 'update' to update it. + * @param object $item Passed when updating an item. Null during creation. + * @return WC_Order_Item_Coupon + * @throws WC_REST_Exception Invalid data, server error. + */ + protected function prepare_coupon_lines( $posted, $action = 'create', $item = null ) { + $item = is_null( $item ) ? new WC_Order_Item_Coupon( ! empty( $posted['id'] ) ? $posted['id'] : '' ) : $item; + + if ( 'create' === $action ) { + if ( empty( $posted['code'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_coupon_coupon', __( 'Coupon code is required.', 'woocommerce' ), 400 ); + } + } + + $this->maybe_set_item_props( $item, array( 'code', 'discount' ), $posted ); + $this->maybe_set_item_meta_data( $item, $posted ); + + return $item; + } + + /** + * Wrapper method to create/update order items. + * When updating, the item ID provided is checked to ensure it is associated + * with the order. + * + * @param WC_Order $order order object. + * @param string $item_type The item type. + * @param array $posted item provided in the request body. + * @throws WC_REST_Exception If item ID is not associated with order. + */ + protected function set_item( $order, $item_type, $posted ) { + global $wpdb; + + if ( ! empty( $posted['id'] ) ) { + $action = 'update'; + } else { + $action = 'create'; + } + + $method = 'prepare_' . $item_type; + $item = null; + + // Verify provided line item ID is associated with order. + if ( 'update' === $action ) { + $item = $order->get_item( absint( $posted['id'] ), false ); + + if ( ! $item ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_item_id', __( 'Order item ID provided is not associated with order.', 'woocommerce' ), 400 ); + } + } + + // Prepare item data. + $item = $this->$method( $posted, $action, $item ); + + do_action( 'woocommerce_rest_set_order_item', $item, $posted ); + + // If creating the order, add the item to it. + if ( 'create' === $action ) { + $order->add_item( $item ); + } else { + $item->save(); + } + } + + /** + * Helper method to check if the resource ID associated with the provided item is null. + * Items can be deleted by setting the resource ID to null. + * + * @param array $item Item provided in the request body. + * @return bool True if the item resource ID is null, false otherwise. + */ + protected function item_is_null( $item ) { + $keys = array( 'product_id', 'method_id', 'method_title', 'name', 'code' ); + + foreach ( $keys as $key ) { + if ( array_key_exists( $key, $item ) && is_null( $item[ $key ] ) ) { + return true; + } + } + + return false; + } + + /** + * Get order statuses without prefixes. + * + * @return array + */ + protected function get_order_statuses() { + $order_statuses = array(); + + foreach ( array_keys( wc_get_order_statuses() ) as $status ) { + $order_statuses[] = str_replace( 'wc-', '', $status ); + } + + return $order_statuses; + } + + /** + * Get the Order's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => $this->post_type, + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'parent_id' => array( + 'description' => __( 'Parent order ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'number' => array( + 'description' => __( 'Order number.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'order_key' => array( + 'description' => __( 'Order key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'created_via' => array( + 'description' => __( 'Shows where the order was created.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'version' => array( + 'description' => __( 'Version of WooCommerce which last updated the order.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'status' => array( + 'description' => __( 'Order status.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'pending', + 'enum' => $this->get_order_statuses(), + 'context' => array( 'view', 'edit' ), + ), + 'currency' => array( + 'description' => __( 'Currency the order was created with, in ISO format.', 'woocommerce' ), + 'type' => 'string', + 'default' => get_woocommerce_currency(), + 'enum' => array_keys( get_woocommerce_currencies() ), + 'context' => array( 'view', 'edit' ), + ), + 'date_created' => array( + 'description' => __( "The date the order was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created_gmt' => array( + 'description' => __( 'The date the order was created, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the order was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified_gmt' => array( + 'description' => __( 'The date the order was last modified, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'discount_total' => array( + 'description' => __( 'Total discount amount for the order.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'discount_tax' => array( + 'description' => __( 'Total discount tax amount for the order.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'shipping_total' => array( + 'description' => __( 'Total shipping amount for the order.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'shipping_tax' => array( + 'description' => __( 'Total shipping tax amount for the order.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'cart_tax' => array( + 'description' => __( 'Sum of line item taxes only.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Grand total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total_tax' => array( + 'description' => __( 'Sum of all taxes.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'prices_include_tax' => array( + 'description' => __( 'True the prices included tax during checkout.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'customer_id' => array( + 'description' => __( 'User ID who owns the order. 0 for guests.', 'woocommerce' ), + 'type' => 'integer', + 'default' => 0, + 'context' => array( 'view', 'edit' ), + ), + 'customer_ip_address' => array( + 'description' => __( "Customer's IP address.", 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'customer_user_agent' => array( + 'description' => __( 'User agent of the customer.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'customer_note' => array( + 'description' => __( 'Note left by customer during checkout.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'billing' => array( + 'description' => __( 'Billing address.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'first_name' => array( + 'description' => __( 'First name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'last_name' => array( + 'description' => __( 'Last name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'company' => array( + 'description' => __( 'Company name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_1' => array( + 'description' => __( 'Address line 1', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_2' => array( + 'description' => __( 'Address line 2', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'city' => array( + 'description' => __( 'City name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'state' => array( + 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'postcode' => array( + 'description' => __( 'Postal code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'country' => array( + 'description' => __( 'Country code in ISO 3166-1 alpha-2 format.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'email' => array( + 'description' => __( 'Email address.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'email', + 'context' => array( 'view', 'edit' ), + ), + 'phone' => array( + 'description' => __( 'Phone number.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'shipping' => array( + 'description' => __( 'Shipping address.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'first_name' => array( + 'description' => __( 'First name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'last_name' => array( + 'description' => __( 'Last name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'company' => array( + 'description' => __( 'Company name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_1' => array( + 'description' => __( 'Address line 1', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_2' => array( + 'description' => __( 'Address line 2', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'city' => array( + 'description' => __( 'City name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'state' => array( + 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'postcode' => array( + 'description' => __( 'Postal code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'country' => array( + 'description' => __( 'Country code in ISO 3166-1 alpha-2 format.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'payment_method' => array( + 'description' => __( 'Payment method ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'payment_method_title' => array( + 'description' => __( 'Payment method title.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'transaction_id' => array( + 'description' => __( 'Unique transaction ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'date_paid' => array( + 'description' => __( "The date the order was paid, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_paid_gmt' => array( + 'description' => __( 'The date the order was paid, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_completed' => array( + 'description' => __( "The date the order was completed, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_completed_gmt' => array( + 'description' => __( 'The date the order was completed, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'cart_hash' => array( + 'description' => __( 'MD5 hash of cart items to ensure orders are not modified.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'meta_data' => array( + 'description' => __( 'Meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Meta ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => array( 'string', 'null' ), + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'line_items' => array( + 'description' => __( 'Line items data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Product name.', 'woocommerce' ), + 'type' => array( 'string', 'null' ), + 'context' => array( 'view', 'edit' ), + ), + 'product_id' => array( + 'description' => __( 'Product ID.', 'woocommerce' ), + 'type' => array( 'integer' ), + 'context' => array( 'view', 'edit' ), + ), + 'variation_id' => array( + 'description' => __( 'Variation ID, if applicable.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'quantity' => array( + 'description' => __( 'Quantity ordered.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'tax_class' => array( + 'description' => __( 'Tax class of product.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'subtotal' => array( + 'description' => __( 'Line subtotal (before discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'subtotal_tax' => array( + 'description' => __( 'Line subtotal tax (before discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Line total (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'total_tax' => array( + 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'taxes' => array( + 'description' => __( 'Line taxes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Tax rate ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'total' => array( + 'description' => __( 'Tax total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'subtotal' => array( + 'description' => __( 'Tax subtotal.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'meta_data' => array( + 'description' => __( 'Meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Meta ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => array( 'string', 'null' ), + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'sku' => array( + 'description' => __( 'Product SKU.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'price' => array( + 'description' => __( 'Product price.', 'woocommerce' ), + 'type' => 'number', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + 'tax_lines' => array( + 'description' => __( 'Tax lines data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'rate_code' => array( + 'description' => __( 'Tax rate code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'rate_id' => array( + 'description' => __( 'Tax rate ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'label' => array( + 'description' => __( 'Tax rate label.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'compound' => array( + 'description' => __( 'Show if is a compound tax rate.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'tax_total' => array( + 'description' => __( 'Tax total (not including shipping taxes).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'shipping_tax_total' => array( + 'description' => __( 'Shipping tax total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'meta_data' => array( + 'description' => __( 'Meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Meta ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => array( 'string', 'null' ), + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + ), + ), + ), + 'shipping_lines' => array( + 'description' => __( 'Shipping lines data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'method_title' => array( + 'description' => __( 'Shipping method name.', 'woocommerce' ), + 'type' => array( 'string', 'null' ), + 'context' => array( 'view', 'edit' ), + ), + 'method_id' => array( + 'description' => __( 'Shipping method ID.', 'woocommerce' ), + 'type' => array( 'string', 'null' ), + 'context' => array( 'view', 'edit' ), + ), + 'instance_id' => array( + 'description' => __( 'Shipping instance ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'total' => array( + 'description' => __( 'Line total (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'total_tax' => array( + 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'taxes' => array( + 'description' => __( 'Line taxes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Tax rate ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Tax total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + 'meta_data' => array( + 'description' => __( 'Meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Meta ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => array( 'string', 'null' ), + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + ), + ), + ), + 'fee_lines' => array( + 'description' => __( 'Fee lines data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Fee name.', 'woocommerce' ), + 'type' => array( 'string', 'null' ), + 'context' => array( 'view', 'edit' ), + ), + 'tax_class' => array( + 'description' => __( 'Tax class of fee.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'tax_status' => array( + 'description' => __( 'Tax status of fee.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'enum' => array( 'taxable', 'none' ), + ), + 'total' => array( + 'description' => __( 'Line total (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'total_tax' => array( + 'description' => __( 'Line total tax (after discounts).', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'taxes' => array( + 'description' => __( 'Line taxes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Tax rate ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Tax total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'subtotal' => array( + 'description' => __( 'Tax subtotal.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + 'meta_data' => array( + 'description' => __( 'Meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Meta ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => array( 'string', 'null' ), + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + ), + ), + ), + 'coupon_lines' => array( + 'description' => __( 'Coupons line data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Item ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'code' => array( + 'description' => __( 'Coupon code.', 'woocommerce' ), + 'type' => array( 'string', 'null' ), + 'context' => array( 'view', 'edit' ), + ), + 'discount' => array( + 'description' => __( 'Discount total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'discount_tax' => array( + 'description' => __( 'Discount total tax.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'meta_data' => array( + 'description' => __( 'Meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Meta ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => array( 'string', 'null' ), + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + ), + ), + ), + 'refunds' => array( + 'description' => __( 'List of refunds.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Refund ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'reason' => array( + 'description' => __( 'Refund reason.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Refund total.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + 'set_paid' => array( + 'description' => __( 'Define if the order is paid. It will set the status to processing and reduce stock items.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'edit' ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + $params['status'] = array( + 'default' => 'any', + 'description' => __( 'Limit result set to orders assigned a specific status.', 'woocommerce' ), + 'type' => 'string', + 'enum' => array_merge( array( 'any', 'trash' ), $this->get_order_statuses() ), + 'sanitize_callback' => 'sanitize_key', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['customer'] = array( + 'description' => __( 'Limit result set to orders assigned a specific customer.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['product'] = array( + 'description' => __( 'Limit result set to orders assigned a specific product.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['dp'] = array( + 'default' => wc_get_price_decimals(), + 'description' => __( 'Number of decimal points to use in each resource.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } +} diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-payment-gateways-v2-controller.php b/includes/rest-api/Controllers/Version2/class-wc-rest-payment-gateways-v2-controller.php new file mode 100644 index 00000000000..69bfc51f418 --- /dev/null +++ b/includes/rest-api/Controllers/Version2/class-wc-rest-payment-gateways-v2-controller.php @@ -0,0 +1,466 @@ + + */ + public function register_routes() { + register_rest_route( + $this->namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + register_rest_route( + $this->namespace, '/' . $this->rest_base . '/(?P[\w-]+)', array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'string', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Check whether a given request has permission to view payment gateways. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'payment_gateways', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + return true; + } + + /** + * Check if a given request has access to read a payment gateway. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'payment_gateways', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + return true; + } + + /** + * Check whether a given request has permission to edit payment gateways. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function update_items_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'payment_gateways', 'edit' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + return true; + } + + /** + * Get payment gateways. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + $payment_gateways = WC()->payment_gateways->payment_gateways(); + $response = array(); + foreach ( $payment_gateways as $payment_gateway_id => $payment_gateway ) { + $payment_gateway->id = $payment_gateway_id; + $gateway = $this->prepare_item_for_response( $payment_gateway, $request ); + $gateway = $this->prepare_response_for_collection( $gateway ); + $response[] = $gateway; + } + return rest_ensure_response( $response ); + } + + /** + * Get a single payment gateway. + * + * @param WP_REST_Request $request Request data. + * @return WP_REST_Response|WP_Error + */ + public function get_item( $request ) { + $gateway = $this->get_gateway( $request ); + + if ( is_null( $gateway ) ) { + return new WP_Error( 'woocommerce_rest_payment_gateway_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $gateway = $this->prepare_item_for_response( $gateway, $request ); + return rest_ensure_response( $gateway ); + } + + /** + * Update A Single Payment Method. + * + * @param WP_REST_Request $request Request data. + * @return WP_REST_Response|WP_Error + */ + public function update_item( $request ) { + $gateway = $this->get_gateway( $request ); + + if ( is_null( $gateway ) ) { + return new WP_Error( 'woocommerce_rest_payment_gateway_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + // Get settings. + $gateway->init_form_fields(); + $settings = $gateway->settings; + + // Update settings. + if ( isset( $request['settings'] ) ) { + $errors_found = false; + foreach ( $gateway->form_fields as $key => $field ) { + if ( isset( $request['settings'][ $key ] ) ) { + if ( is_callable( array( $this, 'validate_setting_' . $field['type'] . '_field' ) ) ) { + $value = $this->{'validate_setting_' . $field['type'] . '_field'}( $request['settings'][ $key ], $field ); + } else { + $value = $this->validate_setting_text_field( $request['settings'][ $key ], $field ); + } + if ( is_wp_error( $value ) ) { + $errors_found = true; + break; + } + $settings[ $key ] = $value; + } + } + + if ( $errors_found ) { + return new WP_Error( 'rest_setting_value_invalid', __( 'An invalid setting value was passed.', 'woocommerce' ), array( 'status' => 400 ) ); + } + } + + // Update if this method is enabled or not. + if ( isset( $request['enabled'] ) ) { + $settings['enabled'] = wc_bool_to_string( $request['enabled'] ); + $gateway->enabled = $settings['enabled']; + } + + // Update title. + if ( isset( $request['title'] ) ) { + $settings['title'] = $request['title']; + $gateway->title = $settings['title']; + } + + // Update description. + if ( isset( $request['description'] ) ) { + $settings['description'] = $request['description']; + $gateway->description = $settings['description']; + } + + // Update options. + $gateway->settings = $settings; + update_option( $gateway->get_option_key(), apply_filters( 'woocommerce_gateway_' . $gateway->id . '_settings_values', $settings, $gateway ) ); + + // Update order. + if ( isset( $request['order'] ) ) { + $order = (array) get_option( 'woocommerce_gateway_order' ); + $order[ $gateway->id ] = $request['order']; + update_option( 'woocommerce_gateway_order', $order ); + $gateway->order = absint( $request['order'] ); + } + + $gateway = $this->prepare_item_for_response( $gateway, $request ); + return rest_ensure_response( $gateway ); + } + + /** + * Get a gateway based on the current request object. + * + * @param WP_REST_Request $request Request data. + * @return WP_REST_Response|null + */ + public function get_gateway( $request ) { + $gateway = null; + $payment_gateways = WC()->payment_gateways->payment_gateways(); + foreach ( $payment_gateways as $payment_gateway_id => $payment_gateway ) { + if ( $request['id'] !== $payment_gateway_id ) { + continue; + } + $payment_gateway->id = $payment_gateway_id; + $gateway = $payment_gateway; + } + return $gateway; + } + + /** + * Prepare a payment gateway for response. + * + * @param WC_Payment_Gateway $gateway Payment gateway object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $gateway, $request ) { + $order = (array) get_option( 'woocommerce_gateway_order' ); + $item = array( + 'id' => $gateway->id, + 'title' => $gateway->title, + 'description' => $gateway->description, + 'order' => isset( $order[ $gateway->id ] ) ? $order[ $gateway->id ] : '', + 'enabled' => ( 'yes' === $gateway->enabled ), + 'method_title' => $gateway->get_method_title(), + 'method_description' => $gateway->get_method_description(), + 'settings' => $this->get_settings( $gateway ), + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $item, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + $response = rest_ensure_response( $data ); + $response->add_links( $this->prepare_links( $gateway, $request ) ); + + /** + * Filter payment gateway objects returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param WC_Payment_Gateway $gateway Payment gateway object. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'woocommerce_rest_prepare_payment_gateway', $response, $gateway, $request ); + } + + /** + * Return settings associated with this payment gateway. + * + * @param WC_Payment_Gateway $gateway Gateway data. + * + * @return array + */ + public function get_settings( $gateway ) { + $settings = array(); + $gateway->init_form_fields(); + foreach ( $gateway->form_fields as $id => $field ) { + // Make sure we at least have a title and type. + if ( empty( $field['title'] ) || empty( $field['type'] ) ) { + continue; + } + // Ignore 'title' settings/fields -- they are UI only. + if ( 'title' === $field['type'] ) { + continue; + } + // Ignore 'enabled' and 'description' which get included elsewhere. + if ( in_array( $id, array( 'enabled', 'description' ), true ) ) { + continue; + } + $data = array( + 'id' => $id, + 'label' => empty( $field['label'] ) ? $field['title'] : $field['label'], + 'description' => empty( $field['description'] ) ? '' : $field['description'], + 'type' => $field['type'], + 'value' => empty( $gateway->settings[ $id ] ) ? '' : $gateway->settings[ $id ], + 'default' => empty( $field['default'] ) ? '' : $field['default'], + 'tip' => empty( $field['description'] ) ? '' : $field['description'], + 'placeholder' => empty( $field['placeholder'] ) ? '' : $field['placeholder'], + ); + if ( ! empty( $field['options'] ) ) { + $data['options'] = $field['options']; + } + $settings[ $id ] = $data; + } + return $settings; + } + + /** + * Prepare links for the request. + * + * @param WC_Payment_Gateway $gateway Payment gateway object. + * @param WP_REST_Request $request Request object. + * @return array + */ + protected function prepare_links( $gateway, $request ) { + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $gateway->id ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + + return $links; + } + + /** + * Get the payment gateway schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'payment_gateway', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Payment gateway ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'title' => array( + 'description' => __( 'Payment gateway title on checkout.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'description' => array( + 'description' => __( 'Payment gateway description on checkout.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'order' => array( + 'description' => __( 'Payment gateway sort order.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'absint', + ), + ), + 'enabled' => array( + 'description' => __( 'Payment gateway enabled status.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + ), + 'method_title' => array( + 'description' => __( 'Payment gateway method title.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'method_description' => array( + 'description' => __( 'Payment gateway method description.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'settings' => array( + 'description' => __( 'Payment gateway settings.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'id' => array( + 'description' => __( 'A unique identifier for the setting.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'label' => array( + 'description' => __( 'A human readable label for the setting used in interfaces.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'description' => array( + 'description' => __( 'A human readable description for the setting used in interfaces.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'type' => array( + 'description' => __( 'Type of setting.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'enum' => array( 'text', 'email', 'number', 'color', 'password', 'textarea', 'select', 'multiselect', 'radio', 'image_width', 'checkbox' ), + 'readonly' => true, + ), + 'value' => array( + 'description' => __( 'Setting value.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'default' => array( + 'description' => __( 'Default value for the setting.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'tip' => array( + 'description' => __( 'Additional help text shown to the user about the setting.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'placeholder' => array( + 'description' => __( 'Placeholder text to be displayed in text inputs.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get any query params needed. + * + * @return array + */ + public function get_collection_params() { + return array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ); + } + +} diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-product-attribute-terms-v2-controller.php b/includes/rest-api/Controllers/Version2/class-wc-rest-product-attribute-terms-v2-controller.php new file mode 100644 index 00000000000..27d71b11c10 --- /dev/null +++ b/includes/rest-api/Controllers/Version2/class-wc-rest-product-attribute-terms-v2-controller.php @@ -0,0 +1,27 @@ +/terms endpoint. + * + * @package Automattic/WooCommerce/RestApi + * @since 2.6.0 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * REST API Product Attribute Terms controller class. + * + * @package Automattic/WooCommerce/RestApi + * @extends WC_REST_Product_Attribute_Terms_V1_Controller + */ +class WC_REST_Product_Attribute_Terms_V2_Controller extends WC_REST_Product_Attribute_Terms_V1_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + protected $namespace = 'wc/v2'; +} diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-product-attributes-v2-controller.php b/includes/rest-api/Controllers/Version2/class-wc-rest-product-attributes-v2-controller.php new file mode 100644 index 00000000000..3ff18b310a2 --- /dev/null +++ b/includes/rest-api/Controllers/Version2/class-wc-rest-product-attributes-v2-controller.php @@ -0,0 +1,27 @@ +term_id, 'display_type', true ); + + // Get category order. + $menu_order = get_term_meta( $item->term_id, 'order', true ); + + $data = array( + 'id' => (int) $item->term_id, + 'name' => $item->name, + 'slug' => $item->slug, + 'parent' => (int) $item->parent, + 'description' => $item->description, + 'display' => $display_type ? $display_type : 'default', + 'image' => null, + 'menu_order' => (int) $menu_order, + 'count' => (int) $item->count, + ); + + // Get category image. + $image_id = get_term_meta( $item->term_id, 'thumbnail_id', true ); + if ( $image_id ) { + $attachment = get_post( $image_id ); + + $data['image'] = array( + 'id' => (int) $image_id, + 'date_created' => wc_rest_prepare_date_response( $attachment->post_date ), + 'date_created_gmt' => wc_rest_prepare_date_response( $attachment->post_date_gmt ), + 'date_modified' => wc_rest_prepare_date_response( $attachment->post_modified ), + 'date_modified_gmt' => wc_rest_prepare_date_response( $attachment->post_modified_gmt ), + 'src' => wp_get_attachment_url( $image_id ), + 'title' => get_the_title( $attachment ), + 'alt' => get_post_meta( $image_id, '_wp_attachment_image_alt', true ), + ); + } + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $item, $request ) ); + + /** + * Filter a term item returned from the API. + * + * Allows modification of the term data right before it is returned. + * + * @param WP_REST_Response $response The response object. + * @param object $item The original term object. + * @param WP_REST_Request $request Request used to generate the response. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->taxonomy}", $response, $item, $request ); + } + + /** + * Get the Category schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => $this->taxonomy, + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Category name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'slug' => array( + 'description' => __( 'An alphanumeric identifier for the resource unique to its type.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_title', + ), + ), + 'parent' => array( + 'description' => __( 'The ID for the parent of the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'description' => array( + 'description' => __( 'HTML description of the resource.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'wp_filter_post_kses', + ), + ), + 'display' => array( + 'description' => __( 'Category archive display type.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'default', + 'enum' => array( 'default', 'products', 'subcategories', 'both' ), + 'context' => array( 'view', 'edit' ), + ), + 'image' => array( + 'description' => __( 'Image data.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'id' => array( + 'description' => __( 'Image ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'date_created' => array( + 'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created_gmt' => array( + 'description' => __( 'The date the image was created, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified_gmt' => array( + 'description' => __( 'The date the image was last modified, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'src' => array( + 'description' => __( 'Image URL.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + ), + 'title' => array( + 'description' => __( 'Image name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'alt' => array( + 'description' => __( 'Image alternative text.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'menu_order' => array( + 'description' => __( 'Menu order, used to custom sort the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'count' => array( + 'description' => __( 'Number of published products for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-product-reviews-v2-controller.php b/includes/rest-api/Controllers/Version2/class-wc-rest-product-reviews-v2-controller.php new file mode 100644 index 00000000000..12ab7d62655 --- /dev/null +++ b/includes/rest-api/Controllers/Version2/class-wc-rest-product-reviews-v2-controller.php @@ -0,0 +1,199 @@ +/reviews. + * + * @package Automattic/WooCommerce/RestApi + * @since 2.6.0 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * REST API Product Reviews Controller Class. + * + * @package Automattic/WooCommerce/RestApi + * @extends WC_REST_Product_Reviews_V1_Controller + */ +class WC_REST_Product_Reviews_V2_Controller extends WC_REST_Product_Reviews_V1_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + protected $namespace = 'wc/v2'; + + /** + * Route base. + * + * @var string + */ + protected $rest_base = 'products/(?P[\d]+)/reviews'; + + /** + * Register the routes for product reviews. + */ + public function register_routes() { + parent::register_routes(); + + register_rest_route( + $this->namespace, '/' . $this->rest_base . '/batch', array( + 'args' => array( + 'product_id' => array( + 'description' => __( 'Unique identifier for the variable product.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'batch_items' ), + 'permission_callback' => array( $this, 'batch_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_batch_schema' ), + ) + ); + } + + /** + * Check if a given request has access to batch manage product reviews. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function batch_items_permissions_check( $request ) { + if ( ! wc_rest_check_post_permissions( 'product', 'batch' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to batch manipulate this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + return true; + } + + /** + * Prepare a single product review output for response. + * + * @param WP_Comment $review Product review object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $review, $request ) { + $data = array( + 'id' => (int) $review->comment_ID, + 'date_created' => wc_rest_prepare_date_response( $review->comment_date ), + 'date_created_gmt' => wc_rest_prepare_date_response( $review->comment_date_gmt ), + 'review' => $review->comment_content, + 'rating' => (int) get_comment_meta( $review->comment_ID, 'rating', true ), + 'name' => $review->comment_author, + 'email' => $review->comment_author_email, + 'verified' => wc_review_is_from_verified_owner( $review->comment_ID ), + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $review, $request ) ); + + /** + * Filter product reviews object returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param WP_Comment $review Product review object used to create response. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'woocommerce_rest_prepare_product_review', $response, $review, $request ); + } + + + /** + * Bulk create, update and delete items. + * + * @since 3.0.0 + * @param WP_REST_Request $request Full details about the request. + * @return array Of WP_Error or WP_REST_Response. + */ + public function batch_items( $request ) { + $items = array_filter( $request->get_params() ); + $params = $request->get_url_params(); + $product_id = $params['product_id']; + $body_params = array(); + + foreach ( array( 'update', 'create', 'delete' ) as $batch_type ) { + if ( ! empty( $items[ $batch_type ] ) ) { + $injected_items = array(); + foreach ( $items[ $batch_type ] as $item ) { + $injected_items[] = is_array( $item ) ? array_merge( array( 'product_id' => $product_id ), $item ) : $item; + } + $body_params[ $batch_type ] = $injected_items; + } + } + + $request = new WP_REST_Request( $request->get_method() ); + $request->set_body_params( $body_params ); + + return parent::batch_items( $request ); + } + + /** + * Get the Product Review's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'product_review', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'review' => array( + 'description' => __( 'The content of the review.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'date_created' => array( + 'description' => __( "The date the review was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'date_created_gmt' => array( + 'description' => __( 'The date the review was created, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'rating' => array( + 'description' => __( 'Review rating (0 to 5).', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Reviewer name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'email' => array( + 'description' => __( 'Reviewer email.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'verified' => array( + 'description' => __( 'Shows if the reviewer bought the product or not.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-product-shipping-classes-v2-controller.php b/includes/rest-api/Controllers/Version2/class-wc-rest-product-shipping-classes-v2-controller.php new file mode 100644 index 00000000000..6430ded093f --- /dev/null +++ b/includes/rest-api/Controllers/Version2/class-wc-rest-product-shipping-classes-v2-controller.php @@ -0,0 +1,27 @@ +/variations endpoints. + * + * @package Automattic/WooCommerce/RestApi + * @since 3.0.0 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * REST API variations controller class. + * + * @package Automattic/WooCommerce/RestApi + * @extends WC_REST_Products_V2_Controller + */ +class WC_REST_Product_Variations_V2_Controller extends WC_REST_Products_V2_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + protected $namespace = 'wc/v2'; + + /** + * Route base. + * + * @var string + */ + protected $rest_base = 'products/(?P[\d]+)/variations'; + + /** + * Post type. + * + * @var string + */ + protected $post_type = 'product_variation'; + + /** + * Register the routes for products. + */ + public function register_routes() { + register_rest_route( + $this->namespace, '/' . $this->rest_base, array( + 'args' => array( + 'product_id' => array( + 'description' => __( 'Unique identifier for the variable product.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + register_rest_route( + $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + 'args' => array( + 'product_id' => array( + 'description' => __( 'Unique identifier for the variable product.', 'woocommerce' ), + 'type' => 'integer', + ), + 'id' => array( + 'description' => __( 'Unique identifier for the variation.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( + array( + 'default' => 'view', + ) + ), + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'type' => 'boolean', + 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + register_rest_route( + $this->namespace, '/' . $this->rest_base . '/batch', array( + 'args' => array( + 'product_id' => array( + 'description' => __( 'Unique identifier for the variable product.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'batch_items' ), + 'permission_callback' => array( $this, 'batch_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_batch_schema' ), + ) + ); + } + + /** + * Get object. + * + * @since 3.0.0 + * @param int $id Object ID. + * @return WC_Data + */ + protected function get_object( $id ) { + return wc_get_product( $id ); + } + + /** + * Check if a given request has access to update an item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function update_item_permissions_check( $request ) { + $object = $this->get_object( (int) $request['id'] ); + + if ( $object && 0 !== $object->get_id() && ! wc_rest_check_post_permissions( $this->post_type, 'edit', $object->get_id() ) ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + // Check if variation belongs to the correct parent product. + if ( $object && 0 !== $object->get_parent_id() && absint( $request['product_id'] ) !== $object->get_parent_id() ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Parent product does not match current variation.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Prepare a single variation output for response. + * + * @since 3.0.0 + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response + */ + public function prepare_object_for_response( $object, $request ) { + $data = array( + 'id' => $object->get_id(), + 'date_created' => wc_rest_prepare_date_response( $object->get_date_created(), false ), + 'date_created_gmt' => wc_rest_prepare_date_response( $object->get_date_created() ), + 'date_modified' => wc_rest_prepare_date_response( $object->get_date_modified(), false ), + 'date_modified_gmt' => wc_rest_prepare_date_response( $object->get_date_modified() ), + 'description' => wc_format_content( $object->get_description() ), + 'permalink' => $object->get_permalink(), + 'sku' => $object->get_sku(), + 'price' => $object->get_price(), + 'regular_price' => $object->get_regular_price(), + 'sale_price' => $object->get_sale_price(), + 'date_on_sale_from' => wc_rest_prepare_date_response( $object->get_date_on_sale_from(), false ), + 'date_on_sale_from_gmt' => wc_rest_prepare_date_response( $object->get_date_on_sale_from() ), + 'date_on_sale_to' => wc_rest_prepare_date_response( $object->get_date_on_sale_to(), false ), + 'date_on_sale_to_gmt' => wc_rest_prepare_date_response( $object->get_date_on_sale_to() ), + 'on_sale' => $object->is_on_sale(), + 'visible' => $object->is_visible(), + 'purchasable' => $object->is_purchasable(), + 'virtual' => $object->is_virtual(), + 'downloadable' => $object->is_downloadable(), + 'downloads' => $this->get_downloads( $object ), + 'download_limit' => '' !== $object->get_download_limit() ? (int) $object->get_download_limit() : -1, + 'download_expiry' => '' !== $object->get_download_expiry() ? (int) $object->get_download_expiry() : -1, + 'tax_status' => $object->get_tax_status(), + 'tax_class' => $object->get_tax_class(), + 'manage_stock' => $object->managing_stock(), + 'stock_quantity' => $object->get_stock_quantity(), + 'in_stock' => $object->is_in_stock(), + 'backorders' => $object->get_backorders(), + 'backorders_allowed' => $object->backorders_allowed(), + 'backordered' => $object->is_on_backorder(), + 'weight' => $object->get_weight(), + 'dimensions' => array( + 'length' => $object->get_length(), + 'width' => $object->get_width(), + 'height' => $object->get_height(), + ), + 'shipping_class' => $object->get_shipping_class(), + 'shipping_class_id' => $object->get_shipping_class_id(), + 'image' => current( $this->get_images( $object ) ), + 'attributes' => $this->get_attributes( $object ), + 'menu_order' => $object->get_menu_order(), + 'meta_data' => $object->get_meta_data(), + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + $response = rest_ensure_response( $data ); + $response->add_links( $this->prepare_links( $object, $request ) ); + + /** + * Filter the data for a response. + * + * The dynamic portion of the hook name, $this->post_type, + * refers to object type being prepared for the response. + * + * @param WP_REST_Response $response The response object. + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->post_type}_object", $response, $object, $request ); + } + + /** + * Prepare objects query. + * + * @since 3.0.0 + * @param WP_REST_Request $request Full details about the request. + * @return array + */ + protected function prepare_objects_query( $request ) { + $args = parent::prepare_objects_query( $request ); + + $args['post_parent'] = $request['product_id']; + + return $args; + } + + /** + * Prepare a single variation for create or update. + * + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + * @return WP_Error|WC_Data + */ + protected function prepare_object_for_database( $request, $creating = false ) { + if ( isset( $request['id'] ) ) { + $variation = wc_get_product( absint( $request['id'] ) ); + } else { + $variation = new WC_Product_Variation(); + } + + // Update parent ID just once. + if ( 0 === $variation->get_parent_id() ) { + $variation->set_parent_id( absint( $request['product_id'] ) ); + } + + // Status. + if ( isset( $request['visible'] ) ) { + $variation->set_status( false === $request['visible'] ? 'private' : 'publish' ); + } + + // SKU. + if ( isset( $request['sku'] ) ) { + $variation->set_sku( wc_clean( $request['sku'] ) ); + } + + // Thumbnail. + if ( isset( $request['image'] ) ) { + if ( is_array( $request['image'] ) && ! empty( $request['image'] ) ) { + $image = $request['image']; + if ( is_array( $image ) ) { + $image['position'] = 0; + } + + $variation = $this->set_product_images( $variation, array( $image ) ); + } else { + $variation->set_image_id( '' ); + } + } + + // Virtual variation. + if ( isset( $request['virtual'] ) ) { + $variation->set_virtual( $request['virtual'] ); + } + + // Downloadable variation. + if ( isset( $request['downloadable'] ) ) { + $variation->set_downloadable( $request['downloadable'] ); + } + + // Downloads. + if ( $variation->get_downloadable() ) { + // Downloadable files. + if ( isset( $request['downloads'] ) && is_array( $request['downloads'] ) ) { + $variation = $this->save_downloadable_files( $variation, $request['downloads'] ); + } + + // Download limit. + if ( isset( $request['download_limit'] ) ) { + $variation->set_download_limit( $request['download_limit'] ); + } + + // Download expiry. + if ( isset( $request['download_expiry'] ) ) { + $variation->set_download_expiry( $request['download_expiry'] ); + } + } + + // Shipping data. + $variation = $this->save_product_shipping_data( $variation, $request ); + + // Stock handling. + if ( isset( $request['manage_stock'] ) ) { + if ( 'parent' === $request['manage_stock'] ) { + $variation->set_manage_stock( false ); // This just indicates the variation does not manage stock, but the parent does. + } else { + $variation->set_manage_stock( wc_string_to_bool( $request['manage_stock'] ) ); + } + } + + if ( isset( $request['in_stock'] ) ) { + $variation->set_stock_status( true === $request['in_stock'] ? 'instock' : 'outofstock' ); + } + + if ( isset( $request['backorders'] ) ) { + $variation->set_backorders( $request['backorders'] ); + } + + if ( $variation->get_manage_stock() ) { + if ( isset( $request['stock_quantity'] ) ) { + $variation->set_stock_quantity( $request['stock_quantity'] ); + } elseif ( isset( $request['inventory_delta'] ) ) { + $stock_quantity = wc_stock_amount( $variation->get_stock_quantity() ); + $stock_quantity += wc_stock_amount( $request['inventory_delta'] ); + $variation->set_stock_quantity( $stock_quantity ); + } + } else { + $variation->set_backorders( 'no' ); + $variation->set_stock_quantity( '' ); + } + + // Regular Price. + if ( isset( $request['regular_price'] ) ) { + $variation->set_regular_price( $request['regular_price'] ); + } + + // Sale Price. + if ( isset( $request['sale_price'] ) ) { + $variation->set_sale_price( $request['sale_price'] ); + } + + if ( isset( $request['date_on_sale_from'] ) ) { + $variation->set_date_on_sale_from( $request['date_on_sale_from'] ); + } + + if ( isset( $request['date_on_sale_from_gmt'] ) ) { + $variation->set_date_on_sale_from( $request['date_on_sale_from_gmt'] ? strtotime( $request['date_on_sale_from_gmt'] ) : null ); + } + + if ( isset( $request['date_on_sale_to'] ) ) { + $variation->set_date_on_sale_to( $request['date_on_sale_to'] ); + } + + if ( isset( $request['date_on_sale_to_gmt'] ) ) { + $variation->set_date_on_sale_to( $request['date_on_sale_to_gmt'] ? strtotime( $request['date_on_sale_to_gmt'] ) : null ); + } + + // Tax class. + if ( isset( $request['tax_class'] ) ) { + $variation->set_tax_class( $request['tax_class'] ); + } + + // Description. + if ( isset( $request['description'] ) ) { + $variation->set_description( wp_kses_post( $request['description'] ) ); + } + + // Update taxonomies. + if ( isset( $request['attributes'] ) ) { + $attributes = array(); + $parent = wc_get_product( $variation->get_parent_id() ); + $parent_attributes = $parent->get_attributes(); + + foreach ( $request['attributes'] as $attribute ) { + $attribute_id = 0; + $attribute_name = ''; + + // Check ID for global attributes or name for product attributes. + if ( ! empty( $attribute['id'] ) ) { + $attribute_id = absint( $attribute['id'] ); + $raw_attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); + } elseif ( ! empty( $attribute['name'] ) ) { + $raw_attribute_name = sanitize_title( $attribute['name'] ); + } + + if ( ! $attribute_id && ! $raw_attribute_name ) { + continue; + } + + $attribute_name = sanitize_title( $raw_attribute_name ); + + if ( ! isset( $parent_attributes[ $attribute_name ] ) || ! $parent_attributes[ $attribute_name ]->get_variation() ) { + continue; + } + + $attribute_key = sanitize_title( $parent_attributes[ $attribute_name ]->get_name() ); + $attribute_value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : ''; + + if ( $parent_attributes[ $attribute_name ]->is_taxonomy() ) { + // If dealing with a taxonomy, we need to get the slug from the name posted to the API. + $term = get_term_by( 'name', $attribute_value, $raw_attribute_name ); // @codingStandardsIgnoreLine + + if ( $term && ! is_wp_error( $term ) ) { + $attribute_value = $term->slug; + } else { + $attribute_value = sanitize_title( $attribute_value ); + } + } + + $attributes[ $attribute_key ] = $attribute_value; + } + + $variation->set_attributes( $attributes ); + } + + // Menu order. + if ( $request['menu_order'] ) { + $variation->set_menu_order( $request['menu_order'] ); + } + + // Meta data. + if ( is_array( $request['meta_data'] ) ) { + foreach ( $request['meta_data'] as $meta ) { + $variation->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); + } + } + + /** + * Filters an object before it is inserted via the REST API. + * + * The dynamic portion of the hook name, `$this->post_type`, + * refers to the object type slug. + * + * @param WC_Data $variation Object object. + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + */ + return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $variation, $request, $creating ); + } + + /** + * Clear caches here so in sync with any new variations. + * + * @param WC_Data $object Object data. + */ + public function clear_transients( $object ) { + wc_delete_product_transients( $object->get_parent_id() ); + wp_cache_delete( 'product-' . $object->get_parent_id(), 'products' ); + } + + /** + * Delete a variation. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return bool|WP_Error|WP_REST_Response + */ + public function delete_item( $request ) { + $force = (bool) $request['force']; + $object = $this->get_object( (int) $request['id'] ); + $result = false; + + if ( ! $object || 0 === $object->get_id() ) { + return new WP_Error( + "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( + 'status' => 404, + ) + ); + } + + $supports_trash = EMPTY_TRASH_DAYS > 0 && is_callable( array( $object, 'get_status' ) ); + + /** + * Filter whether an object is trashable. + * + * Return false to disable trash support for the object. + * + * @param boolean $supports_trash Whether the object type support trashing. + * @param WC_Data $object The object being considered for trashing support. + */ + $supports_trash = apply_filters( "woocommerce_rest_{$this->post_type}_object_trashable", $supports_trash, $object ); + + if ( ! wc_rest_check_post_permissions( $this->post_type, 'delete', $object->get_id() ) ) { + return new WP_Error( + /* translators: %s: post type */ + "woocommerce_rest_user_cannot_delete_{$this->post_type}", sprintf( __( 'Sorry, you are not allowed to delete %s.', 'woocommerce' ), $this->post_type ), array( + 'status' => rest_authorization_required_code(), + ) + ); + } + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_object_for_response( $object, $request ); + + // If we're forcing, then delete permanently. + if ( $force ) { + $object->delete( true ); + $result = 0 === $object->get_id(); + } else { + // If we don't support trashing for this type, error out. + if ( ! $supports_trash ) { + return new WP_Error( + /* translators: %s: post type */ + 'woocommerce_rest_trash_not_supported', sprintf( __( 'The %s does not support trashing.', 'woocommerce' ), $this->post_type ), array( + 'status' => 501, + ) + ); + } + + // Otherwise, only trash if we haven't already. + if ( is_callable( array( $object, 'get_status' ) ) ) { + if ( 'trash' === $object->get_status() ) { + return new WP_Error( + /* translators: %s: post type */ + 'woocommerce_rest_already_trashed', sprintf( __( 'The %s has already been deleted.', 'woocommerce' ), $this->post_type ), array( + 'status' => 410, + ) + ); + } + + $object->delete(); + $result = 'trash' === $object->get_status(); + } + } + + if ( ! $result ) { + return new WP_Error( + /* translators: %s: post type */ + 'woocommerce_rest_cannot_delete', sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), $this->post_type ), array( + 'status' => 500, + ) + ); + } + + // Delete parent product transients. + if ( 0 !== $object->get_parent_id() ) { + wc_delete_product_transients( $object->get_parent_id() ); + } + + /** + * Fires after a single object is deleted or trashed via the REST API. + * + * @param WC_Data $object The deleted or trashed object. + * @param WP_REST_Response $response The response data. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( "woocommerce_rest_delete_{$this->post_type}_object", $object, $response, $request ); + + return $response; + } + + /** + * Bulk create, update and delete items. + * + * @since 3.0.0 + * @param WP_REST_Request $request Full details about the request. + * @return array Of WP_Error or WP_REST_Response. + */ + public function batch_items( $request ) { + $items = array_filter( $request->get_params() ); + $params = $request->get_url_params(); + $query = $request->get_query_params(); + $product_id = $params['product_id']; + $body_params = array(); + + foreach ( array( 'update', 'create', 'delete' ) as $batch_type ) { + if ( ! empty( $items[ $batch_type ] ) ) { + $injected_items = array(); + foreach ( $items[ $batch_type ] as $item ) { + $injected_items[] = is_array( $item ) ? array_merge( + array( + 'product_id' => $product_id, + ), $item + ) : $item; + } + $body_params[ $batch_type ] = $injected_items; + } + } + + $request = new WP_REST_Request( $request->get_method() ); + $request->set_body_params( $body_params ); + $request->set_query_params( $query ); + + return parent::batch_items( $request ); + } + + /** + * Prepare links for the request. + * + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + * @return array Links for the given post. + */ + protected function prepare_links( $object, $request ) { + $product_id = (int) $request['product_id']; + $base = str_replace( '(?P[\d]+)', $product_id, $this->rest_base ); + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $base, $object->get_id() ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ), + ), + 'up' => array( + 'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $product_id ) ), + ), + ); + return $links; + } + + /** + * Get the Variation's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $weight_unit = get_option( 'woocommerce_weight_unit' ); + $dimension_unit = get_option( 'woocommerce_dimension_unit' ); + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => $this->post_type, + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( "The date the variation was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the variation was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'description' => array( + 'description' => __( 'Variation description.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'permalink' => array( + 'description' => __( 'Variation URL.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'sku' => array( + 'description' => __( 'Unique identifier.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'price' => array( + 'description' => __( 'Current variation price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'regular_price' => array( + 'description' => __( 'Variation regular price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'sale_price' => array( + 'description' => __( 'Variation sale price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_from' => array( + 'description' => __( "Start date of sale price, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_from_gmt' => array( + 'description' => __( 'Start date of sale price, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_to' => array( + 'description' => __( "End date of sale price, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_to_gmt' => array( + 'description' => __( 'End date of sale price, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'on_sale' => array( + 'description' => __( 'Shows if the variation is on sale.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'visible' => array( + 'description' => __( "Define if the variation is visible on the product's page.", 'woocommerce' ), + 'type' => 'boolean', + 'default' => true, + 'context' => array( 'view', 'edit' ), + ), + 'purchasable' => array( + 'description' => __( 'Shows if the variation can be bought.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'virtual' => array( + 'description' => __( 'If the variation is virtual.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'downloadable' => array( + 'description' => __( 'If the variation is downloadable.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'downloads' => array( + 'description' => __( 'List of downloadable files.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'File ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'File name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'file' => array( + 'description' => __( 'File URL.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'download_limit' => array( + 'description' => __( 'Number of times downloadable files can be downloaded after purchase.', 'woocommerce' ), + 'type' => 'integer', + 'default' => -1, + 'context' => array( 'view', 'edit' ), + ), + 'download_expiry' => array( + 'description' => __( 'Number of days until access to downloadable files expires.', 'woocommerce' ), + 'type' => 'integer', + 'default' => -1, + 'context' => array( 'view', 'edit' ), + ), + 'tax_status' => array( + 'description' => __( 'Tax status.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'taxable', + 'enum' => array( 'taxable', 'shipping', 'none' ), + 'context' => array( 'view', 'edit' ), + ), + 'tax_class' => array( + 'description' => __( 'Tax class.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'manage_stock' => array( + 'description' => __( 'Stock management at variation level.', 'woocommerce' ), + 'type' => array( 'boolean', 'null' ), + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'stock_quantity' => array( + 'description' => __( 'Stock quantity.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'in_stock' => array( + 'description' => __( 'Controls whether or not the variation is listed as "in stock" or "out of stock" on the frontend.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => true, + 'context' => array( 'view', 'edit' ), + ), + 'backorders' => array( + 'description' => __( 'If managing stock, this controls if backorders are allowed.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'no', + 'enum' => array( 'no', 'notify', 'yes' ), + 'context' => array( 'view', 'edit' ), + ), + 'backorders_allowed' => array( + 'description' => __( 'Shows if backorders are allowed.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'backordered' => array( + 'description' => __( 'Shows if the variation is on backordered.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'weight' => array( + /* translators: %s: weight unit */ + 'description' => sprintf( __( 'Variation weight (%s).', 'woocommerce' ), $weight_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'dimensions' => array( + 'description' => __( 'Variation dimensions.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'length' => array( + /* translators: %s: dimension unit */ + 'description' => sprintf( __( 'Variation length (%s).', 'woocommerce' ), $dimension_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'width' => array( + /* translators: %s: dimension unit */ + 'description' => sprintf( __( 'Variation width (%s).', 'woocommerce' ), $dimension_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'height' => array( + /* translators: %s: dimension unit */ + 'description' => sprintf( __( 'Variation height (%s).', 'woocommerce' ), $dimension_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'shipping_class' => array( + 'description' => __( 'Shipping class slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'shipping_class_id' => array( + 'description' => __( 'Shipping class ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'image' => array( + 'description' => __( 'Variation image data.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'id' => array( + 'description' => __( 'Image ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'date_created' => array( + 'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created_gmt' => array( + 'description' => __( 'The date the image was created, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified_gmt' => array( + 'description' => __( 'The date the image was last modified, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'src' => array( + 'description' => __( 'Image URL.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Image name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'alt' => array( + 'description' => __( 'Image alternative text.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'position' => array( + 'description' => __( 'Image position. 0 means that the image is featured.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'attributes' => array( + 'description' => __( 'List of attributes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Attribute ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Attribute name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'option' => array( + 'description' => __( 'Selected attribute term name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'menu_order' => array( + 'description' => __( 'Menu order, used to custom sort products.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'meta_data' => array( + 'description' => __( 'Meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Meta ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => array( 'string', 'null' ), + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-products-v2-controller.php b/includes/rest-api/Controllers/Version2/class-wc-rest-products-v2-controller.php new file mode 100644 index 00000000000..ad4dbd0807a --- /dev/null +++ b/includes/rest-api/Controllers/Version2/class-wc-rest-products-v2-controller.php @@ -0,0 +1,2209 @@ +post_type}_object", array( $this, 'clear_transients' ) ); + } + + /** + * Register the routes for products. + */ + public function register_routes() { + register_rest_route( + $this->namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P[\d]+)', + array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( + array( + 'default' => 'view', + ) + ), + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), + 'type' => 'boolean', + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/batch', + array( + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'batch_items' ), + 'permission_callback' => array( $this, 'batch_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_batch_schema' ), + ) + ); + } + + /** + * Get object. + * + * @param int $id Object ID. + * + * @since 3.0.0 + * @return WC_Data + */ + protected function get_object( $id ) { + return wc_get_product( $id ); + } + + /** + * Prepare a single product output for response. + * + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + * + * @since 3.0.0 + * @return WP_REST_Response + */ + public function prepare_object_for_response( $object, $request ) { + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->get_product_data( $object, $context ); + + // Add variations to variable products. + if ( $object->is_type( 'variable' ) && $object->has_child() ) { + $data['variations'] = $object->get_children(); + } + + // Add grouped products data. + if ( $object->is_type( 'grouped' ) && $object->has_child() ) { + $data['grouped_products'] = $object->get_children(); + } + + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + $response = rest_ensure_response( $data ); + $response->add_links( $this->prepare_links( $object, $request ) ); + + /** + * Filter the data for a response. + * + * The dynamic portion of the hook name, $this->post_type, + * refers to object type being prepared for the response. + * + * @param WP_REST_Response $response The response object. + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->post_type}_object", $response, $object, $request ); + } + + /** + * Prepare objects query. + * + * @param WP_REST_Request $request Full details about the request. + * + * @since 3.0.0 + * @return array + */ + protected function prepare_objects_query( $request ) { + $args = parent::prepare_objects_query( $request ); + + // Set post_status. + $args['post_status'] = $request['status']; + + // Taxonomy query to filter products by type, category, + // tag, shipping class, and attribute. + $tax_query = array(); + + // Map between taxonomy name and arg's key. + $taxonomies = array( + 'product_cat' => 'category', + 'product_tag' => 'tag', + 'product_shipping_class' => 'shipping_class', + ); + + // Set tax_query for each passed arg. + foreach ( $taxonomies as $taxonomy => $key ) { + if ( ! empty( $request[ $key ] ) ) { + $tax_query[] = array( + 'taxonomy' => $taxonomy, + 'field' => 'term_id', + 'terms' => $request[ $key ], + ); + } + } + + // Filter product type by slug. + if ( ! empty( $request['type'] ) ) { + $tax_query[] = array( + 'taxonomy' => 'product_type', + 'field' => 'slug', + 'terms' => $request['type'], + ); + } + + // Filter by attribute and term. + if ( ! empty( $request['attribute'] ) && ! empty( $request['attribute_term'] ) ) { + if ( in_array( $request['attribute'], wc_get_attribute_taxonomy_names(), true ) ) { + $tax_query[] = array( + 'taxonomy' => $request['attribute'], + 'field' => 'term_id', + 'terms' => $request['attribute_term'], + ); + } + } + + if ( ! empty( $tax_query ) ) { + $args['tax_query'] = $tax_query; // WPCS: slow query ok. + } + + // Filter featured. + if ( is_bool( $request['featured'] ) ) { + $args['tax_query'][] = array( + 'taxonomy' => 'product_visibility', + 'field' => 'name', + 'terms' => 'featured', + 'operator' => true === $request['featured'] ? 'IN' : 'NOT IN', + ); + } + + // Filter by sku. + if ( ! empty( $request['sku'] ) ) { + $skus = explode( ',', $request['sku'] ); + // Include the current string as a SKU too. + if ( 1 < count( $skus ) ) { + $skus[] = $request['sku']; + } + + $args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok. + $args, + array( + 'key' => '_sku', + 'value' => $skus, + 'compare' => 'IN', + ) + ); + } + + // Filter by tax class. + if ( ! empty( $request['tax_class'] ) ) { + $args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok. + $args, + array( + 'key' => '_tax_class', + 'value' => 'standard' !== $request['tax_class'] ? $request['tax_class'] : '', + ) + ); + } + + // Price filter. + if ( ! empty( $request['min_price'] ) || ! empty( $request['max_price'] ) ) { + $args['meta_query'] = $this->add_meta_query( $args, wc_get_min_max_price_meta_query( $request ) ); // WPCS: slow query ok. + } + + // Filter product in stock or out of stock. + if ( is_bool( $request['in_stock'] ) ) { + $args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok. + $args, + array( + 'key' => '_stock_status', + 'value' => true === $request['in_stock'] ? 'instock' : 'outofstock', + ) + ); + } + + // Filter by on sale products. + if ( is_bool( $request['on_sale'] ) ) { + $on_sale_key = $request['on_sale'] ? 'post__in' : 'post__not_in'; + $on_sale_ids = wc_get_product_ids_on_sale(); + + // Use 0 when there's no on sale products to avoid return all products. + $on_sale_ids = empty( $on_sale_ids ) ? array( 0 ) : $on_sale_ids; + + $args[ $on_sale_key ] += $on_sale_ids; + } + + // Force the post_type argument, since it's not a user input variable. + if ( ! empty( $request['sku'] ) ) { + $args['post_type'] = array( 'product', 'product_variation' ); + } else { + $args['post_type'] = $this->post_type; + } + + return $args; + } + + /** + * Get the downloads for a product or product variation. + * + * @param WC_Product|WC_Product_Variation $product Product instance. + * + * @return array + */ + protected function get_downloads( $product ) { + $downloads = array(); + + if ( $product->is_downloadable() ) { + foreach ( $product->get_downloads() as $file_id => $file ) { + $downloads[] = array( + 'id' => $file_id, // MD5 hash. + 'name' => $file['name'], + 'file' => $file['file'], + ); + } + } + + return $downloads; + } + + /** + * Get taxonomy terms. + * + * @param WC_Product $product Product instance. + * @param string $taxonomy Taxonomy slug. + * + * @return array + */ + protected function get_taxonomy_terms( $product, $taxonomy = 'cat' ) { + $terms = array(); + + foreach ( wc_get_object_terms( $product->get_id(), 'product_' . $taxonomy ) as $term ) { + $terms[] = array( + 'id' => $term->term_id, + 'name' => $term->name, + 'slug' => $term->slug, + ); + } + + return $terms; + } + + /** + * Get the images for a product or product variation. + * + * @param WC_Product|WC_Product_Variation $product Product instance. + * + * @return array + */ + protected function get_images( $product ) { + $images = array(); + $attachment_ids = array(); + + // Add featured image. + if ( $product->get_image_id() ) { + $attachment_ids[] = $product->get_image_id(); + } + + // Add gallery images. + $attachment_ids = array_merge( $attachment_ids, $product->get_gallery_image_ids() ); + + // Build image data. + foreach ( $attachment_ids as $position => $attachment_id ) { + $attachment_post = get_post( $attachment_id ); + if ( is_null( $attachment_post ) ) { + continue; + } + + $attachment = wp_get_attachment_image_src( $attachment_id, 'full' ); + if ( ! is_array( $attachment ) ) { + continue; + } + + $images[] = array( + 'id' => (int) $attachment_id, + 'date_created' => wc_rest_prepare_date_response( $attachment_post->post_date, false ), + 'date_created_gmt' => wc_rest_prepare_date_response( strtotime( $attachment_post->post_date_gmt ) ), + 'date_modified' => wc_rest_prepare_date_response( $attachment_post->post_modified, false ), + 'date_modified_gmt' => wc_rest_prepare_date_response( strtotime( $attachment_post->post_modified_gmt ) ), + 'src' => current( $attachment ), + 'name' => get_the_title( $attachment_id ), + 'alt' => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ), + 'position' => (int) $position, + ); + } + + // Set a placeholder image if the product has no images set. + if ( empty( $images ) ) { + $images[] = array( + 'id' => 0, + 'date_created' => wc_rest_prepare_date_response( current_time( 'mysql' ), false ), // Default to now. + 'date_created_gmt' => wc_rest_prepare_date_response( time() ), // Default to now. + 'date_modified' => wc_rest_prepare_date_response( current_time( 'mysql' ), false ), + 'date_modified_gmt' => wc_rest_prepare_date_response( time() ), + 'src' => wc_placeholder_img_src(), + 'name' => __( 'Placeholder', 'woocommerce' ), + 'alt' => __( 'Placeholder', 'woocommerce' ), + 'position' => 0, + ); + } + + return $images; + } + + /** + * Get attribute taxonomy label. + * + * @param string $name Taxonomy name. + * + * @deprecated 3.0.0 + * @return string + */ + protected function get_attribute_taxonomy_label( $name ) { + $tax = get_taxonomy( $name ); + $labels = get_taxonomy_labels( $tax ); + + return $labels->singular_name; + } + + /** + * Get product attribute taxonomy name. + * + * @param string $slug Taxonomy name. + * @param WC_Product $product Product data. + * + * @since 3.0.0 + * @return string + */ + protected function get_attribute_taxonomy_name( $slug, $product ) { + // Format slug so it matches attributes of the product. + $slug = wc_attribute_taxonomy_slug( $slug ); + $attributes = $product->get_attributes(); + $attribute = false; + + // pa_ attributes. + if ( isset( $attributes[ wc_attribute_taxonomy_name( $slug ) ] ) ) { + $attribute = $attributes[ wc_attribute_taxonomy_name( $slug ) ]; + } elseif ( isset( $attributes[ $slug ] ) ) { + $attribute = $attributes[ $slug ]; + } + + if ( ! $attribute ) { + return $slug; + } + + // Taxonomy attribute name. + if ( $attribute->is_taxonomy() ) { + $taxonomy = $attribute->get_taxonomy_object(); + return $taxonomy->attribute_label; + } + + // Custom product attribute name. + return $attribute->get_name(); + } + + /** + * Get default attributes. + * + * @param WC_Product $product Product instance. + * + * @return array + */ + protected function get_default_attributes( $product ) { + $default = array(); + + if ( $product->is_type( 'variable' ) ) { + foreach ( array_filter( (array) $product->get_default_attributes(), 'strlen' ) as $key => $value ) { + if ( 0 === strpos( $key, 'pa_' ) ) { + $default[] = array( + 'id' => wc_attribute_taxonomy_id_by_name( $key ), + 'name' => $this->get_attribute_taxonomy_name( $key, $product ), + 'option' => $value, + ); + } else { + $default[] = array( + 'id' => 0, + 'name' => $this->get_attribute_taxonomy_name( $key, $product ), + 'option' => $value, + ); + } + } + } + + return $default; + } + + /** + * Get attribute options. + * + * @param int $product_id Product ID. + * @param array $attribute Attribute data. + * + * @return array + */ + protected function get_attribute_options( $product_id, $attribute ) { + if ( isset( $attribute['is_taxonomy'] ) && $attribute['is_taxonomy'] ) { + return wc_get_product_terms( + $product_id, + $attribute['name'], + array( + 'fields' => 'names', + ) + ); + } elseif ( isset( $attribute['value'] ) ) { + return array_map( 'trim', explode( '|', $attribute['value'] ) ); + } + + return array(); + } + + /** + * Get the attributes for a product or product variation. + * + * @param WC_Product|WC_Product_Variation $product Product instance. + * + * @return array + */ + protected function get_attributes( $product ) { + $attributes = array(); + + if ( $product->is_type( 'variation' ) ) { + $_product = wc_get_product( $product->get_parent_id() ); + foreach ( $product->get_variation_attributes() as $attribute_name => $attribute ) { + $name = str_replace( 'attribute_', '', $attribute_name ); + + if ( empty( $attribute ) && '0' !== $attribute ) { + continue; + } + + // Taxonomy-based attributes are prefixed with `pa_`, otherwise simply `attribute_`. + if ( 0 === strpos( $attribute_name, 'attribute_pa_' ) ) { + $option_term = get_term_by( 'slug', $attribute, $name ); + $attributes[] = array( + 'id' => wc_attribute_taxonomy_id_by_name( $name ), + 'name' => $this->get_attribute_taxonomy_name( $name, $_product ), + 'option' => $option_term && ! is_wp_error( $option_term ) ? $option_term->name : $attribute, + ); + } else { + $attributes[] = array( + 'id' => 0, + 'name' => $this->get_attribute_taxonomy_name( $name, $_product ), + 'option' => $attribute, + ); + } + } + } else { + foreach ( $product->get_attributes() as $attribute ) { + $attributes[] = array( + 'id' => $attribute['is_taxonomy'] ? wc_attribute_taxonomy_id_by_name( $attribute['name'] ) : 0, + 'name' => $this->get_attribute_taxonomy_name( $attribute['name'], $product ), + 'position' => (int) $attribute['position'], + 'visible' => (bool) $attribute['is_visible'], + 'variation' => (bool) $attribute['is_variation'], + 'options' => $this->get_attribute_options( $product->get_id(), $attribute ), + ); + } + } + + return $attributes; + } + + /** + * Get product data. + * + * @param WC_Product $product Product instance. + * @param string $context Request context. + * Options: 'view' and 'edit'. + * + * @return array + */ + protected function get_product_data( $product, $context = 'view' ) { + $data = array( + 'id' => $product->get_id(), + 'name' => $product->get_name( $context ), + 'slug' => $product->get_slug( $context ), + 'permalink' => $product->get_permalink(), + 'date_created' => wc_rest_prepare_date_response( $product->get_date_created( $context ), false ), + 'date_created_gmt' => wc_rest_prepare_date_response( $product->get_date_created( $context ) ), + 'date_modified' => wc_rest_prepare_date_response( $product->get_date_modified( $context ), false ), + 'date_modified_gmt' => wc_rest_prepare_date_response( $product->get_date_modified( $context ) ), + 'type' => $product->get_type(), + 'status' => $product->get_status( $context ), + 'featured' => $product->is_featured(), + 'catalog_visibility' => $product->get_catalog_visibility( $context ), + 'description' => 'view' === $context ? wpautop( do_shortcode( $product->get_description() ) ) : $product->get_description( $context ), + 'short_description' => 'view' === $context ? apply_filters( 'woocommerce_short_description', $product->get_short_description() ) : $product->get_short_description( $context ), + 'sku' => $product->get_sku( $context ), + 'price' => $product->get_price( $context ), + 'regular_price' => $product->get_regular_price( $context ), + 'sale_price' => $product->get_sale_price( $context ) ? $product->get_sale_price( $context ) : '', + 'date_on_sale_from' => wc_rest_prepare_date_response( $product->get_date_on_sale_from( $context ), false ), + 'date_on_sale_from_gmt' => wc_rest_prepare_date_response( $product->get_date_on_sale_from( $context ) ), + 'date_on_sale_to' => wc_rest_prepare_date_response( $product->get_date_on_sale_to( $context ), false ), + 'date_on_sale_to_gmt' => wc_rest_prepare_date_response( $product->get_date_on_sale_to( $context ) ), + 'price_html' => $product->get_price_html(), + 'on_sale' => $product->is_on_sale( $context ), + 'purchasable' => $product->is_purchasable(), + 'total_sales' => $product->get_total_sales( $context ), + 'virtual' => $product->is_virtual(), + 'downloadable' => $product->is_downloadable(), + 'downloads' => $this->get_downloads( $product ), + 'download_limit' => $product->get_download_limit( $context ), + 'download_expiry' => $product->get_download_expiry( $context ), + 'external_url' => $product->is_type( 'external' ) ? $product->get_product_url( $context ) : '', + 'button_text' => $product->is_type( 'external' ) ? $product->get_button_text( $context ) : '', + 'tax_status' => $product->get_tax_status( $context ), + 'tax_class' => $product->get_tax_class( $context ), + 'manage_stock' => $product->managing_stock(), + 'stock_quantity' => $product->get_stock_quantity( $context ), + 'in_stock' => $product->is_in_stock(), + 'backorders' => $product->get_backorders( $context ), + 'backorders_allowed' => $product->backorders_allowed(), + 'backordered' => $product->is_on_backorder(), + 'sold_individually' => $product->is_sold_individually(), + 'weight' => $product->get_weight( $context ), + 'dimensions' => array( + 'length' => $product->get_length( $context ), + 'width' => $product->get_width( $context ), + 'height' => $product->get_height( $context ), + ), + 'shipping_required' => $product->needs_shipping(), + 'shipping_taxable' => $product->is_shipping_taxable(), + 'shipping_class' => $product->get_shipping_class(), + 'shipping_class_id' => $product->get_shipping_class_id( $context ), + 'reviews_allowed' => $product->get_reviews_allowed( $context ), + 'average_rating' => 'view' === $context ? wc_format_decimal( $product->get_average_rating(), 2 ) : $product->get_average_rating( $context ), + 'rating_count' => $product->get_rating_count(), + 'related_ids' => array_map( 'absint', array_values( wc_get_related_products( $product->get_id() ) ) ), + 'upsell_ids' => array_map( 'absint', $product->get_upsell_ids( $context ) ), + 'cross_sell_ids' => array_map( 'absint', $product->get_cross_sell_ids( $context ) ), + 'parent_id' => $product->get_parent_id( $context ), + 'purchase_note' => 'view' === $context ? wpautop( do_shortcode( wp_kses_post( $product->get_purchase_note() ) ) ) : $product->get_purchase_note( $context ), + 'categories' => $this->get_taxonomy_terms( $product ), + 'tags' => $this->get_taxonomy_terms( $product, 'tag' ), + 'images' => $this->get_images( $product ), + 'attributes' => $this->get_attributes( $product ), + 'default_attributes' => $this->get_default_attributes( $product ), + 'variations' => array(), + 'grouped_products' => array(), + 'menu_order' => $product->get_menu_order( $context ), + 'meta_data' => $product->get_meta_data(), + ); + + return $data; + } + + /** + * Prepare links for the request. + * + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + * + * @return array Links for the given post. + */ + protected function prepare_links( $object, $request ) { + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $object->get_id() ) ), // @codingStandardsIgnoreLine. + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), // @codingStandardsIgnoreLine. + ), + ); + + if ( $object->get_parent_id() ) { + $links['up'] = array( + 'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $object->get_parent_id() ) ), // @codingStandardsIgnoreLine. + ); + } + + return $links; + } + + /** + * Prepare a single product for create or update. + * + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + * + * @return WP_Error|WC_Data + */ + protected function prepare_object_for_database( $request, $creating = false ) { + $id = isset( $request['id'] ) ? absint( $request['id'] ) : 0; + + // Type is the most important part here because we need to be using the correct class and methods. + if ( isset( $request['type'] ) ) { + $classname = WC_Product_Factory::get_classname_from_product_type( $request['type'] ); + + if ( ! class_exists( $classname ) ) { + $classname = 'WC_Product_Simple'; + } + + $product = new $classname( $id ); + } elseif ( isset( $request['id'] ) ) { + $product = wc_get_product( $id ); + } else { + $product = new WC_Product_Simple(); + } + + if ( 'variation' === $product->get_type() ) { + return new WP_Error( + "woocommerce_rest_invalid_{$this->post_type}_id", + __( 'To manipulate product variations you should use the /products/<product_id>/variations/<id> endpoint.', 'woocommerce' ), + array( + 'status' => 404, + ) + ); + } + + // Post title. + if ( isset( $request['name'] ) ) { + $product->set_name( wp_filter_post_kses( $request['name'] ) ); + } + + // Post content. + if ( isset( $request['description'] ) ) { + $product->set_description( wp_filter_post_kses( $request['description'] ) ); + } + + // Post excerpt. + if ( isset( $request['short_description'] ) ) { + $product->set_short_description( wp_filter_post_kses( $request['short_description'] ) ); + } + + // Post status. + if ( isset( $request['status'] ) ) { + $product->set_status( get_post_status_object( $request['status'] ) ? $request['status'] : 'draft' ); + } + + // Post slug. + if ( isset( $request['slug'] ) ) { + $product->set_slug( $request['slug'] ); + } + + // Menu order. + if ( isset( $request['menu_order'] ) ) { + $product->set_menu_order( $request['menu_order'] ); + } + + // Comment status. + if ( isset( $request['reviews_allowed'] ) ) { + $product->set_reviews_allowed( $request['reviews_allowed'] ); + } + + // Virtual. + if ( isset( $request['virtual'] ) ) { + $product->set_virtual( $request['virtual'] ); + } + + // Tax status. + if ( isset( $request['tax_status'] ) ) { + $product->set_tax_status( $request['tax_status'] ); + } + + // Tax Class. + if ( isset( $request['tax_class'] ) ) { + $product->set_tax_class( $request['tax_class'] ); + } + + // Catalog Visibility. + if ( isset( $request['catalog_visibility'] ) ) { + $product->set_catalog_visibility( $request['catalog_visibility'] ); + } + + // Purchase Note. + if ( isset( $request['purchase_note'] ) ) { + $product->set_purchase_note( wp_kses_post( wp_unslash( $request['purchase_note'] ) ) ); + } + + // Featured Product. + if ( isset( $request['featured'] ) ) { + $product->set_featured( $request['featured'] ); + } + + // Shipping data. + $product = $this->save_product_shipping_data( $product, $request ); + + // SKU. + if ( isset( $request['sku'] ) ) { + $product->set_sku( wc_clean( $request['sku'] ) ); + } + + // Attributes. + if ( isset( $request['attributes'] ) ) { + $attributes = array(); + + foreach ( $request['attributes'] as $attribute ) { + $attribute_id = 0; + $attribute_name = ''; + + // Check ID for global attributes or name for product attributes. + if ( ! empty( $attribute['id'] ) ) { + $attribute_id = absint( $attribute['id'] ); + $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); + } elseif ( ! empty( $attribute['name'] ) ) { + $attribute_name = wc_clean( $attribute['name'] ); + } + + if ( ! $attribute_id && ! $attribute_name ) { + continue; + } + + if ( $attribute_id ) { + + if ( isset( $attribute['options'] ) ) { + $options = $attribute['options']; + + if ( ! is_array( $attribute['options'] ) ) { + // Text based attributes - Posted values are term names. + $options = explode( WC_DELIMITER, $options ); + } + + $values = array_map( 'wc_sanitize_term_text_based', $options ); + $values = array_filter( $values, 'strlen' ); + } else { + $values = array(); + } + + if ( ! empty( $values ) ) { + // Add attribute to array, but don't set values. + $attribute_object = new WC_Product_Attribute(); + $attribute_object->set_id( $attribute_id ); + $attribute_object->set_name( $attribute_name ); + $attribute_object->set_options( $values ); + $attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' ); + $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); + $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); + $attributes[] = $attribute_object; + } + } elseif ( isset( $attribute['options'] ) ) { + // Custom attribute - Add attribute to array and set the values. + if ( is_array( $attribute['options'] ) ) { + $values = $attribute['options']; + } else { + $values = explode( WC_DELIMITER, $attribute['options'] ); + } + $attribute_object = new WC_Product_Attribute(); + $attribute_object->set_name( $attribute_name ); + $attribute_object->set_options( $values ); + $attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' ); + $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); + $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); + $attributes[] = $attribute_object; + } + } + $product->set_attributes( $attributes ); + } + + // Sales and prices. + if ( in_array( $product->get_type(), array( 'variable', 'grouped' ), true ) ) { + $product->set_regular_price( '' ); + $product->set_sale_price( '' ); + $product->set_date_on_sale_to( '' ); + $product->set_date_on_sale_from( '' ); + $product->set_price( '' ); + } else { + // Regular Price. + if ( isset( $request['regular_price'] ) ) { + $product->set_regular_price( $request['regular_price'] ); + } + + // Sale Price. + if ( isset( $request['sale_price'] ) ) { + $product->set_sale_price( $request['sale_price'] ); + } + + if ( isset( $request['date_on_sale_from'] ) ) { + $product->set_date_on_sale_from( $request['date_on_sale_from'] ); + } + + if ( isset( $request['date_on_sale_from_gmt'] ) ) { + $product->set_date_on_sale_from( $request['date_on_sale_from_gmt'] ? strtotime( $request['date_on_sale_from_gmt'] ) : null ); + } + + if ( isset( $request['date_on_sale_to'] ) ) { + $product->set_date_on_sale_to( $request['date_on_sale_to'] ); + } + + if ( isset( $request['date_on_sale_to_gmt'] ) ) { + $product->set_date_on_sale_to( $request['date_on_sale_to_gmt'] ? strtotime( $request['date_on_sale_to_gmt'] ) : null ); + } + } + + // Product parent ID. + if ( isset( $request['parent_id'] ) ) { + $product->set_parent_id( $request['parent_id'] ); + } + + // Sold individually. + if ( isset( $request['sold_individually'] ) ) { + $product->set_sold_individually( $request['sold_individually'] ); + } + + // Stock status. + if ( isset( $request['in_stock'] ) ) { + $stock_status = true === $request['in_stock'] ? 'instock' : 'outofstock'; + } else { + $stock_status = $product->get_stock_status(); + } + + // Stock data. + if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) { + // Manage stock. + if ( isset( $request['manage_stock'] ) ) { + $product->set_manage_stock( $request['manage_stock'] ); + } + + // Backorders. + if ( isset( $request['backorders'] ) ) { + $product->set_backorders( $request['backorders'] ); + } + + if ( $product->is_type( 'grouped' ) ) { + $product->set_manage_stock( 'no' ); + $product->set_backorders( 'no' ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( $stock_status ); + } elseif ( $product->is_type( 'external' ) ) { + $product->set_manage_stock( 'no' ); + $product->set_backorders( 'no' ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( 'instock' ); + } elseif ( $product->get_manage_stock() ) { + // Stock status is always determined by children so sync later. + if ( ! $product->is_type( 'variable' ) ) { + $product->set_stock_status( $stock_status ); + } + + // Stock quantity. + if ( isset( $request['stock_quantity'] ) ) { + $product->set_stock_quantity( wc_stock_amount( $request['stock_quantity'] ) ); + } elseif ( isset( $request['inventory_delta'] ) ) { + $stock_quantity = wc_stock_amount( $product->get_stock_quantity() ); + $stock_quantity += wc_stock_amount( $request['inventory_delta'] ); + $product->set_stock_quantity( wc_stock_amount( $stock_quantity ) ); + } + } else { + // Don't manage stock. + $product->set_manage_stock( 'no' ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( $stock_status ); + } + } elseif ( ! $product->is_type( 'variable' ) ) { + $product->set_stock_status( $stock_status ); + } + + // Upsells. + if ( isset( $request['upsell_ids'] ) ) { + $upsells = array(); + $ids = $request['upsell_ids']; + + if ( ! empty( $ids ) ) { + foreach ( $ids as $id ) { + if ( $id && $id > 0 ) { + $upsells[] = $id; + } + } + } + + $product->set_upsell_ids( $upsells ); + } + + // Cross sells. + if ( isset( $request['cross_sell_ids'] ) ) { + $crosssells = array(); + $ids = $request['cross_sell_ids']; + + if ( ! empty( $ids ) ) { + foreach ( $ids as $id ) { + if ( $id && $id > 0 ) { + $crosssells[] = $id; + } + } + } + + $product->set_cross_sell_ids( $crosssells ); + } + + // Product categories. + if ( isset( $request['categories'] ) && is_array( $request['categories'] ) ) { + $product = $this->save_taxonomy_terms( $product, $request['categories'] ); + } + + // Product tags. + if ( isset( $request['tags'] ) && is_array( $request['tags'] ) ) { + $product = $this->save_taxonomy_terms( $product, $request['tags'], 'tag' ); + } + + // Downloadable. + if ( isset( $request['downloadable'] ) ) { + $product->set_downloadable( $request['downloadable'] ); + } + + // Downloadable options. + if ( $product->get_downloadable() ) { + + // Downloadable files. + if ( isset( $request['downloads'] ) && is_array( $request['downloads'] ) ) { + $product = $this->save_downloadable_files( $product, $request['downloads'] ); + } + + // Download limit. + if ( isset( $request['download_limit'] ) ) { + $product->set_download_limit( $request['download_limit'] ); + } + + // Download expiry. + if ( isset( $request['download_expiry'] ) ) { + $product->set_download_expiry( $request['download_expiry'] ); + } + } + + // Product url and button text for external products. + if ( $product->is_type( 'external' ) ) { + if ( isset( $request['external_url'] ) ) { + $product->set_product_url( $request['external_url'] ); + } + + if ( isset( $request['button_text'] ) ) { + $product->set_button_text( $request['button_text'] ); + } + } + + // Save default attributes for variable products. + if ( $product->is_type( 'variable' ) ) { + $product = $this->save_default_attributes( $product, $request ); + } + + // Set children for a grouped product. + if ( $product->is_type( 'grouped' ) && isset( $request['grouped_products'] ) ) { + $product->set_children( $request['grouped_products'] ); + } + + // Check for featured/gallery images, upload it and set it. + if ( isset( $request['images'] ) ) { + $product = $this->set_product_images( $product, $request['images'] ); + } + + // Allow set meta_data. + if ( is_array( $request['meta_data'] ) ) { + foreach ( $request['meta_data'] as $meta ) { + $product->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); + } + } + + /** + * Filters an object before it is inserted via the REST API. + * + * The dynamic portion of the hook name, `$this->post_type`, + * refers to the object type slug. + * + * @param WC_Data $product Object object. + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + */ + return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $product, $request, $creating ); + } + + /** + * Set product images. + * + * @param WC_Product $product Product instance. + * @param array $images Images data. + * + * @throws WC_REST_Exception REST API exceptions. + * @return WC_Product + */ + protected function set_product_images( $product, $images ) { + $images = is_array( $images ) ? array_filter( $images ) : array(); + + if ( ! empty( $images ) ) { + $gallery_positions = array(); + + foreach ( $images as $index => $image ) { + $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; + + if ( 0 === $attachment_id && isset( $image['src'] ) ) { + $upload = wc_rest_upload_image_from_url( esc_url_raw( $image['src'] ) ); + + if ( is_wp_error( $upload ) ) { + if ( ! apply_filters( 'woocommerce_rest_suppress_image_upload_error', false, $upload, $product->get_id(), $images ) ) { + throw new WC_REST_Exception( 'woocommerce_product_image_upload_error', $upload->get_error_message(), 400 ); + } else { + continue; + } + } + + $attachment_id = wc_rest_set_uploaded_image_as_attachment( $upload, $product->get_id() ); + } + + if ( ! wp_attachment_is_image( $attachment_id ) ) { + /* translators: %s: attachment id */ + throw new WC_REST_Exception( 'woocommerce_product_invalid_image_id', sprintf( __( '#%s is an invalid image ID.', 'woocommerce' ), $attachment_id ), 400 ); + } + + $gallery_positions[ $attachment_id ] = absint( isset( $image['position'] ) ? $image['position'] : $index ); + + // Set the image alt if present. + if ( ! empty( $image['alt'] ) ) { + update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) ); + } + + // Set the image name if present. + if ( ! empty( $image['name'] ) ) { + wp_update_post( + array( + 'ID' => $attachment_id, + 'post_title' => $image['name'], + ) + ); + } + + // Set the image source if present, for future reference. + if ( ! empty( $image['src'] ) ) { + update_post_meta( $attachment_id, '_wc_attachment_source', esc_url_raw( $image['src'] ) ); + } + } + + // Sort images and get IDs in correct order. + asort( $gallery_positions ); + + // Get gallery in correct order. + $gallery = array_keys( $gallery_positions ); + + // Featured image is in position 0. + $image_id = array_shift( $gallery ); + + // Set images. + $product->set_image_id( $image_id ); + $product->set_gallery_image_ids( $gallery ); + } else { + $product->set_image_id( '' ); + $product->set_gallery_image_ids( array() ); + } + + return $product; + } + + /** + * Save product shipping data. + * + * @param WC_Product $product Product instance. + * @param array $data Shipping data. + * + * @return WC_Product + */ + protected function save_product_shipping_data( $product, $data ) { + // Virtual. + if ( isset( $data['virtual'] ) && true === $data['virtual'] ) { + $product->set_weight( '' ); + $product->set_height( '' ); + $product->set_length( '' ); + $product->set_width( '' ); + } else { + if ( isset( $data['weight'] ) ) { + $product->set_weight( $data['weight'] ); + } + + // Height. + if ( isset( $data['dimensions']['height'] ) ) { + $product->set_height( $data['dimensions']['height'] ); + } + + // Width. + if ( isset( $data['dimensions']['width'] ) ) { + $product->set_width( $data['dimensions']['width'] ); + } + + // Length. + if ( isset( $data['dimensions']['length'] ) ) { + $product->set_length( $data['dimensions']['length'] ); + } + } + + // Shipping class. + if ( isset( $data['shipping_class'] ) ) { + $data_store = $product->get_data_store(); + $shipping_class_id = $data_store->get_shipping_class_id_by_slug( wc_clean( $data['shipping_class'] ) ); + $product->set_shipping_class_id( $shipping_class_id ); + } + + return $product; + } + + /** + * Save downloadable files. + * + * @param WC_Product $product Product instance. + * @param array $downloads Downloads data. + * @param int $deprecated Deprecated since 3.0. + * + * @return WC_Product + */ + protected function save_downloadable_files( $product, $downloads, $deprecated = 0 ) { + if ( $deprecated ) { + wc_deprecated_argument( 'variation_id', '3.0', 'save_downloadable_files() not requires a variation_id anymore.' ); + } + + $files = array(); + foreach ( $downloads as $key => $file ) { + if ( empty( $file['file'] ) ) { + continue; + } + + $download = new WC_Product_Download(); + $download->set_id( ! empty( $file['id'] ) ? $file['id'] : wp_generate_uuid4() ); + $download->set_name( $file['name'] ? $file['name'] : wc_get_filename_from_url( $file['file'] ) ); + $download->set_file( apply_filters( 'woocommerce_file_download_path', $file['file'], $product, $key ) ); + $files[] = $download; + } + $product->set_downloads( $files ); + + return $product; + } + + /** + * Save taxonomy terms. + * + * @param WC_Product $product Product instance. + * @param array $terms Terms data. + * @param string $taxonomy Taxonomy name. + * + * @return WC_Product + */ + protected function save_taxonomy_terms( $product, $terms, $taxonomy = 'cat' ) { + $term_ids = wp_list_pluck( $terms, 'id' ); + + if ( 'cat' === $taxonomy ) { + $product->set_category_ids( $term_ids ); + } elseif ( 'tag' === $taxonomy ) { + $product->set_tag_ids( $term_ids ); + } + + return $product; + } + + /** + * Save default attributes. + * + * @param WC_Product $product Product instance. + * @param WP_REST_Request $request Request data. + * + * @since 3.0.0 + * @return WC_Product + */ + protected function save_default_attributes( $product, $request ) { + if ( isset( $request['default_attributes'] ) && is_array( $request['default_attributes'] ) ) { + + $attributes = $product->get_attributes(); + $default_attributes = array(); + + foreach ( $request['default_attributes'] as $attribute ) { + $attribute_id = 0; + $attribute_name = ''; + + // Check ID for global attributes or name for product attributes. + if ( ! empty( $attribute['id'] ) ) { + $attribute_id = absint( $attribute['id'] ); + $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); + } elseif ( ! empty( $attribute['name'] ) ) { + $attribute_name = sanitize_title( $attribute['name'] ); + } + + if ( ! $attribute_id && ! $attribute_name ) { + continue; + } + + if ( isset( $attributes[ $attribute_name ] ) ) { + $_attribute = $attributes[ $attribute_name ]; + + if ( $_attribute['is_variation'] ) { + $value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : ''; + + if ( ! empty( $_attribute['is_taxonomy'] ) ) { + // If dealing with a taxonomy, we need to get the slug from the name posted to the API. + $term = get_term_by( 'name', $value, $attribute_name ); + + if ( $term && ! is_wp_error( $term ) ) { + $value = $term->slug; + } else { + $value = sanitize_title( $value ); + } + } + + if ( $value ) { + $default_attributes[ $attribute_name ] = $value; + } + } + } + } + + $product->set_default_attributes( $default_attributes ); + } + + return $product; + } + + /** + * Clear caches here so in sync with any new variations/children. + * + * @param WC_Data $object Object data. + */ + public function clear_transients( $object ) { + wc_delete_product_transients( $object->get_id() ); + wp_cache_delete( 'product-' . $object->get_id(), 'products' ); + } + + /** + * Delete a single item. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return WP_REST_Response|WP_Error + */ + public function delete_item( $request ) { + $id = (int) $request['id']; + $force = (bool) $request['force']; + $object = $this->get_object( (int) $request['id'] ); + $result = false; + + if ( ! $object || 0 === $object->get_id() ) { + return new WP_Error( + "woocommerce_rest_{$this->post_type}_invalid_id", + __( 'Invalid ID.', 'woocommerce' ), + array( + 'status' => 404, + ) + ); + } + + if ( 'variation' === $object->get_type() ) { + return new WP_Error( + "woocommerce_rest_invalid_{$this->post_type}_id", + __( 'To manipulate product variations you should use the /products/<product_id>/variations/<id> endpoint.', 'woocommerce' ), + array( + 'status' => 404, + ) + ); + } + + $supports_trash = EMPTY_TRASH_DAYS > 0 && is_callable( array( $object, 'get_status' ) ); + + /** + * Filter whether an object is trashable. + * + * Return false to disable trash support for the object. + * + * @param boolean $supports_trash Whether the object type support trashing. + * @param WC_Data $object The object being considered for trashing support. + */ + $supports_trash = apply_filters( "woocommerce_rest_{$this->post_type}_object_trashable", $supports_trash, $object ); + + if ( ! wc_rest_check_post_permissions( $this->post_type, 'delete', $object->get_id() ) ) { + return new WP_Error( + "woocommerce_rest_user_cannot_delete_{$this->post_type}", + /* translators: %s: post type */ + sprintf( __( 'Sorry, you are not allowed to delete %s.', 'woocommerce' ), $this->post_type ), + array( + 'status' => rest_authorization_required_code(), + ) + ); + } + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_object_for_response( $object, $request ); + + // If we're forcing, then delete permanently. + if ( $force ) { + if ( $object->is_type( 'variable' ) ) { + foreach ( $object->get_children() as $child_id ) { + $child = wc_get_product( $child_id ); + if ( ! empty( $child ) ) { + $child->delete( true ); + } + } + } else { + // For other product types, if the product has children, remove the relationship. + foreach ( $object->get_children() as $child_id ) { + $child = wc_get_product( $child_id ); + if ( ! empty( $child ) ) { + $child->set_parent_id( 0 ); + $child->save(); + } + } + } + + $object->delete( true ); + $result = 0 === $object->get_id(); + } else { + // If we don't support trashing for this type, error out. + if ( ! $supports_trash ) { + return new WP_Error( + 'woocommerce_rest_trash_not_supported', + /* translators: %s: post type */ + sprintf( __( 'The %s does not support trashing.', 'woocommerce' ), $this->post_type ), + array( + 'status' => 501, + ) + ); + } + + // Otherwise, only trash if we haven't already. + if ( is_callable( array( $object, 'get_status' ) ) ) { + if ( 'trash' === $object->get_status() ) { + return new WP_Error( + 'woocommerce_rest_already_trashed', + /* translators: %s: post type */ + sprintf( __( 'The %s has already been deleted.', 'woocommerce' ), $this->post_type ), + array( + 'status' => 410, + ) + ); + } + + $object->delete(); + $result = 'trash' === $object->get_status(); + } + } + + if ( ! $result ) { + return new WP_Error( + 'woocommerce_rest_cannot_delete', + /* translators: %s: post type */ + sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), $this->post_type ), + array( + 'status' => 500, + ) + ); + } + + // Delete parent product transients. + if ( 0 !== $object->get_parent_id() ) { + wc_delete_product_transients( $object->get_parent_id() ); + } + + /** + * Fires after a single object is deleted or trashed via the REST API. + * + * @param WC_Data $object The deleted or trashed object. + * @param WP_REST_Response $response The response data. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( "woocommerce_rest_delete_{$this->post_type}_object", $object, $response, $request ); + + return $response; + } + + /** + * Get the Product's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $weight_unit = get_option( 'woocommerce_weight_unit' ); + $dimension_unit = get_option( 'woocommerce_dimension_unit' ); + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => $this->post_type, + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Product name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'slug' => array( + 'description' => __( 'Product slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'permalink' => array( + 'description' => __( 'Product URL.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( "The date the product was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created_gmt' => array( + 'description' => __( 'The date the product was created, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the product was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified_gmt' => array( + 'description' => __( 'The date the product was last modified, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'type' => array( + 'description' => __( 'Product type.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'simple', + 'enum' => array_keys( wc_get_product_types() ), + 'context' => array( 'view', 'edit' ), + ), + 'status' => array( + 'description' => __( 'Product status (post status).', 'woocommerce' ), + 'type' => 'string', + 'default' => 'publish', + 'enum' => array_merge( array_keys( get_post_statuses() ), array( 'future' ) ), + 'context' => array( 'view', 'edit' ), + ), + 'featured' => array( + 'description' => __( 'Featured product.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'catalog_visibility' => array( + 'description' => __( 'Catalog visibility.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'visible', + 'enum' => array( 'visible', 'catalog', 'search', 'hidden' ), + 'context' => array( 'view', 'edit' ), + ), + 'description' => array( + 'description' => __( 'Product description.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'short_description' => array( + 'description' => __( 'Product short description.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'sku' => array( + 'description' => __( 'Unique identifier.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'price' => array( + 'description' => __( 'Current product price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'regular_price' => array( + 'description' => __( 'Product regular price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'sale_price' => array( + 'description' => __( 'Product sale price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_from' => array( + 'description' => __( "Start date of sale price, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_from_gmt' => array( + 'description' => __( 'Start date of sale price, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_to' => array( + 'description' => __( "End date of sale price, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_to_gmt' => array( + 'description' => __( 'End date of sale price, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'price_html' => array( + 'description' => __( 'Price formatted in HTML.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'on_sale' => array( + 'description' => __( 'Shows if the product is on sale.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'purchasable' => array( + 'description' => __( 'Shows if the product can be bought.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total_sales' => array( + 'description' => __( 'Amount of sales.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'virtual' => array( + 'description' => __( 'If the product is virtual.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'downloadable' => array( + 'description' => __( 'If the product is downloadable.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'downloads' => array( + 'description' => __( 'List of downloadable files.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'File ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'File name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'file' => array( + 'description' => __( 'File URL.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'download_limit' => array( + 'description' => __( 'Number of times downloadable files can be downloaded after purchase.', 'woocommerce' ), + 'type' => 'integer', + 'default' => -1, + 'context' => array( 'view', 'edit' ), + ), + 'download_expiry' => array( + 'description' => __( 'Number of days until access to downloadable files expires.', 'woocommerce' ), + 'type' => 'integer', + 'default' => -1, + 'context' => array( 'view', 'edit' ), + ), + 'external_url' => array( + 'description' => __( 'Product external URL. Only for external products.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + ), + 'button_text' => array( + 'description' => __( 'Product external button text. Only for external products.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'tax_status' => array( + 'description' => __( 'Tax status.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'taxable', + 'enum' => array( 'taxable', 'shipping', 'none' ), + 'context' => array( 'view', 'edit' ), + ), + 'tax_class' => array( + 'description' => __( 'Tax class.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'manage_stock' => array( + 'description' => __( 'Stock management at product level.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'stock_quantity' => array( + 'description' => __( 'Stock quantity.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'in_stock' => array( + 'description' => __( 'Controls whether or not the product is listed as "in stock" or "out of stock" on the frontend.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => true, + 'context' => array( 'view', 'edit' ), + ), + 'backorders' => array( + 'description' => __( 'If managing stock, this controls if backorders are allowed.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'no', + 'enum' => array( 'no', 'notify', 'yes' ), + 'context' => array( 'view', 'edit' ), + ), + 'backorders_allowed' => array( + 'description' => __( 'Shows if backorders are allowed.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'backordered' => array( + 'description' => __( 'Shows if the product is on backordered.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'sold_individually' => array( + 'description' => __( 'Allow one item to be bought in a single order.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'weight' => array( + /* translators: %s: weight unit */ + 'description' => sprintf( __( 'Product weight (%s).', 'woocommerce' ), $weight_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'dimensions' => array( + 'description' => __( 'Product dimensions.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'length' => array( + /* translators: %s: dimension unit */ + 'description' => sprintf( __( 'Product length (%s).', 'woocommerce' ), $dimension_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'width' => array( + /* translators: %s: dimension unit */ + 'description' => sprintf( __( 'Product width (%s).', 'woocommerce' ), $dimension_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'height' => array( + /* translators: %s: dimension unit */ + 'description' => sprintf( __( 'Product height (%s).', 'woocommerce' ), $dimension_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'shipping_required' => array( + 'description' => __( 'Shows if the product need to be shipped.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'shipping_taxable' => array( + 'description' => __( 'Shows whether or not the product shipping is taxable.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'shipping_class' => array( + 'description' => __( 'Shipping class slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'shipping_class_id' => array( + 'description' => __( 'Shipping class ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'reviews_allowed' => array( + 'description' => __( 'Allow reviews.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => true, + 'context' => array( 'view', 'edit' ), + ), + 'average_rating' => array( + 'description' => __( 'Reviews average rating.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'rating_count' => array( + 'description' => __( 'Amount of reviews that the product have.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'related_ids' => array( + 'description' => __( 'List of related products IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'upsell_ids' => array( + 'description' => __( 'List of up-sell products IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + ), + 'cross_sell_ids' => array( + 'description' => __( 'List of cross-sell products IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + ), + 'parent_id' => array( + 'description' => __( 'Product parent ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'purchase_note' => array( + 'description' => __( 'Optional note to send the customer after purchase.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'categories' => array( + 'description' => __( 'List of categories.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Category ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Category name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'slug' => array( + 'description' => __( 'Category slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + 'tags' => array( + 'description' => __( 'List of tags.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Tag ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Tag name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'slug' => array( + 'description' => __( 'Tag slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + 'images' => array( + 'description' => __( 'List of images.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Image ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'date_created' => array( + 'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created_gmt' => array( + 'description' => __( 'The date the image was created, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified_gmt' => array( + 'description' => __( 'The date the image was last modified, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'src' => array( + 'description' => __( 'Image URL.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Image name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'alt' => array( + 'description' => __( 'Image alternative text.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'position' => array( + 'description' => __( 'Image position. 0 means that the image is featured.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'attributes' => array( + 'description' => __( 'List of attributes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Attribute ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Attribute name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'position' => array( + 'description' => __( 'Attribute position.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'visible' => array( + 'description' => __( "Define if the attribute is visible on the \"Additional information\" tab in the product's page.", 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'variation' => array( + 'description' => __( 'Define if the attribute can be used as variation.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'options' => array( + 'description' => __( 'List of available term names of the attribute.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'string', + ), + ), + ), + ), + ), + 'default_attributes' => array( + 'description' => __( 'Defaults variation attributes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Attribute ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Attribute name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'option' => array( + 'description' => __( 'Selected attribute term name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'variations' => array( + 'description' => __( 'List of variations IDs.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'integer', + ), + 'readonly' => true, + ), + 'grouped_products' => array( + 'description' => __( 'List of grouped products ID.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + ), + 'menu_order' => array( + 'description' => __( 'Menu order, used to custom sort products.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'meta_data' => array( + 'description' => __( 'Meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Meta ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => array( 'string', 'null' ), + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections of attachments. + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + $params['orderby']['enum'] = array_merge( $params['orderby']['enum'], array( 'menu_order' ) ); + + $params['slug'] = array( + 'description' => __( 'Limit result set to products with a specific slug.', 'woocommerce' ), + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['status'] = array( + 'default' => 'any', + 'description' => __( 'Limit result set to products assigned a specific status.', 'woocommerce' ), + 'type' => 'string', + 'enum' => array_merge( array( 'any', 'future', 'trash' ), array_keys( get_post_statuses() ) ), + 'sanitize_callback' => 'sanitize_key', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['type'] = array( + 'description' => __( 'Limit result set to products assigned a specific type.', 'woocommerce' ), + 'type' => 'string', + 'enum' => array_keys( wc_get_product_types() ), + 'sanitize_callback' => 'sanitize_key', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['sku'] = array( + 'description' => __( 'Limit result set to products with specific SKU(s). Use commas to separate.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['featured'] = array( + 'description' => __( 'Limit result set to featured products.', 'woocommerce' ), + 'type' => 'boolean', + 'sanitize_callback' => 'wc_string_to_bool', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['category'] = array( + 'description' => __( 'Limit result set to products assigned a specific category ID.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'wp_parse_id_list', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['tag'] = array( + 'description' => __( 'Limit result set to products assigned a specific tag ID.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'wp_parse_id_list', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['shipping_class'] = array( + 'description' => __( 'Limit result set to products assigned a specific shipping class ID.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'wp_parse_id_list', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['attribute'] = array( + 'description' => __( 'Limit result set to products with a specific attribute. Use the taxonomy name/attribute slug.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['attribute_term'] = array( + 'description' => __( 'Limit result set to products with a specific attribute term ID (required an assigned attribute).', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'wp_parse_id_list', + 'validate_callback' => 'rest_validate_request_arg', + ); + + if ( wc_tax_enabled() ) { + $params['tax_class'] = array( + 'description' => __( 'Limit result set to products with a specific tax class.', 'woocommerce' ), + 'type' => 'string', + 'enum' => array_merge( array( 'standard' ), WC_Tax::get_tax_class_slugs() ), + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + } + + $params['in_stock'] = array( + 'description' => __( 'Limit result set to products in stock or out of stock.', 'woocommerce' ), + 'type' => 'boolean', + 'sanitize_callback' => 'wc_string_to_bool', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['on_sale'] = array( + 'description' => __( 'Limit result set to products on sale.', 'woocommerce' ), + 'type' => 'boolean', + 'sanitize_callback' => 'wc_string_to_bool', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['min_price'] = array( + 'description' => __( 'Limit result set to products based on a minimum price.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['max_price'] = array( + 'description' => __( 'Limit result set to products based on a maximum price.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } +} diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-report-sales-v2-controller.php b/includes/rest-api/Controllers/Version2/class-wc-rest-report-sales-v2-controller.php new file mode 100644 index 00000000000..4c5a873f351 --- /dev/null +++ b/includes/rest-api/Controllers/Version2/class-wc-rest-report-sales-v2-controller.php @@ -0,0 +1,27 @@ +[\w-]+)'; + + /** + * Register routes. + * + * @since 3.0.0 + */ + public function register_routes() { + register_rest_route( + $this->namespace, '/' . $this->rest_base, array( + 'args' => array( + 'group' => array( + 'description' => __( 'Settings group ID.', 'woocommerce' ), + 'type' => 'string', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + register_rest_route( + $this->namespace, '/' . $this->rest_base . '/batch', array( + 'args' => array( + 'group' => array( + 'description' => __( 'Settings group ID.', 'woocommerce' ), + 'type' => 'string', + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'batch_items' ), + 'permission_callback' => array( $this, 'update_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_batch_schema' ), + ) + ); + + register_rest_route( + $this->namespace, '/' . $this->rest_base . '/(?P[\w-]+)', array( + 'args' => array( + 'group' => array( + 'description' => __( 'Settings group ID.', 'woocommerce' ), + 'type' => 'string', + ), + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'string', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Return a single setting. + * + * @since 3.0.0 + * @param WP_REST_Request $request Request data. + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + $setting = $this->get_setting( $request['group_id'], $request['id'] ); + + if ( is_wp_error( $setting ) ) { + return $setting; + } + + $response = $this->prepare_item_for_response( $setting, $request ); + + return rest_ensure_response( $response ); + } + + /** + * Return all settings in a group. + * + * @since 3.0.0 + * @param WP_REST_Request $request Request data. + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + $settings = $this->get_group_settings( $request['group_id'] ); + + if ( is_wp_error( $settings ) ) { + return $settings; + } + + $data = array(); + + foreach ( $settings as $setting_obj ) { + $setting = $this->prepare_item_for_response( $setting_obj, $request ); + $setting = $this->prepare_response_for_collection( $setting ); + if ( $this->is_setting_type_valid( $setting['type'] ) ) { + $data[] = $setting; + } + } + + return rest_ensure_response( $data ); + } + + /** + * Get all settings in a group. + * + * @since 3.0.0 + * @param string $group_id Group ID. + * @return array|WP_Error + */ + public function get_group_settings( $group_id ) { + if ( empty( $group_id ) ) { + return new WP_Error( 'rest_setting_setting_group_invalid', __( 'Invalid setting group.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $settings = apply_filters( 'woocommerce_settings-' . $group_id, array() ); + + if ( empty( $settings ) ) { + return new WP_Error( 'rest_setting_setting_group_invalid', __( 'Invalid setting group.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $filtered_settings = array(); + foreach ( $settings as $setting ) { + $option_key = $setting['option_key']; + $setting = $this->filter_setting( $setting ); + $default = isset( $setting['default'] ) ? $setting['default'] : ''; + // Get the option value. + if ( is_array( $option_key ) ) { + $option = get_option( $option_key[0] ); + $setting['value'] = isset( $option[ $option_key[1] ] ) ? $option[ $option_key[1] ] : $default; + } else { + $admin_setting_value = WC_Admin_Settings::get_option( $option_key, $default ); + $setting['value'] = $admin_setting_value; + } + + if ( 'multi_select_countries' === $setting['type'] ) { + $setting['options'] = WC()->countries->get_countries(); + $setting['type'] = 'multiselect'; + } elseif ( 'single_select_country' === $setting['type'] ) { + $setting['type'] = 'select'; + $setting['options'] = $this->get_countries_and_states(); + } + + $filtered_settings[] = $setting; + } + + return $filtered_settings; + } + + /** + * Returns a list of countries and states for use in the base location setting. + * + * @since 3.0.7 + * @return array Array of states and countries. + */ + private function get_countries_and_states() { + $countries = WC()->countries->get_countries(); + if ( ! $countries ) { + return array(); + } + + $output = array(); + + foreach ( $countries as $key => $value ) { + $states = WC()->countries->get_states( $key ); + if ( $states ) { + foreach ( $states as $state_key => $state_value ) { + $output[ $key . ':' . $state_key ] = $value . ' - ' . $state_value; + } + } else { + $output[ $key ] = $value; + } + } + + return $output; + } + + /** + * Get setting data. + * + * @since 3.0.0 + * @param string $group_id Group ID. + * @param string $setting_id Setting ID. + * @return stdClass|WP_Error + */ + public function get_setting( $group_id, $setting_id ) { + if ( empty( $setting_id ) ) { + return new WP_Error( 'rest_setting_setting_invalid', __( 'Invalid setting.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $settings = $this->get_group_settings( $group_id ); + + if ( is_wp_error( $settings ) ) { + return $settings; + } + + $array_key = array_keys( wp_list_pluck( $settings, 'id' ), $setting_id ); + + if ( empty( $array_key ) ) { + return new WP_Error( 'rest_setting_setting_invalid', __( 'Invalid setting.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $setting = $settings[ $array_key[0] ]; + + if ( ! $this->is_setting_type_valid( $setting['type'] ) ) { + return new WP_Error( 'rest_setting_setting_invalid', __( 'Invalid setting.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + return $setting; + } + + /** + * Bulk create, update and delete items. + * + * @since 3.0.0 + * @param WP_REST_Request $request Full details about the request. + * @return array Of WP_Error or WP_REST_Response. + */ + public function batch_items( $request ) { + // Get the request params. + $items = array_filter( $request->get_params() ); + + /* + * Since our batch settings update is group-specific and matches based on the route, + * we inject the URL parameters (containing group) into the batch items + */ + if ( ! empty( $items['update'] ) ) { + $to_update = array(); + foreach ( $items['update'] as $item ) { + $to_update[] = array_merge( $request->get_url_params(), $item ); + } + $request = new WP_REST_Request( $request->get_method() ); + $request->set_body_params( array( 'update' => $to_update ) ); + } + + return parent::batch_items( $request ); + } + + /** + * Update a single setting in a group. + * + * @since 3.0.0 + * @param WP_REST_Request $request Request data. + * @return WP_Error|WP_REST_Response + */ + public function update_item( $request ) { + $setting = $this->get_setting( $request['group_id'], $request['id'] ); + + if ( is_wp_error( $setting ) ) { + return $setting; + } + + if ( is_callable( array( $this, 'validate_setting_' . $setting['type'] . '_field' ) ) ) { + $value = $this->{'validate_setting_' . $setting['type'] . '_field'}( $request['value'], $setting ); + } else { + $value = $this->validate_setting_text_field( $request['value'], $setting ); + } + + if ( is_wp_error( $value ) ) { + return $value; + } + + if ( is_array( $setting['option_key'] ) ) { + $setting['value'] = $value; + $option_key = $setting['option_key']; + $prev = get_option( $option_key[0] ); + $prev[ $option_key[1] ] = $request['value']; + update_option( $option_key[0], $prev ); + } else { + $update_data = array(); + $update_data[ $setting['option_key'] ] = $value; + $setting['value'] = $value; + WC_Admin_Settings::save_fields( array( $setting ), $update_data ); + } + + $response = $this->prepare_item_for_response( $setting, $request ); + + return rest_ensure_response( $response ); + } + + /** + * Prepare a single setting object for response. + * + * @since 3.0.0 + * @param object $item Setting object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $item, $request ) { + unset( $item['option_key'] ); + $data = $this->filter_setting( $item ); + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, empty( $request['context'] ) ? 'view' : $request['context'] ); + $response = rest_ensure_response( $data ); + $response->add_links( $this->prepare_links( $data['id'], $request['group_id'] ) ); + return $response; + } + + /** + * Prepare links for the request. + * + * @since 3.0.0 + * @param string $setting_id Setting ID. + * @param string $group_id Group ID. + * @return array Links for the given setting. + */ + protected function prepare_links( $setting_id, $group_id ) { + $base = str_replace( '(?P[\w-]+)', $group_id, $this->rest_base ); + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $base, $setting_id ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ), + ), + ); + + return $links; + } + + /** + * Makes sure the current user has access to READ the settings APIs. + * + * @since 3.0.0 + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Makes sure the current user has access to WRITE the settings APIs. + * + * @since 3.0.0 + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|boolean + */ + public function update_items_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'edit' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you cannot edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Filters out bad values from the settings array/filter so we + * only return known values via the API. + * + * @since 3.0.0 + * @param array $setting Settings. + * @return array + */ + public function filter_setting( $setting ) { + $setting = array_intersect_key( + $setting, + array_flip( array_filter( array_keys( $setting ), array( $this, 'allowed_setting_keys' ) ) ) + ); + + if ( empty( $setting['options'] ) ) { + unset( $setting['options'] ); + } + + if ( 'image_width' === $setting['type'] ) { + $setting = $this->cast_image_width( $setting ); + } + + return $setting; + } + + /** + * For image_width, Crop can return "0" instead of false -- so we want + * to make sure we return these consistently the same we accept them. + * + * @todo remove in 4.0 + * @since 3.0.0 + * @param array $setting Settings. + * @return array + */ + public function cast_image_width( $setting ) { + foreach ( array( 'default', 'value' ) as $key ) { + if ( isset( $setting[ $key ] ) ) { + $setting[ $key ]['width'] = intval( $setting[ $key ]['width'] ); + $setting[ $key ]['height'] = intval( $setting[ $key ]['height'] ); + $setting[ $key ]['crop'] = (bool) $setting[ $key ]['crop']; + } + } + return $setting; + } + + /** + * Callback for allowed keys for each setting response. + * + * @since 3.0.0 + * @param string $key Key to check. + * @return boolean + */ + public function allowed_setting_keys( $key ) { + return in_array( + $key, array( + 'id', + 'label', + 'description', + 'default', + 'tip', + 'placeholder', + 'type', + 'options', + 'value', + 'option_key', + ) + ); + } + + /** + * Boolean for if a setting type is a valid supported setting type. + * + * @since 3.0.0 + * @param string $type Type. + * @return bool + */ + public function is_setting_type_valid( $type ) { + return in_array( + $type, array( + 'text', // Validates with validate_setting_text_field. + 'email', // Validates with validate_setting_text_field. + 'number', // Validates with validate_setting_text_field. + 'color', // Validates with validate_setting_text_field. + 'password', // Validates with validate_setting_text_field. + 'textarea', // Validates with validate_setting_textarea_field. + 'select', // Validates with validate_setting_select_field. + 'multiselect', // Validates with validate_setting_multiselect_field. + 'radio', // Validates with validate_setting_radio_field (-> validate_setting_select_field). + 'checkbox', // Validates with validate_setting_checkbox_field. + 'image_width', // Validates with validate_setting_image_width_field. + 'thumbnail_cropping', // Validates with validate_setting_text_field. + ) + ); + } + + /** + * Get the settings schema, conforming to JSON Schema. + * + * @since 3.0.0 + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'setting', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'A unique identifier for the setting.', 'woocommerce' ), + 'type' => 'string', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_title', + ), + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'label' => array( + 'description' => __( 'A human readable label for the setting used in interfaces.', 'woocommerce' ), + 'type' => 'string', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'description' => array( + 'description' => __( 'A human readable description for the setting used in interfaces.', 'woocommerce' ), + 'type' => 'string', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'value' => array( + 'description' => __( 'Setting value.', 'woocommerce' ), + 'type' => array( 'string', 'array', 'null' ), + 'items' => array( + 'type' => array( 'string', 'null' ), + ), + 'context' => array( 'view', 'edit' ), + ), + 'default' => array( + 'description' => __( 'Default value for the setting.', 'woocommerce' ), + 'type' => array( 'string', 'array', 'null' ), + 'items' => array( + 'type' => array( 'string', 'null' ), + ), + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'tip' => array( + 'description' => __( 'Additional help text shown to the user about the setting.', 'woocommerce' ), + 'type' => 'string', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'placeholder' => array( + 'description' => __( 'Placeholder text to be displayed in text inputs.', 'woocommerce' ), + 'type' => 'string', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'type' => array( + 'description' => __( 'Type of setting.', 'woocommerce' ), + 'type' => 'string', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + 'context' => array( 'view', 'edit' ), + 'enum' => array( 'text', 'email', 'number', 'color', 'password', 'textarea', 'select', 'multiselect', 'radio', 'image_width', 'checkbox', 'thumbnail_cropping' ), + 'readonly' => true, + ), + 'options' => array( + 'description' => __( 'Array of options (key value pairs) for inputs such as select, multiselect, and radio buttons.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-settings-v2-controller.php b/includes/rest-api/Controllers/Version2/class-wc-rest-settings-v2-controller.php new file mode 100644 index 00000000000..f41c91e722d --- /dev/null +++ b/includes/rest-api/Controllers/Version2/class-wc-rest-settings-v2-controller.php @@ -0,0 +1,232 @@ +namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Get all settings groups items. + * + * @since 3.0.0 + * @param WP_REST_Request $request Request data. + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + $groups = apply_filters( 'woocommerce_settings_groups', array() ); + if ( empty( $groups ) ) { + return new WP_Error( 'rest_setting_groups_empty', __( 'No setting groups have been registered.', 'woocommerce' ), array( 'status' => 500 ) ); + } + + $defaults = $this->group_defaults(); + $filtered_groups = array(); + foreach ( $groups as $group ) { + $sub_groups = array(); + foreach ( $groups as $_group ) { + if ( ! empty( $_group['parent_id'] ) && $group['id'] === $_group['parent_id'] ) { + $sub_groups[] = $_group['id']; + } + } + $group['sub_groups'] = $sub_groups; + + $group = wp_parse_args( $group, $defaults ); + if ( ! is_null( $group['id'] ) && ! is_null( $group['label'] ) ) { + $group_obj = $this->filter_group( $group ); + $group_data = $this->prepare_item_for_response( $group_obj, $request ); + $group_data = $this->prepare_response_for_collection( $group_data ); + + $filtered_groups[] = $group_data; + } + } + + $response = rest_ensure_response( $filtered_groups ); + return $response; + } + + /** + * Prepare links for the request. + * + * @param string $group_id Group ID. + * @return array Links for the given group. + */ + protected function prepare_links( $group_id ) { + $base = '/' . $this->namespace . '/' . $this->rest_base; + $links = array( + 'options' => array( + 'href' => rest_url( trailingslashit( $base ) . $group_id ), + ), + ); + + return $links; + } + + /** + * Prepare a report sales object for serialization. + * + * @since 3.0.0 + * @param array $item Group object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $item, $request ) { + $context = empty( $request['context'] ) ? 'view' : $request['context']; + $data = $this->add_additional_fields_to_object( $item, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $item['id'] ) ); + + return $response; + } + + /** + * Filters out bad values from the groups array/filter so we + * only return known values via the API. + * + * @since 3.0.0 + * @param array $group Group. + * @return array + */ + public function filter_group( $group ) { + return array_intersect_key( + $group, + array_flip( array_filter( array_keys( $group ), array( $this, 'allowed_group_keys' ) ) ) + ); + } + + /** + * Callback for allowed keys for each group response. + * + * @since 3.0.0 + * @param string $key Key to check. + * @return boolean + */ + public function allowed_group_keys( $key ) { + return in_array( $key, array( 'id', 'label', 'description', 'parent_id', 'sub_groups' ) ); + } + + /** + * Returns default settings for groups. null means the field is required. + * + * @since 3.0.0 + * @return array + */ + protected function group_defaults() { + return array( + 'id' => null, + 'label' => null, + 'description' => '', + 'parent_id' => '', + 'sub_groups' => array(), + ); + } + + /** + * Makes sure the current user has access to READ the settings APIs. + * + * @since 3.0.0 + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Get the groups schema, conforming to JSON Schema. + * + * @since 3.0.0 + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'setting_group', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'A unique identifier that can be used to link settings together.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'label' => array( + 'description' => __( 'A human readable label for the setting used in interfaces.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'description' => array( + 'description' => __( 'A human readable description for the setting used in interfaces.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'parent_id' => array( + 'description' => __( 'ID of parent grouping.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'sub_groups' => array( + 'description' => __( 'IDs for settings sub groups.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-shipping-methods-v2-controller.php b/includes/rest-api/Controllers/Version2/class-wc-rest-shipping-methods-v2-controller.php new file mode 100644 index 00000000000..795c92b3e18 --- /dev/null +++ b/includes/rest-api/Controllers/Version2/class-wc-rest-shipping-methods-v2-controller.php @@ -0,0 +1,231 @@ + + */ + public function register_routes() { + register_rest_route( + $this->namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + register_rest_route( + $this->namespace, '/' . $this->rest_base . '/(?P[\w-]+)', array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'string', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Check whether a given request has permission to view shipping methods. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'shipping_methods', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + return true; + } + + /** + * Check if a given request has access to read a shipping method. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'shipping_methods', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + return true; + } + + /** + * Get shipping methods. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + $wc_shipping = WC_Shipping::instance(); + $response = array(); + foreach ( $wc_shipping->get_shipping_methods() as $id => $shipping_method ) { + $method = $this->prepare_item_for_response( $shipping_method, $request ); + $method = $this->prepare_response_for_collection( $method ); + $response[] = $method; + } + return rest_ensure_response( $response ); + } + + /** + * Get a single Shipping Method. + * + * @param WP_REST_Request $request Request data. + * @return WP_REST_Response|WP_Error + */ + public function get_item( $request ) { + $wc_shipping = WC_Shipping::instance(); + $methods = $wc_shipping->get_shipping_methods(); + if ( empty( $methods[ $request['id'] ] ) ) { + return new WP_Error( 'woocommerce_rest_shipping_method_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $method = $methods[ $request['id'] ]; + $response = $this->prepare_item_for_response( $method, $request ); + + return rest_ensure_response( $response ); + } + + /** + * Prepare a shipping method for response. + * + * @param WC_Shipping_Method $method Shipping method object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $method, $request ) { + $data = array( + 'id' => $method->id, + 'title' => $method->method_title, + 'description' => $method->method_description, + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $method, $request ) ); + + /** + * Filter shipping methods object returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param WC_Shipping_Method $method Shipping method object used to create response. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'woocommerce_rest_prepare_shipping_method', $response, $method, $request ); + } + + /** + * Prepare links for the request. + * + * @param WC_Shipping_Method $method Shipping method object. + * @param WP_REST_Request $request Request object. + * @return array + */ + protected function prepare_links( $method, $request ) { + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $method->id ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + + return $links; + } + + /** + * Get the shipping method schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'shipping_method', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Method ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'title' => array( + 'description' => __( 'Shipping method title.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'description' => array( + 'description' => __( 'Shipping method description.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get any query params needed. + * + * @return array + */ + public function get_collection_params() { + return array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ); + } +} diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-shipping-zone-locations-v2-controller.php b/includes/rest-api/Controllers/Version2/class-wc-rest-shipping-zone-locations-v2-controller.php new file mode 100644 index 00000000000..9e171814f70 --- /dev/null +++ b/includes/rest-api/Controllers/Version2/class-wc-rest-shipping-zone-locations-v2-controller.php @@ -0,0 +1,190 @@ +/locations endpoint. + * + * @package Automattic/WooCommerce/RestApi + * @since 3.0.0 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * REST API Shipping Zone Locations class. + * + * @package Automattic/WooCommerce/RestApi + * @extends WC_REST_Shipping_Zones_Controller_Base + */ +class WC_REST_Shipping_Zone_Locations_V2_Controller extends WC_REST_Shipping_Zones_Controller_Base { + + /** + * Register the routes for Shipping Zone Locations. + */ + public function register_routes() { + register_rest_route( + $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)/locations', array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique ID for the resource.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_items' ), + 'permission_callback' => array( $this, 'update_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Get all Shipping Zone Locations. + * + * @param WP_REST_Request $request Request data. + * @return WP_REST_Response|WP_Error + */ + public function get_items( $request ) { + $zone = $this->get_zone( (int) $request['id'] ); + + if ( is_wp_error( $zone ) ) { + return $zone; + } + + $locations = $zone->get_zone_locations(); + $data = array(); + + foreach ( $locations as $location_obj ) { + $location = $this->prepare_item_for_response( $location_obj, $request ); + $location = $this->prepare_response_for_collection( $location ); + $data[] = $location; + } + + return rest_ensure_response( $data ); + } + + /** + * Update all Shipping Zone Locations. + * + * @param WP_REST_Request $request Request data. + * @return WP_REST_Response|WP_Error + */ + public function update_items( $request ) { + $zone = $this->get_zone( (int) $request['id'] ); + + if ( is_wp_error( $zone ) ) { + return $zone; + } + + if ( 0 === $zone->get_id() ) { + return new WP_Error( 'woocommerce_rest_shipping_zone_locations_invalid_zone', __( 'The "locations not covered by your other zones" zone cannot be updated.', 'woocommerce' ), array( 'status' => 403 ) ); + } + + $raw_locations = $request->get_json_params(); + $locations = array(); + + foreach ( (array) $raw_locations as $raw_location ) { + if ( empty( $raw_location['code'] ) ) { + continue; + } + + $type = ! empty( $raw_location['type'] ) ? sanitize_text_field( $raw_location['type'] ) : 'country'; + + if ( ! in_array( $type, array( 'postcode', 'state', 'country', 'continent' ), true ) ) { + continue; + } + + $locations[] = array( + 'code' => sanitize_text_field( $raw_location['code'] ), + 'type' => sanitize_text_field( $type ), + ); + } + + $zone->set_locations( $locations ); + $zone->save(); + + return $this->get_items( $request ); + } + + /** + * Prepare the Shipping Zone Location for the REST response. + * + * @param array $item Shipping Zone Location. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response + */ + public function prepare_item_for_response( $item, $request ) { + $context = empty( $request['context'] ) ? 'view' : $request['context']; + $data = $this->add_additional_fields_to_object( $item, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( (int) $request['id'] ) ); + + return $response; + } + + /** + * Prepare links for the request. + * + * @param int $zone_id Given Shipping Zone ID. + * @return array Links for the given Shipping Zone Location. + */ + protected function prepare_links( $zone_id ) { + $base = '/' . $this->namespace . '/' . $this->rest_base . '/' . $zone_id; + $links = array( + 'collection' => array( + 'href' => rest_url( $base . '/locations' ), + ), + 'describes' => array( + 'href' => rest_url( $base ), + ), + ); + + return $links; + } + + /** + * Get the Shipping Zone Locations schema, conforming to JSON Schema + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'shipping_zone_location', + 'type' => 'object', + 'properties' => array( + 'code' => array( + 'description' => __( 'Shipping zone location code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'type' => array( + 'description' => __( 'Shipping zone location type.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'country', + 'enum' => array( + 'postcode', + 'state', + 'country', + 'continent', + ), + 'context' => array( 'view', 'edit' ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-shipping-zone-methods-v2-controller.php b/includes/rest-api/Controllers/Version2/class-wc-rest-shipping-zone-methods-v2-controller.php new file mode 100644 index 00000000000..c753fd8e42f --- /dev/null +++ b/includes/rest-api/Controllers/Version2/class-wc-rest-shipping-zone-methods-v2-controller.php @@ -0,0 +1,541 @@ +/methods endpoint. + * + * @package Automattic/WooCommerce/RestApi + * @since 3.0.0 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * REST API Shipping Zone Methods class. + * + * @package Automattic/WooCommerce/RestApi + * @extends WC_REST_Shipping_Zones_Controller_Base + */ +class WC_REST_Shipping_Zone_Methods_V2_Controller extends WC_REST_Shipping_Zones_Controller_Base { + + /** + * Register the routes for Shipping Zone Methods. + */ + public function register_routes() { + register_rest_route( + $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)/methods', array( + 'args' => array( + 'zone_id' => array( + 'description' => __( 'Unique ID for the zone.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => array_merge( + $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( + 'method_id' => array( + 'required' => true, + 'readonly' => false, + 'description' => __( 'Shipping method ID.', 'woocommerce' ), + ), + ) + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + register_rest_route( + $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)/methods/(?P[\d]+)', array( + 'args' => array( + 'zone_id' => array( + 'description' => __( 'Unique ID for the zone.', 'woocommerce' ), + 'type' => 'integer', + ), + 'instance_id' => array( + 'description' => __( 'Unique ID for the instance.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_items_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'type' => 'boolean', + 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Get a single Shipping Zone Method. + * + * @param WP_REST_Request $request Request data. + * @return WP_REST_Response|WP_Error + */ + public function get_item( $request ) { + $zone = $this->get_zone( $request['zone_id'] ); + + if ( is_wp_error( $zone ) ) { + return $zone; + } + + $instance_id = (int) $request['instance_id']; + $methods = $zone->get_shipping_methods(); + $method = false; + + foreach ( $methods as $method_obj ) { + if ( $instance_id === $method_obj->instance_id ) { + $method = $method_obj; + break; + } + } + + if ( false === $method ) { + return new WP_Error( 'woocommerce_rest_shipping_zone_method_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $data = $this->prepare_item_for_response( $method, $request ); + + return rest_ensure_response( $data ); + } + + /** + * Get all Shipping Zone Methods. + * + * @param WP_REST_Request $request Request data. + * @return WP_REST_Response|WP_Error + */ + public function get_items( $request ) { + $zone = $this->get_zone( $request['zone_id'] ); + + if ( is_wp_error( $zone ) ) { + return $zone; + } + + $methods = $zone->get_shipping_methods(); + $data = array(); + + foreach ( $methods as $method_obj ) { + $method = $this->prepare_item_for_response( $method_obj, $request ); + $data[] = $method; + } + + return rest_ensure_response( $data ); + } + + /** + * Create a new shipping zone method instance. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Request|WP_Error + */ + public function create_item( $request ) { + $method_id = $request['method_id']; + $zone = $this->get_zone( $request['zone_id'] ); + if ( is_wp_error( $zone ) ) { + return $zone; + } + + $instance_id = $zone->add_shipping_method( $method_id ); + $methods = $zone->get_shipping_methods(); + $method = false; + foreach ( $methods as $method_obj ) { + if ( $instance_id === $method_obj->instance_id ) { + $method = $method_obj; + break; + } + } + + if ( false === $method ) { + return new WP_Error( 'woocommerce_rest_shipping_zone_not_created', __( 'Resource cannot be created.', 'woocommerce' ), array( 'status' => 500 ) ); + } + + $method = $this->update_fields( $instance_id, $method, $request ); + if ( is_wp_error( $method ) ) { + return $method; + } + + $data = $this->prepare_item_for_response( $method, $request ); + return rest_ensure_response( $data ); + } + + /** + * Delete a shipping method instance. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function delete_item( $request ) { + $zone = $this->get_zone( $request['zone_id'] ); + if ( is_wp_error( $zone ) ) { + return $zone; + } + + $instance_id = (int) $request['instance_id']; + $force = $request['force']; + + $methods = $zone->get_shipping_methods(); + $method = false; + + foreach ( $methods as $method_obj ) { + if ( $instance_id === $method_obj->instance_id ) { + $method = $method_obj; + break; + } + } + + if ( false === $method ) { + return new WP_Error( 'woocommerce_rest_shipping_zone_method_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $method = $this->update_fields( $instance_id, $method, $request ); + if ( is_wp_error( $method ) ) { + return $method; + } + + $request->set_param( 'context', 'view' ); + $response = $this->prepare_item_for_response( $method, $request ); + + // Actually delete. + if ( $force ) { + $zone->delete_shipping_method( $instance_id ); + } else { + return new WP_Error( 'rest_trash_not_supported', __( 'Shipping methods do not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); + } + + /** + * Fires after a product review is deleted via the REST API. + * + * @param object $method + * @param WP_REST_Response $response The response data. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( 'rest_delete_product_review', $method, $response, $request ); + + return $response; + } + + /** + * Update A Single Shipping Zone Method. + * + * @param WP_REST_Request $request Request data. + * @return WP_REST_Response|WP_Error + */ + public function update_item( $request ) { + $zone = $this->get_zone( $request['zone_id'] ); + if ( is_wp_error( $zone ) ) { + return $zone; + } + + $instance_id = (int) $request['instance_id']; + $methods = $zone->get_shipping_methods(); + $method = false; + + foreach ( $methods as $method_obj ) { + if ( $instance_id === $method_obj->instance_id ) { + $method = $method_obj; + break; + } + } + + if ( false === $method ) { + return new WP_Error( 'woocommerce_rest_shipping_zone_method_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $method = $this->update_fields( $instance_id, $method, $request ); + if ( is_wp_error( $method ) ) { + return $method; + } + + $data = $this->prepare_item_for_response( $method, $request ); + return rest_ensure_response( $data ); + } + + /** + * Updates settings, order, and enabled status on create. + * + * @param int $instance_id Instance ID. + * @param WC_Shipping_Method $method Shipping method data. + * @param WP_REST_Request $request Request data. + * + * @return WC_Shipping_Method + */ + public function update_fields( $instance_id, $method, $request ) { + global $wpdb; + + // Update settings if present. + if ( isset( $request['settings'] ) ) { + $method->init_instance_settings(); + $instance_settings = $method->instance_settings; + $errors_found = false; + foreach ( $method->get_instance_form_fields() as $key => $field ) { + if ( isset( $request['settings'][ $key ] ) ) { + if ( is_callable( array( $this, 'validate_setting_' . $field['type'] . '_field' ) ) ) { + $value = $this->{'validate_setting_' . $field['type'] . '_field'}( $request['settings'][ $key ], $field ); + } else { + $value = $this->validate_setting_text_field( $request['settings'][ $key ], $field ); + } + if ( is_wp_error( $value ) ) { + $errors_found = true; + break; + } + $instance_settings[ $key ] = $value; + } + } + + if ( $errors_found ) { + return new WP_Error( 'rest_setting_value_invalid', __( 'An invalid setting value was passed.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + update_option( $method->get_instance_option_key(), apply_filters( 'woocommerce_shipping_' . $method->id . '_instance_settings_values', $instance_settings, $method ) ); + } + + // Update order. + if ( isset( $request['order'] ) ) { + $wpdb->update( "{$wpdb->prefix}woocommerce_shipping_zone_methods", array( 'method_order' => absint( $request['order'] ) ), array( 'instance_id' => absint( $instance_id ) ) ); + $method->method_order = absint( $request['order'] ); + } + + // Update if this method is enabled or not. + if ( isset( $request['enabled'] ) ) { + if ( $wpdb->update( "{$wpdb->prefix}woocommerce_shipping_zone_methods", array( 'is_enabled' => $request['enabled'] ), array( 'instance_id' => absint( $instance_id ) ) ) ) { + do_action( 'woocommerce_shipping_zone_method_status_toggled', $instance_id, $method->id, $request['zone_id'], $request['enabled'] ); + $method->enabled = ( true === $request['enabled'] ? 'yes' : 'no' ); + } + } + + return $method; + } + + /** + * Prepare the Shipping Zone Method for the REST response. + * + * @param array $item Shipping Zone Method. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response + */ + public function prepare_item_for_response( $item, $request ) { + $method = array( + 'id' => $item->instance_id, + 'instance_id' => $item->instance_id, + 'title' => $item->instance_settings['title'], + 'order' => $item->method_order, + 'enabled' => ( 'yes' === $item->enabled ), + 'method_id' => $item->id, + 'method_title' => $item->method_title, + 'method_description' => $item->method_description, + 'settings' => $this->get_settings( $item ), + ); + + $context = empty( $request['context'] ) ? 'view' : $request['context']; + $data = $this->add_additional_fields_to_object( $method, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $request['zone_id'], $item->instance_id ) ); + + $response = $this->prepare_response_for_collection( $response ); + + return $response; + } + + /** + * Return settings associated with this shipping zone method instance. + * + * @param WC_Shipping_Method $item Shipping method data. + * + * @return array + */ + public function get_settings( $item ) { + $item->init_instance_settings(); + $settings = array(); + foreach ( $item->get_instance_form_fields() as $id => $field ) { + $data = array( + 'id' => $id, + 'label' => $field['title'], + 'description' => empty( $field['description'] ) ? '' : $field['description'], + 'type' => $field['type'], + 'value' => $item->instance_settings[ $id ], + 'default' => empty( $field['default'] ) ? '' : $field['default'], + 'tip' => empty( $field['description'] ) ? '' : $field['description'], + 'placeholder' => empty( $field['placeholder'] ) ? '' : $field['placeholder'], + ); + if ( ! empty( $field['options'] ) ) { + $data['options'] = $field['options']; + } + $settings[ $id ] = $data; + } + return $settings; + } + + /** + * Prepare links for the request. + * + * @param int $zone_id Given Shipping Zone ID. + * @param int $instance_id Given Shipping Zone Method Instance ID. + * @return array Links for the given Shipping Zone Method. + */ + protected function prepare_links( $zone_id, $instance_id ) { + $base = '/' . $this->namespace . '/' . $this->rest_base . '/' . $zone_id; + $links = array( + 'self' => array( + 'href' => rest_url( $base . '/methods/' . $instance_id ), + ), + 'collection' => array( + 'href' => rest_url( $base . '/methods' ), + ), + 'describes' => array( + 'href' => rest_url( $base ), + ), + ); + + return $links; + } + + /** + * Get the Shipping Zone Methods schema, conforming to JSON Schema + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'shipping_zone_method', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Shipping method instance ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'instance_id' => array( + 'description' => __( 'Shipping method instance ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'title' => array( + 'description' => __( 'Shipping method customer facing title.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'order' => array( + 'description' => __( 'Shipping method sort order.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'enabled' => array( + 'description' => __( 'Shipping method enabled status.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + ), + 'method_id' => array( + 'description' => __( 'Shipping method ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'method_title' => array( + 'description' => __( 'Shipping method title.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'method_description' => array( + 'description' => __( 'Shipping method description.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'settings' => array( + 'description' => __( 'Shipping method settings.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'id' => array( + 'description' => __( 'A unique identifier for the setting.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'label' => array( + 'description' => __( 'A human readable label for the setting used in interfaces.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'description' => array( + 'description' => __( 'A human readable description for the setting used in interfaces.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'type' => array( + 'description' => __( 'Type of setting.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'enum' => array( 'text', 'email', 'number', 'color', 'password', 'textarea', 'select', 'multiselect', 'radio', 'image_width', 'checkbox' ), + 'readonly' => true, + ), + 'value' => array( + 'description' => __( 'Setting value.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'default' => array( + 'description' => __( 'Default value for the setting.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'tip' => array( + 'description' => __( 'Additional help text shown to the user about the setting.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'placeholder' => array( + 'description' => __( 'Placeholder text to be displayed in text inputs.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-shipping-zones-v2-controller.php b/includes/rest-api/Controllers/Version2/class-wc-rest-shipping-zones-v2-controller.php new file mode 100644 index 00000000000..5190b6e84b5 --- /dev/null +++ b/includes/rest-api/Controllers/Version2/class-wc-rest-shipping-zones-v2-controller.php @@ -0,0 +1,304 @@ +namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => array_merge( + $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( + 'name' => array( + 'required' => true, + 'type' => 'string', + 'description' => __( 'Shipping zone name.', 'woocommerce' ), + ), + ) + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + register_rest_route( + $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique ID for the resource.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_items_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'type' => 'boolean', + 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Get a single Shipping Zone. + * + * @param WP_REST_Request $request Request data. + * @return WP_REST_Response|WP_Error + */ + public function get_item( $request ) { + $zone = $this->get_zone( $request->get_param( 'id' ) ); + + if ( is_wp_error( $zone ) ) { + return $zone; + } + + $data = $zone->get_data(); + $data = $this->prepare_item_for_response( $data, $request ); + $data = $this->prepare_response_for_collection( $data ); + + return rest_ensure_response( $data ); + } + + /** + * Get all Shipping Zones. + * + * @param WP_REST_Request $request Request data. + * @return WP_REST_Response + */ + public function get_items( $request ) { + $rest_of_the_world = WC_Shipping_Zones::get_zone_by( 'zone_id', 0 ); + + $zones = WC_Shipping_Zones::get_zones(); + array_unshift( $zones, $rest_of_the_world->get_data() ); + $data = array(); + + foreach ( $zones as $zone_obj ) { + $zone = $this->prepare_item_for_response( $zone_obj, $request ); + $zone = $this->prepare_response_for_collection( $zone ); + $data[] = $zone; + } + + return rest_ensure_response( $data ); + } + + /** + * Create a single Shipping Zone. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Request|WP_Error + */ + public function create_item( $request ) { + $zone = new WC_Shipping_Zone( null ); + + if ( ! is_null( $request->get_param( 'name' ) ) ) { + $zone->set_zone_name( $request->get_param( 'name' ) ); + } + + if ( ! is_null( $request->get_param( 'order' ) ) ) { + $zone->set_zone_order( $request->get_param( 'order' ) ); + } + + $zone->save(); + + if ( $zone->get_id() !== 0 ) { + $request->set_param( 'id', $zone->get_id() ); + $response = $this->get_item( $request ); + $response->set_status( 201 ); + $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $zone->get_id() ) ) ); + return $response; + } else { + return new WP_Error( 'woocommerce_rest_shipping_zone_not_created', __( "Resource cannot be created. Check to make sure 'order' and 'name' are present.", 'woocommerce' ), array( 'status' => 500 ) ); + } + } + + /** + * Update a single Shipping Zone. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Request|WP_Error + */ + public function update_item( $request ) { + $zone = $this->get_zone( $request->get_param( 'id' ) ); + + if ( is_wp_error( $zone ) ) { + return $zone; + } + + if ( 0 === $zone->get_id() ) { + return new WP_Error( 'woocommerce_rest_shipping_zone_invalid_zone', __( 'The "locations not covered by your other zones" zone cannot be updated.', 'woocommerce' ), array( 'status' => 403 ) ); + } + + $zone_changed = false; + + if ( ! is_null( $request->get_param( 'name' ) ) ) { + $zone->set_zone_name( $request->get_param( 'name' ) ); + $zone_changed = true; + } + + if ( ! is_null( $request->get_param( 'order' ) ) ) { + $zone->set_zone_order( $request->get_param( 'order' ) ); + $zone_changed = true; + } + + if ( $zone_changed ) { + $zone->save(); + } + + return $this->get_item( $request ); + } + + /** + * Delete a single Shipping Zone. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Request|WP_Error + */ + public function delete_item( $request ) { + $zone = $this->get_zone( $request->get_param( 'id' ) ); + + if ( is_wp_error( $zone ) ) { + return $zone; + } + + $force = $request['force']; + + $response = $this->get_item( $request ); + + if ( $force ) { + $zone->delete(); + } else { + return new WP_Error( 'rest_trash_not_supported', __( 'Shipping zones do not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); + } + + return $response; + } + + /** + * Prepare the Shipping Zone for the REST response. + * + * @param array $item Shipping Zone. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response + */ + public function prepare_item_for_response( $item, $request ) { + $data = array( + 'id' => (int) $item['id'], + 'name' => $item['zone_name'], + 'order' => (int) $item['zone_order'], + ); + + $context = empty( $request['context'] ) ? 'view' : $request['context']; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $data['id'] ) ); + + return $response; + } + + /** + * Prepare links for the request. + * + * @param int $zone_id Given Shipping Zone ID. + * @return array Links for the given Shipping Zone. + */ + protected function prepare_links( $zone_id ) { + $base = '/' . $this->namespace . '/' . $this->rest_base; + $links = array( + 'self' => array( + 'href' => rest_url( trailingslashit( $base ) . $zone_id ), + ), + 'collection' => array( + 'href' => rest_url( $base ), + ), + 'describedby' => array( + 'href' => rest_url( trailingslashit( $base ) . $zone_id . '/locations' ), + ), + ); + + return $links; + } + + /** + * Get the Shipping Zones schema, conforming to JSON Schema + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'shipping_zone', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Shipping zone name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'order' => array( + 'description' => __( 'Shipping zone order.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-tools-v2-controller.php b/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-tools-v2-controller.php new file mode 100644 index 00000000000..b98870cb42e --- /dev/null +++ b/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-tools-v2-controller.php @@ -0,0 +1,618 @@ +namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P[\w-]+)', + array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'string', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Check whether a given request has permission to view system status tools. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'system_status', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + return true; + } + + /** + * Check whether a given request has permission to view a specific system status tool. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'system_status', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + return true; + } + + /** + * Check whether a given request has permission to execute a specific system status tool. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function update_item_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'system_status', 'edit' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_update', __( 'Sorry, you cannot update resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + return true; + } + + /** + * A list of available tools for use in the system status section. + * 'button' becomes 'action' in the API. + * + * @return array + */ + public function get_tools() { + $tools = array( + 'clear_transients' => array( + 'name' => __( 'WooCommerce transients', 'woocommerce' ), + 'button' => __( 'Clear transients', 'woocommerce' ), + 'desc' => __( 'This tool will clear the product/shop transients cache.', 'woocommerce' ), + ), + 'clear_expired_transients' => array( + 'name' => __( 'Expired transients', 'woocommerce' ), + 'button' => __( 'Clear transients', 'woocommerce' ), + 'desc' => __( 'This tool will clear ALL expired transients from WordPress.', 'woocommerce' ), + ), + 'delete_orphaned_variations' => array( + 'name' => __( 'Orphaned variations', 'woocommerce' ), + 'button' => __( 'Delete orphaned variations', 'woocommerce' ), + 'desc' => __( 'This tool will delete all variations which have no parent.', 'woocommerce' ), + ), + 'clear_expired_download_permissions' => array( + 'name' => __( 'Used-up download permissions', 'woocommerce' ), + 'button' => __( 'Clean up download permissions', 'woocommerce' ), + 'desc' => __( 'This tool will delete expired download permissions and permissions with 0 remaining downloads.', 'woocommerce' ), + ), + 'regenerate_product_lookup_tables' => array( + 'name' => __( 'Product lookup tables', 'woocommerce' ), + 'button' => __( 'Regenerate', 'woocommerce' ), + 'desc' => __( 'This tool will regenerate product lookup table data. This process may take a while.', 'woocommerce' ), + ), + 'recount_terms' => array( + 'name' => __( 'Term counts', 'woocommerce' ), + 'button' => __( 'Recount terms', 'woocommerce' ), + 'desc' => __( 'This tool will recount product terms - useful when changing your settings in a way which hides products from the catalog.', 'woocommerce' ), + ), + 'reset_roles' => array( + 'name' => __( 'Capabilities', 'woocommerce' ), + 'button' => __( 'Reset capabilities', 'woocommerce' ), + 'desc' => __( 'This tool will reset the admin, customer and shop_manager roles to default. Use this if your users cannot access all of the WooCommerce admin pages.', 'woocommerce' ), + ), + 'clear_sessions' => array( + 'name' => __( 'Clear customer sessions', 'woocommerce' ), + 'button' => __( 'Clear', 'woocommerce' ), + 'desc' => sprintf( + '%1$s %2$s', + __( 'Note:', 'woocommerce' ), + __( 'This tool will delete all customer session data from the database, including current carts and saved carts in the database.', 'woocommerce' ) + ), + ), + 'clear_template_cache' => array( + 'name' => __( 'Clear template cache', 'woocommerce' ), + 'button' => __( 'Clear', 'woocommerce' ), + 'desc' => sprintf( + '%1$s %2$s', + __( 'Note:', 'woocommerce' ), + __( 'This tool will empty the template cache.', 'woocommerce' ) + ), + ), + 'install_pages' => array( + 'name' => __( 'Create default WooCommerce pages', 'woocommerce' ), + 'button' => __( 'Create pages', 'woocommerce' ), + 'desc' => sprintf( + '%1$s %2$s', + __( 'Note:', 'woocommerce' ), + __( 'This tool will install all the missing WooCommerce pages. Pages already defined and set up will not be replaced.', 'woocommerce' ) + ), + ), + 'delete_taxes' => array( + 'name' => __( 'Delete WooCommerce tax rates', 'woocommerce' ), + 'button' => __( 'Delete tax rates', 'woocommerce' ), + 'desc' => sprintf( + '%1$s %2$s', + __( 'Note:', 'woocommerce' ), + __( 'This option will delete ALL of your tax rates, use with caution. This action cannot be reversed.', 'woocommerce' ) + ), + ), + 'regenerate_thumbnails' => array( + 'name' => __( 'Regenerate shop thumbnails', 'woocommerce' ), + 'button' => __( 'Regenerate', 'woocommerce' ), + 'desc' => __( 'This will regenerate all shop thumbnails to match your theme and/or image settings.', 'woocommerce' ), + ), + 'db_update_routine' => array( + 'name' => __( 'Update database', 'woocommerce' ), + 'button' => __( 'Update database', 'woocommerce' ), + 'desc' => sprintf( + '%1$s %2$s', + __( 'Note:', 'woocommerce' ), + __( 'This tool will update your WooCommerce database to the latest version. Please ensure you make sufficient backups before proceeding.', 'woocommerce' ) + ), + ), + ); + if ( method_exists( 'WC_Install', 'verify_base_tables' ) ) { + $tools['verify_db_tables'] = array( + 'name' => __( 'Verify base database tables', 'woocommerce' ), + 'button' => __( 'Verify database', 'woocommerce' ), + 'desc' => sprintf( + __( 'Verify if all base database tables are present.', 'woocommerce' ) + ), + ); + } + + // Jetpack does the image resizing heavy lifting so you don't have to. + if ( ( class_exists( 'Jetpack' ) && Jetpack::is_module_active( 'photon' ) ) || ! apply_filters( 'woocommerce_background_image_regeneration', true ) ) { + unset( $tools['regenerate_thumbnails'] ); + } + + if ( ! function_exists( 'wc_clear_template_cache' ) ) { + unset( $tools['clear_template_cache'] ); + } + + return apply_filters( 'woocommerce_debug_tools', $tools ); + } + + /** + * Get a list of system status tools. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + $tools = array(); + foreach ( $this->get_tools() as $id => $tool ) { + $tools[] = $this->prepare_response_for_collection( + $this->prepare_item_for_response( + array( + 'id' => $id, + 'name' => $tool['name'], + 'action' => $tool['button'], + 'description' => $tool['desc'], + ), + $request + ) + ); + } + + $response = rest_ensure_response( $tools ); + return $response; + } + + /** + * Return a single tool. + * + * @param WP_REST_Request $request Request data. + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + $tools = $this->get_tools(); + if ( empty( $tools[ $request['id'] ] ) ) { + return new WP_Error( 'woocommerce_rest_system_status_tool_invalid_id', __( 'Invalid tool ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + $tool = $tools[ $request['id'] ]; + return rest_ensure_response( + $this->prepare_item_for_response( + array( + 'id' => $request['id'], + 'name' => $tool['name'], + 'action' => $tool['button'], + 'description' => $tool['desc'], + ), + $request + ) + ); + } + + /** + * Update (execute) a tool. + * + * @param WP_REST_Request $request Request data. + * @return WP_Error|WP_REST_Response + */ + public function update_item( $request ) { + $tools = $this->get_tools(); + if ( empty( $tools[ $request['id'] ] ) ) { + return new WP_Error( 'woocommerce_rest_system_status_tool_invalid_id', __( 'Invalid tool ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $tool = $tools[ $request['id'] ]; + $tool = array( + 'id' => $request['id'], + 'name' => $tool['name'], + 'action' => $tool['button'], + 'description' => $tool['desc'], + ); + + $execute_return = $this->execute_tool( $request['id'] ); + $tool = array_merge( $tool, $execute_return ); + + /** + * Fires after a WooCommerce REST system status tool has been executed. + * + * @param array $tool Details about the tool that has been executed. + * @param WP_REST_Request $request The current WP_REST_Request object. + */ + do_action( 'woocommerce_rest_insert_system_status_tool', $tool, $request ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $tool, $request ); + return rest_ensure_response( $response ); + } + + /** + * Prepare a tool item for serialization. + * + * @param array $item Object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $item, $request ) { + $context = empty( $request['context'] ) ? 'view' : $request['context']; + $data = $this->add_additional_fields_to_object( $item, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $item['id'] ) ); + + return $response; + } + + /** + * Get the system status tools schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'system_status_tool', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'A unique identifier for the tool.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_title', + ), + ), + 'name' => array( + 'description' => __( 'Tool name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'action' => array( + 'description' => __( 'What running the tool will do.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'description' => array( + 'description' => __( 'Tool description.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'success' => array( + 'description' => __( 'Did the tool run successfully?', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'edit' ), + ), + 'message' => array( + 'description' => __( 'Tool return message.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Prepare links for the request. + * + * @param string $id ID. + * @return array + */ + protected function prepare_links( $id ) { + $base = '/' . $this->namespace . '/' . $this->rest_base; + $links = array( + 'item' => array( + 'href' => rest_url( trailingslashit( $base ) . $id ), + 'embeddable' => true, + ), + ); + + return $links; + } + + /** + * Get any query params needed. + * + * @return array + */ + public function get_collection_params() { + return array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ); + } + + /** + * Actually executes a tool. + * + * @param string $tool Tool. + * @return array + */ + public function execute_tool( $tool ) { + global $wpdb; + $ran = true; + switch ( $tool ) { + case 'clear_transients': + wc_delete_product_transients(); + wc_delete_shop_order_transients(); + delete_transient( 'wc_count_comments' ); + delete_transient( 'as_comment_count' ); + + $attribute_taxonomies = wc_get_attribute_taxonomies(); + + if ( $attribute_taxonomies ) { + foreach ( $attribute_taxonomies as $attribute ) { + delete_transient( 'wc_layered_nav_counts_pa_' . $attribute->attribute_name ); + } + } + + WC_Cache_Helper::get_transient_version( 'shipping', true ); + $message = __( 'Product transients cleared', 'woocommerce' ); + break; + + case 'clear_expired_transients': + /* translators: %d: amount of expired transients */ + $message = sprintf( __( '%d transients rows cleared', 'woocommerce' ), wc_delete_expired_transients() ); + break; + + case 'delete_orphaned_variations': + // Delete orphans. + $result = absint( + $wpdb->query( + "DELETE products + FROM {$wpdb->posts} products + LEFT JOIN {$wpdb->posts} wp ON wp.ID = products.post_parent + WHERE wp.ID IS NULL AND products.post_type = 'product_variation';" + ) + ); + /* translators: %d: amount of orphaned variations */ + $message = sprintf( __( '%d orphaned variations deleted', 'woocommerce' ), $result ); + break; + + case 'clear_expired_download_permissions': + // Delete expired download permissions and ones with 0 downloads remaining. + $result = absint( + $wpdb->query( + $wpdb->prepare( + "DELETE FROM {$wpdb->prefix}woocommerce_downloadable_product_permissions + WHERE ( downloads_remaining != '' AND downloads_remaining = 0 ) OR ( access_expires IS NOT NULL AND access_expires < %s )", + gmdate( 'Y-m-d', current_time( 'timestamp' ) ) + ) + ) + ); + /* translators: %d: amount of permissions */ + $message = sprintf( __( '%d permissions deleted', 'woocommerce' ), $result ); + break; + + case 'regenerate_product_lookup_tables': + if ( ! wc_update_product_lookup_tables_is_running() ) { + wc_update_product_lookup_tables(); + } + $message = __( 'Lookup tables are regenerating', 'woocommerce' ); + break; + case 'reset_roles': + // Remove then re-add caps and roles. + WC_Install::remove_roles(); + WC_Install::create_roles(); + $message = __( 'Roles successfully reset', 'woocommerce' ); + break; + + case 'recount_terms': + $product_cats = get_terms( + 'product_cat', + array( + 'hide_empty' => false, + 'fields' => 'id=>parent', + ) + ); + _wc_term_recount( $product_cats, get_taxonomy( 'product_cat' ), true, false ); + $product_tags = get_terms( + 'product_tag', + array( + 'hide_empty' => false, + 'fields' => 'id=>parent', + ) + ); + _wc_term_recount( $product_tags, get_taxonomy( 'product_tag' ), true, false ); + $message = __( 'Terms successfully recounted', 'woocommerce' ); + break; + + case 'clear_sessions': + $wpdb->query( "TRUNCATE {$wpdb->prefix}woocommerce_sessions" ); + $result = absint( $wpdb->query( "DELETE FROM {$wpdb->usermeta} WHERE meta_key='_woocommerce_persistent_cart_" . get_current_blog_id() . "';" ) ); // WPCS: unprepared SQL ok. + wp_cache_flush(); + /* translators: %d: amount of sessions */ + $message = sprintf( __( 'Deleted all active sessions, and %d saved carts.', 'woocommerce' ), absint( $result ) ); + break; + + case 'install_pages': + WC_Install::create_pages(); + $message = __( 'All missing WooCommerce pages successfully installed', 'woocommerce' ); + break; + + case 'delete_taxes': + $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}woocommerce_tax_rates;" ); + $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}woocommerce_tax_rate_locations;" ); + + if ( method_exists( 'WC_Cache_Helper', 'invalidate_cache_group' ) ) { + WC_Cache_Helper::invalidate_cache_group( 'taxes' ); + } else { + WC_Cache_Helper::incr_cache_prefix( 'taxes' ); + } + $message = __( 'Tax rates successfully deleted', 'woocommerce' ); + break; + + case 'regenerate_thumbnails': + WC_Regenerate_Images::queue_image_regeneration(); + $message = __( 'Thumbnail regeneration has been scheduled to run in the background.', 'woocommerce' ); + break; + + case 'db_update_routine': + $blog_id = get_current_blog_id(); + // Used to fire an action added in WP_Background_Process::_construct() that calls WP_Background_Process::handle_cron_healthcheck(). + // This method will make sure the database updates are executed even if cron is disabled. Nothing will happen if the updates are already running. + do_action( 'wp_' . $blog_id . '_wc_updater_cron' ); + $message = __( 'Database upgrade routine has been scheduled to run in the background.', 'woocommerce' ); + break; + + case 'clear_template_cache': + if ( function_exists( 'wc_clear_template_cache' ) ) { + wc_clear_template_cache(); + $message = __( 'Template cache cleared.', 'woocommerce' ); + } else { + $message = __( 'The active version of WooCommerce does not support template cache clearing.', 'woocommerce' ); + $ran = false; + } + break; + + case 'verify_db_tables': + if ( ! method_exists( 'WC_Install', 'verify_base_tables' ) ) { + $message = __( 'You need WooCommerce 4.2 or newer to run this tool.', 'woocommerce' ); + $ran = false; + break; + } + // Try to manually create table again. + $missing_tables = WC_Install::verify_base_tables( true, true ); + if ( 0 === count( $missing_tables ) ) { + $message = __( 'Database verified successfully.', 'woocommerce' ); + } else { + $message = __( 'Verifying database... One or more tables are still missing: ', 'woocommerce' ); + $message .= implode( ', ', $missing_tables ); + $ran = false; + } + break; + + default: + $tools = $this->get_tools(); + if ( isset( $tools[ $tool ]['callback'] ) ) { + $callback = $tools[ $tool ]['callback']; + $return = call_user_func( $callback ); + if ( is_string( $return ) ) { + $message = $return; + } elseif ( false === $return ) { + $callback_string = is_array( $callback ) ? get_class( $callback[0] ) . '::' . $callback[1] : $callback; + $ran = false; + /* translators: %s: callback string */ + $message = sprintf( __( 'There was an error calling %s', 'woocommerce' ), $callback_string ); + } else { + $message = __( 'Tool ran.', 'woocommerce' ); + } + } else { + $ran = false; + $message = __( 'There was an error calling this tool. There is no callback present.', 'woocommerce' ); + } + break; + } + + return array( + 'success' => $ran, + 'message' => $message, + ); + } +} diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-v2-controller.php b/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-v2-controller.php new file mode 100644 index 00000000000..e2b6e93c325 --- /dev/null +++ b/includes/rest-api/Controllers/Version2/class-wc-rest-system-status-v2-controller.php @@ -0,0 +1,1259 @@ +namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Check whether a given request has permission to view system status. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'system_status', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + return true; + } + + /** + * Get a system status info, by section. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + $fields = $this->get_fields_for_response( $request ); + $mappings = $this->get_item_mappings_per_fields( $fields ); + $response = $this->prepare_item_for_response( $mappings, $request ); + + return rest_ensure_response( $response ); + } + + /** + * Get the system status schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'system_status', + 'type' => 'object', + 'properties' => array( + 'environment' => array( + 'description' => __( 'Environment.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view' ), + 'readonly' => true, + 'properties' => array( + 'home_url' => array( + 'description' => __( 'Home URL.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'site_url' => array( + 'description' => __( 'Site URL.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'version' => array( + 'description' => __( 'WooCommerce version.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'log_directory' => array( + 'description' => __( 'Log directory.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'log_directory_writable' => array( + 'description' => __( 'Is log directory writable?', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'wp_version' => array( + 'description' => __( 'WordPress version.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'wp_multisite' => array( + 'description' => __( 'Is WordPress multisite?', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'wp_memory_limit' => array( + 'description' => __( 'WordPress memory limit.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'wp_debug_mode' => array( + 'description' => __( 'Is WordPress debug mode active?', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'wp_cron' => array( + 'description' => __( 'Are WordPress cron jobs enabled?', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'language' => array( + 'description' => __( 'WordPress language.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'server_info' => array( + 'description' => __( 'Server info.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'php_version' => array( + 'description' => __( 'PHP version.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'php_post_max_size' => array( + 'description' => __( 'PHP post max size.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'php_max_execution_time' => array( + 'description' => __( 'PHP max execution time.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'php_max_input_vars' => array( + 'description' => __( 'PHP max input vars.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'curl_version' => array( + 'description' => __( 'cURL version.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'suhosin_installed' => array( + 'description' => __( 'Is SUHOSIN installed?', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'max_upload_size' => array( + 'description' => __( 'Max upload size.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'mysql_version' => array( + 'description' => __( 'MySQL version.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'mysql_version_string' => array( + 'description' => __( 'MySQL version string.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'default_timezone' => array( + 'description' => __( 'Default timezone.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'fsockopen_or_curl_enabled' => array( + 'description' => __( 'Is fsockopen/cURL enabled?', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'soapclient_enabled' => array( + 'description' => __( 'Is SoapClient class enabled?', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'domdocument_enabled' => array( + 'description' => __( 'Is DomDocument class enabled?', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'gzip_enabled' => array( + 'description' => __( 'Is GZip enabled?', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'mbstring_enabled' => array( + 'description' => __( 'Is mbstring enabled?', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'remote_post_successful' => array( + 'description' => __( 'Remote POST successful?', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'remote_post_response' => array( + 'description' => __( 'Remote POST response.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'remote_get_successful' => array( + 'description' => __( 'Remote GET successful?', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'remote_get_response' => array( + 'description' => __( 'Remote GET response.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + ), + ), + 'database' => array( + 'description' => __( 'Database.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view' ), + 'readonly' => true, + 'properties' => array( + 'wc_database_version' => array( + 'description' => __( 'WC database version.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'database_prefix' => array( + 'description' => __( 'Database prefix.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'maxmind_geoip_database' => array( + 'description' => __( 'MaxMind GeoIP database.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'database_tables' => array( + 'description' => __( 'Database tables.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view' ), + 'readonly' => true, + 'items' => array( + 'type' => 'string', + ), + ), + ), + ), + 'active_plugins' => array( + 'description' => __( 'Active plugins.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view' ), + 'readonly' => true, + 'items' => array( + 'type' => 'string', + ), + ), + 'inactive_plugins' => array( + 'description' => __( 'Inactive plugins.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view' ), + 'readonly' => true, + 'items' => array( + 'type' => 'string', + ), + ), + 'dropins_mu_plugins' => array( + 'description' => __( 'Dropins & MU plugins.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view' ), + 'readonly' => true, + 'items' => array( + 'type' => 'string', + ), + ), + 'theme' => array( + 'description' => __( 'Theme.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view' ), + 'readonly' => true, + 'properties' => array( + 'name' => array( + 'description' => __( 'Theme name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'version' => array( + 'description' => __( 'Theme version.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'version_latest' => array( + 'description' => __( 'Latest version of theme.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'author_url' => array( + 'description' => __( 'Theme author URL.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'is_child_theme' => array( + 'description' => __( 'Is this theme a child theme?', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'has_woocommerce_support' => array( + 'description' => __( 'Does the theme declare WooCommerce support?', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'has_woocommerce_file' => array( + 'description' => __( 'Does the theme have a woocommerce.php file?', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'has_outdated_templates' => array( + 'description' => __( 'Does this theme have outdated templates?', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'overrides' => array( + 'description' => __( 'Template overrides.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view' ), + 'readonly' => true, + 'items' => array( + 'type' => 'string', + ), + ), + 'parent_name' => array( + 'description' => __( 'Parent theme name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'parent_version' => array( + 'description' => __( 'Parent theme version.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'parent_author_url' => array( + 'description' => __( 'Parent theme author URL.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view' ), + 'readonly' => true, + ), + ), + ), + 'settings' => array( + 'description' => __( 'Settings.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view' ), + 'readonly' => true, + 'properties' => array( + 'api_enabled' => array( + 'description' => __( 'REST API enabled?', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'force_ssl' => array( + 'description' => __( 'SSL forced?', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'currency' => array( + 'description' => __( 'Currency.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'currency_symbol' => array( + 'description' => __( 'Currency symbol.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'currency_position' => array( + 'description' => __( 'Currency position.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'thousand_separator' => array( + 'description' => __( 'Thousand separator.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'decimal_separator' => array( + 'description' => __( 'Decimal separator.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'number_of_decimals' => array( + 'description' => __( 'Number of decimals.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'geolocation_enabled' => array( + 'description' => __( 'Geolocation enabled?', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'taxonomies' => array( + 'description' => __( 'Taxonomy terms for product/order statuses.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view' ), + 'readonly' => true, + 'items' => array( + 'type' => 'string', + ), + ), + 'product_visibility_terms' => array( + 'description' => __( 'Terms in the product visibility taxonomy.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view' ), + 'readonly' => true, + 'items' => array( + 'type' => 'string', + ), + ), + ), + ), + 'security' => array( + 'description' => __( 'Security.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view' ), + 'readonly' => true, + 'properties' => array( + 'secure_connection' => array( + 'description' => __( 'Is the connection to your store secure?', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'hide_errors' => array( + 'description' => __( 'Hide errors from visitors?', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view' ), + 'readonly' => true, + ), + ), + ), + 'pages' => array( + 'description' => __( 'WooCommerce pages.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view' ), + 'readonly' => true, + 'items' => array( + 'type' => 'string', + ), + ), + 'post_type_counts' => array( + 'description' => __( 'Total post count.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view' ), + 'readonly' => true, + 'items' => array( + 'type' => 'string', + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Return an array of sections and the data associated with each. + * + * @deprecated 3.9.0 + * @return array + */ + public function get_item_mappings() { + return array( + 'environment' => $this->get_environment_info(), + 'database' => $this->get_database_info(), + 'active_plugins' => $this->get_active_plugins(), + 'inactive_plugins' => $this->get_inactive_plugins(), + 'dropins_mu_plugins' => $this->get_dropins_mu_plugins(), + 'theme' => $this->get_theme_info(), + 'settings' => $this->get_settings(), + 'security' => $this->get_security_info(), + 'pages' => $this->get_pages(), + 'post_type_counts' => $this->get_post_type_counts(), + ); + } + + /** + * Return an array of sections and the data associated with each. + * + * @since 3.9.0 + * @param array $fields List of fields to be included on the response. + * @return array + */ + public function get_item_mappings_per_fields( $fields ) { + return array( + 'environment' => $this->get_environment_info_per_fields( $fields ), + 'database' => $this->get_database_info(), + 'active_plugins' => $this->get_active_plugins(), + 'inactive_plugins' => $this->get_inactive_plugins(), + 'dropins_mu_plugins' => $this->get_dropins_mu_plugins(), + 'theme' => $this->get_theme_info(), + 'settings' => $this->get_settings(), + 'security' => $this->get_security_info(), + 'pages' => $this->get_pages(), + 'post_type_counts' => $this->get_post_type_counts(), + ); + } + + /** + * Get array of environment information. Includes thing like software + * versions, and various server settings. + * + * @deprecated 3.9.0 + * @return array + */ + public function get_environment_info() { + return $this->get_environment_info_per_fields( array( 'environment' ) ); + } + + /** + * Check if field item exists. + * + * @since 3.9.0 + * @param string $section Fields section. + * @param array $items List of items to check for. + * @param array $fields List of fields to be included on the response. + * @return bool + */ + private function check_if_field_item_exists( $section, $items, $fields ) { + if ( ! in_array( $section, $fields, true ) ) { + return false; + } + + $exclude = array(); + foreach ( $fields as $field ) { + $values = explode( '.', $field ); + + if ( $section !== $values[0] || empty( $values[1] ) ) { + continue; + } + + $exclude[] = $values[1]; + } + + return 0 <= count( array_intersect( $items, $exclude ) ); + } + + /** + * Get array of environment information. Includes thing like software + * versions, and various server settings. + * + * @param array $fields List of fields to be included on the response. + * @return array + */ + public function get_environment_info_per_fields( $fields ) { + global $wpdb; + + $enable_remote_post = $this->check_if_field_item_exists( 'environment', array( 'remote_post_successful', 'remote_post_response' ), $fields ); + $enable_remote_get = $this->check_if_field_item_exists( 'environment', array( 'remote_get_successful', 'remote_get_response' ), $fields ); + + // Figure out cURL version, if installed. + $curl_version = ''; + if ( function_exists( 'curl_version' ) ) { + $curl_version = curl_version(); + $curl_version = $curl_version['version'] . ', ' . $curl_version['ssl_version']; + } elseif ( extension_loaded( 'curl' ) ) { + $curl_version = __( 'cURL installed but unable to retrieve version.', 'woocommerce' ); + } + + // WP memory limit. + $wp_memory_limit = wc_let_to_num( WP_MEMORY_LIMIT ); + if ( function_exists( 'memory_get_usage' ) ) { + $wp_memory_limit = max( $wp_memory_limit, wc_let_to_num( @ini_get( 'memory_limit' ) ) ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged + } + + // Test POST requests. + $post_response_successful = null; + $post_response_code = null; + if ( $enable_remote_post ) { + $post_response_code = get_transient( 'woocommerce_test_remote_post' ); + + if ( false === $post_response_code || is_wp_error( $post_response_code ) ) { + $response = wp_safe_remote_post( + 'https://www.paypal.com/cgi-bin/webscr', + array( + 'timeout' => 10, + 'user-agent' => 'WooCommerce/' . WC()->version, + 'httpversion' => '1.1', + 'body' => array( + 'cmd' => '_notify-validate', + ), + ) + ); + if ( ! is_wp_error( $response ) ) { + $post_response_code = $response['response']['code']; + } + set_transient( 'woocommerce_test_remote_post', $post_response_code, HOUR_IN_SECONDS ); + } + + $post_response_successful = ! is_wp_error( $post_response_code ) && $post_response_code >= 200 && $post_response_code < 300; + } + + // Test GET requests. + $get_response_successful = null; + $get_response_code = null; + if ( $enable_remote_get ) { + $get_response_code = get_transient( 'woocommerce_test_remote_get' ); + + if ( false === $get_response_code || is_wp_error( $get_response_code ) ) { + $response = wp_safe_remote_get( 'https://woocommerce.com/wc-api/product-key-api?request=ping&network=' . ( is_multisite() ? '1' : '0' ) ); + if ( ! is_wp_error( $response ) ) { + $get_response_code = $response['response']['code']; + } + set_transient( 'woocommerce_test_remote_get', $get_response_code, HOUR_IN_SECONDS ); + } + + $get_response_successful = ! is_wp_error( $get_response_code ) && $get_response_code >= 200 && $get_response_code < 300; + } + + $database_version = wc_get_server_database_version(); + + // Return all environment info. Described by JSON Schema. + return array( + 'home_url' => get_option( 'home' ), + 'site_url' => get_option( 'siteurl' ), + 'version' => WC()->version, + 'log_directory' => WC_LOG_DIR, + 'log_directory_writable' => (bool) @fopen( WC_LOG_DIR . 'test-log.log', 'a' ), // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, WordPress.WP.AlternativeFunctions.file_system_read_fopen + 'wp_version' => get_bloginfo( 'version' ), + 'wp_multisite' => is_multisite(), + 'wp_memory_limit' => $wp_memory_limit, + 'wp_debug_mode' => ( defined( 'WP_DEBUG' ) && WP_DEBUG ), + 'wp_cron' => ! ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ), + 'language' => get_locale(), + 'external_object_cache' => wp_using_ext_object_cache(), + 'server_info' => isset( $_SERVER['SERVER_SOFTWARE'] ) ? wc_clean( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ) : '', + 'php_version' => phpversion(), + 'php_post_max_size' => wc_let_to_num( ini_get( 'post_max_size' ) ), + 'php_max_execution_time' => (int) ini_get( 'max_execution_time' ), + 'php_max_input_vars' => (int) ini_get( 'max_input_vars' ), + 'curl_version' => $curl_version, + 'suhosin_installed' => extension_loaded( 'suhosin' ), + 'max_upload_size' => wp_max_upload_size(), + 'mysql_version' => $database_version['number'], + 'mysql_version_string' => $database_version['string'], + 'default_timezone' => date_default_timezone_get(), + 'fsockopen_or_curl_enabled' => ( function_exists( 'fsockopen' ) || function_exists( 'curl_init' ) ), + 'soapclient_enabled' => class_exists( 'SoapClient' ), + 'domdocument_enabled' => class_exists( 'DOMDocument' ), + 'gzip_enabled' => is_callable( 'gzopen' ), + 'mbstring_enabled' => extension_loaded( 'mbstring' ), + 'remote_post_successful' => $post_response_successful, + 'remote_post_response' => is_wp_error( $post_response_code ) ? $post_response_code->get_error_message() : $post_response_code, + 'remote_get_successful' => $get_response_successful, + 'remote_get_response' => is_wp_error( $get_response_code ) ? $get_response_code->get_error_message() : $get_response_code, + ); + } + + /** + * Add prefix to table. + * + * @param string $table Table name. + * @return stromg + */ + protected function add_db_table_prefix( $table ) { + global $wpdb; + return $wpdb->prefix . $table; + } + + /** + * Get array of database information. Version, prefix, and table existence. + * + * @return array + */ + public function get_database_info() { + global $wpdb; + + $tables = array(); + $database_size = array(); + + // It is not possible to get the database name from some classes that replace wpdb (e.g., HyperDB) + // and that is why this if condition is needed. + if ( defined( 'DB_NAME' ) ) { + $database_table_information = $wpdb->get_results( + $wpdb->prepare( + "SELECT + table_name AS 'name', + engine AS 'engine', + round( ( data_length / 1024 / 1024 ), 2 ) 'data', + round( ( index_length / 1024 / 1024 ), 2 ) 'index' + FROM information_schema.TABLES + WHERE table_schema = %s + ORDER BY name ASC;", + DB_NAME + ) + ); + + // WC Core tables to check existence of. + $core_tables = apply_filters( + 'woocommerce_database_tables', + array( + 'woocommerce_sessions', + 'woocommerce_api_keys', + 'woocommerce_attribute_taxonomies', + 'woocommerce_downloadable_product_permissions', + 'woocommerce_order_items', + 'woocommerce_order_itemmeta', + 'woocommerce_tax_rates', + 'woocommerce_tax_rate_locations', + 'woocommerce_shipping_zones', + 'woocommerce_shipping_zone_locations', + 'woocommerce_shipping_zone_methods', + 'woocommerce_payment_tokens', + 'woocommerce_payment_tokenmeta', + 'woocommerce_log', + ) + ); + + /** + * Adding the prefix to the tables array, for backwards compatibility. + * + * If we changed the tables above to include the prefix, then any filters against that table could break. + */ + $core_tables = array_map( array( $this, 'add_db_table_prefix' ), $core_tables ); + + /** + * Organize WooCommerce and non-WooCommerce tables separately for display purposes later. + * + * To ensure we include all WC tables, even if they do not exist, pre-populate the WC array with all the tables. + */ + $tables = array( + 'woocommerce' => array_fill_keys( $core_tables, false ), + 'other' => array(), + ); + + $database_size = array( + 'data' => 0, + 'index' => 0, + ); + + $site_tables_prefix = $wpdb->get_blog_prefix( get_current_blog_id() ); + $global_tables = $wpdb->tables( 'global', true ); + foreach ( $database_table_information as $table ) { + // Only include tables matching the prefix of the current site, this is to prevent displaying all tables on a MS install not relating to the current. + if ( is_multisite() && 0 !== strpos( $table->name, $site_tables_prefix ) && ! in_array( $table->name, $global_tables, true ) ) { + continue; + } + $table_type = in_array( $table->name, $core_tables, true ) ? 'woocommerce' : 'other'; + + $tables[ $table_type ][ $table->name ] = array( + 'data' => $table->data, + 'index' => $table->index, + 'engine' => $table->engine, + ); + + $database_size['data'] += $table->data; + $database_size['index'] += $table->index; + } + } + + // Return all database info. Described by JSON Schema. + return array( + 'wc_database_version' => get_option( 'woocommerce_db_version' ), + 'database_prefix' => $wpdb->prefix, + 'maxmind_geoip_database' => '', + 'database_tables' => $tables, + 'database_size' => $database_size, + ); + } + + /** + * Get array of counts of objects. Orders, products, etc. + * + * @return array + */ + public function get_post_type_counts() { + global $wpdb; + + $post_type_counts = $wpdb->get_results( "SELECT post_type AS 'type', count(1) AS 'count' FROM {$wpdb->posts} GROUP BY post_type;" ); + + return is_array( $post_type_counts ) ? $post_type_counts : array(); + } + + /** + * Get a list of plugins active on the site. + * + * @return array + */ + public function get_active_plugins() { + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + + if ( ! function_exists( 'get_plugin_data' ) ) { + return array(); + } + + $active_plugins = (array) get_option( 'active_plugins', array() ); + if ( is_multisite() ) { + $network_activated_plugins = array_keys( get_site_option( 'active_sitewide_plugins', array() ) ); + $active_plugins = array_merge( $active_plugins, $network_activated_plugins ); + } + + $active_plugins_data = array(); + + foreach ( $active_plugins as $plugin ) { + $data = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin ); + $active_plugins_data[] = $this->format_plugin_data( $plugin, $data ); + } + + return $active_plugins_data; + } + + /** + * Get a list of inplugins active on the site. + * + * @return array + */ + public function get_inactive_plugins() { + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + + if ( ! function_exists( 'get_plugins' ) ) { + return array(); + } + + $plugins = get_plugins(); + $active_plugins = (array) get_option( 'active_plugins', array() ); + + if ( is_multisite() ) { + $network_activated_plugins = array_keys( get_site_option( 'active_sitewide_plugins', array() ) ); + $active_plugins = array_merge( $active_plugins, $network_activated_plugins ); + } + + $plugins_data = array(); + + foreach ( $plugins as $plugin => $data ) { + if ( in_array( $plugin, $active_plugins, true ) ) { + continue; + } + $plugins_data[] = $this->format_plugin_data( $plugin, $data ); + } + + return $plugins_data; + } + + /** + * Format plugin data, including data on updates, into a standard format. + * + * @since 3.6.0 + * @param string $plugin Plugin directory/file. + * @param array $data Plugin data from WP. + * @return array Formatted data. + */ + protected function format_plugin_data( $plugin, $data ) { + require_once ABSPATH . 'wp-admin/includes/update.php'; + + if ( ! function_exists( 'get_plugin_updates' ) ) { + return array(); + } + + // Use WP API to lookup latest updates for plugins. WC_Helper injects updates for premium plugins. + if ( empty( $this->available_updates ) ) { + $this->available_updates = get_plugin_updates(); + } + + $version_latest = $data['Version']; + + // Find latest version. + if ( isset( $this->available_updates[ $plugin ]->update->new_version ) ) { + $version_latest = $this->available_updates[ $plugin ]->update->new_version; + } + + return array( + 'plugin' => $plugin, + 'name' => $data['Name'], + 'version' => $data['Version'], + 'version_latest' => $version_latest, + 'url' => $data['PluginURI'], + 'author_name' => $data['AuthorName'], + 'author_url' => esc_url_raw( $data['AuthorURI'] ), + 'network_activated' => $data['Network'], + ); + } + + /** + * Get a list of Dropins and MU plugins. + * + * @since 3.6.0 + * @return array + */ + public function get_dropins_mu_plugins() { + $dropins = get_dropins(); + $plugins = array( + 'dropins' => array(), + 'mu_plugins' => array(), + ); + foreach ( $dropins as $key => $dropin ) { + $plugins['dropins'][] = array( + 'plugin' => $key, + 'name' => $dropin['Name'], + ); + } + + $mu_plugins = get_mu_plugins(); + foreach ( $mu_plugins as $plugin => $mu_plugin ) { + $plugins['mu_plugins'][] = array( + 'plugin' => $plugin, + 'name' => $mu_plugin['Name'], + 'version' => $mu_plugin['Version'], + 'url' => $mu_plugin['PluginURI'], + 'author_name' => $mu_plugin['AuthorName'], + 'author_url' => esc_url_raw( $mu_plugin['AuthorURI'] ), + ); + } + return $plugins; + } + + /** + * Get info on the current active theme, info on parent theme (if presnet) + * and a list of template overrides. + * + * @return array + */ + public function get_theme_info() { + $active_theme = wp_get_theme(); + + // Get parent theme info if this theme is a child theme, otherwise + // pass empty info in the response. + if ( is_child_theme() ) { + $parent_theme = wp_get_theme( $active_theme->template ); + $parent_theme_info = array( + 'parent_name' => $parent_theme->name, + 'parent_version' => $parent_theme->version, + 'parent_version_latest' => WC_Admin_Status::get_latest_theme_version( $parent_theme ), + 'parent_author_url' => $parent_theme->{'Author URI'}, + ); + } else { + $parent_theme_info = array( + 'parent_name' => '', + 'parent_version' => '', + 'parent_version_latest' => '', + 'parent_author_url' => '', + ); + } + + /** + * Scan the theme directory for all WC templates to see if our theme + * overrides any of them. + */ + $override_files = array(); + $outdated_templates = false; + $scan_files = WC_Admin_Status::scan_template_files( WC()->plugin_path() . '/templates/' ); + foreach ( $scan_files as $file ) { + $located = apply_filters( 'wc_get_template', $file, $file, array(), WC()->template_path(), WC()->plugin_path() . '/templates/' ); + + if ( file_exists( $located ) ) { + $theme_file = $located; + } elseif ( file_exists( get_stylesheet_directory() . '/' . $file ) ) { + $theme_file = get_stylesheet_directory() . '/' . $file; + } elseif ( file_exists( get_stylesheet_directory() . '/' . WC()->template_path() . $file ) ) { + $theme_file = get_stylesheet_directory() . '/' . WC()->template_path() . $file; + } elseif ( file_exists( get_template_directory() . '/' . $file ) ) { + $theme_file = get_template_directory() . '/' . $file; + } elseif ( file_exists( get_template_directory() . '/' . WC()->template_path() . $file ) ) { + $theme_file = get_template_directory() . '/' . WC()->template_path() . $file; + } else { + $theme_file = false; + } + + if ( ! empty( $theme_file ) ) { + $core_version = WC_Admin_Status::get_file_version( WC()->plugin_path() . '/templates/' . $file ); + $theme_version = WC_Admin_Status::get_file_version( $theme_file ); + if ( $core_version && ( empty( $theme_version ) || version_compare( $theme_version, $core_version, '<' ) ) ) { + if ( ! $outdated_templates ) { + $outdated_templates = true; + } + } + $override_files[] = array( + 'file' => str_replace( WP_CONTENT_DIR . '/themes/', '', $theme_file ), + 'version' => $theme_version, + 'core_version' => $core_version, + ); + } + } + + $active_theme_info = array( + 'name' => $active_theme->name, + 'version' => $active_theme->version, + 'version_latest' => WC_Admin_Status::get_latest_theme_version( $active_theme ), + 'author_url' => esc_url_raw( $active_theme->{'Author URI'} ), + 'is_child_theme' => is_child_theme(), + 'has_woocommerce_support' => current_theme_supports( 'woocommerce' ), + 'has_woocommerce_file' => ( file_exists( get_stylesheet_directory() . '/woocommerce.php' ) || file_exists( get_template_directory() . '/woocommerce.php' ) ), + 'has_outdated_templates' => $outdated_templates, + 'overrides' => $override_files, + ); + + return array_merge( $active_theme_info, $parent_theme_info ); + } + + /** + * Get some setting values for the site that are useful for debugging + * purposes. For full settings access, use the settings api. + * + * @return array + */ + public function get_settings() { + // Get a list of terms used for product/order taxonomies. + $term_response = array(); + $terms = get_terms( 'product_type', array( 'hide_empty' => 0 ) ); + foreach ( $terms as $term ) { + $term_response[ $term->slug ] = strtolower( $term->name ); + } + + // Get a list of terms used for product visibility. + $product_visibility_terms = array(); + $terms = get_terms( 'product_visibility', array( 'hide_empty' => 0 ) ); + foreach ( $terms as $term ) { + $product_visibility_terms[ $term->slug ] = strtolower( $term->name ); + } + + // Check if WooCommerce.com account is connected. + $woo_com_connected = 'no'; + $helper_options = get_option( 'woocommerce_helper_data', array() ); + if ( array_key_exists( 'auth', $helper_options ) && ! empty( $helper_options['auth'] ) ) { + $woo_com_connected = 'yes'; + } + + // Return array of useful settings for debugging. + return array( + 'api_enabled' => 'yes' === get_option( 'woocommerce_api_enabled' ), + 'force_ssl' => 'yes' === get_option( 'woocommerce_force_ssl_checkout' ), + 'currency' => get_woocommerce_currency(), + 'currency_symbol' => get_woocommerce_currency_symbol(), + 'currency_position' => get_option( 'woocommerce_currency_pos' ), + 'thousand_separator' => wc_get_price_thousand_separator(), + 'decimal_separator' => wc_get_price_decimal_separator(), + 'number_of_decimals' => wc_get_price_decimals(), + 'geolocation_enabled' => in_array( get_option( 'woocommerce_default_customer_address' ), array( 'geolocation_ajax', 'geolocation' ), true ), + 'taxonomies' => $term_response, + 'product_visibility_terms' => $product_visibility_terms, + 'woocommerce_com_connected' => $woo_com_connected, + ); + } + + /** + * Returns security tips. + * + * @return array + */ + public function get_security_info() { + $check_page = wc_get_page_permalink( 'shop' ); + return array( + 'secure_connection' => 'https' === substr( $check_page, 0, 5 ), + 'hide_errors' => ! ( defined( 'WP_DEBUG' ) && defined( 'WP_DEBUG_DISPLAY' ) && WP_DEBUG && WP_DEBUG_DISPLAY ) || 0 === intval( ini_get( 'display_errors' ) ), + ); + } + + /** + * Returns a mini-report on WC pages and if they are configured correctly: + * Present, visible, and including the correct shortcode. + * + * @return array + */ + public function get_pages() { + // WC pages to check against. + $check_pages = array( + _x( 'Shop base', 'Page setting', 'woocommerce' ) => array( + 'option' => 'woocommerce_shop_page_id', + 'shortcode' => '', + ), + _x( 'Cart', 'Page setting', 'woocommerce' ) => array( + 'option' => 'woocommerce_cart_page_id', + 'shortcode' => '[' . apply_filters( 'woocommerce_cart_shortcode_tag', 'woocommerce_cart' ) . ']', + ), + _x( 'Checkout', 'Page setting', 'woocommerce' ) => array( + 'option' => 'woocommerce_checkout_page_id', + 'shortcode' => '[' . apply_filters( 'woocommerce_checkout_shortcode_tag', 'woocommerce_checkout' ) . ']', + ), + _x( 'My account', 'Page setting', 'woocommerce' ) => array( + 'option' => 'woocommerce_myaccount_page_id', + 'shortcode' => '[' . apply_filters( 'woocommerce_my_account_shortcode_tag', 'woocommerce_my_account' ) . ']', + ), + _x( 'Terms and conditions', 'Page setting', 'woocommerce' ) => array( + 'option' => 'woocommerce_terms_page_id', + 'shortcode' => '', + ), + ); + + $pages_output = array(); + foreach ( $check_pages as $page_name => $values ) { + $page_id = get_option( $values['option'] ); + $page_set = false; + $page_exists = false; + $page_visible = false; + $shortcode_present = false; + $shortcode_required = false; + + // Page checks. + if ( $page_id ) { + $page_set = true; + } + if ( get_post( $page_id ) ) { + $page_exists = true; + } + if ( 'publish' === get_post_status( $page_id ) ) { + $page_visible = true; + } + + // Shortcode checks. + if ( $values['shortcode'] && get_post( $page_id ) ) { + $shortcode_required = true; + $page = get_post( $page_id ); + if ( strstr( $page->post_content, $values['shortcode'] ) ) { + $shortcode_present = true; + } + } + + // Wrap up our findings into an output array. + $pages_output[] = array( + 'page_name' => $page_name, + 'page_id' => $page_id, + 'page_set' => $page_set, + 'page_exists' => $page_exists, + 'page_visible' => $page_visible, + 'shortcode' => $values['shortcode'], + 'shortcode_required' => $shortcode_required, + 'shortcode_present' => $shortcode_present, + ); + } + + return $pages_output; + } + + /** + * Get any query params needed. + * + * @return array + */ + public function get_collection_params() { + return array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ); + } + + /** + * Prepare the system status response + * + * @param array $system_status System status data. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response + */ + public function prepare_item_for_response( $system_status, $request ) { + $data = $this->add_additional_fields_to_object( $system_status, $request ); + $data = $this->filter_response_by_context( $data, 'view' ); + + $response = rest_ensure_response( $data ); + + /** + * Filter the system status returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param mixed $system_status System status + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'woocommerce_rest_prepare_system_status', $response, $system_status, $request ); + } +} diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-tax-classes-v2-controller.php b/includes/rest-api/Controllers/Version2/class-wc-rest-tax-classes-v2-controller.php new file mode 100644 index 00000000000..dc1cdd24bd3 --- /dev/null +++ b/includes/rest-api/Controllers/Version2/class-wc-rest-tax-classes-v2-controller.php @@ -0,0 +1,27 @@ +/deliveries endpoint. + * + * @package Automattic/WooCommerce/RestApi + * @since 2.6.0 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * REST API Webhook Deliveries controller class. + * + * @deprecated 3.3.0 Webhooks deliveries logs now uses logging system. + * @package Automattic/WooCommerce/RestApi + * @extends WC_REST_Webhook_Deliveries_V1_Controller + */ +class WC_REST_Webhook_Deliveries_V2_Controller extends WC_REST_Webhook_Deliveries_V1_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + protected $namespace = 'wc/v2'; + + /** + * Prepare a single webhook delivery output for response. + * + * @param stdClass $log Delivery log object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response + */ + public function prepare_item_for_response( $log, $request ) { + $data = (array) $log; + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $log ) ); + + /** + * Filter webhook delivery object returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param stdClass $log Delivery log object used to create response. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'woocommerce_rest_prepare_webhook_delivery', $response, $log, $request ); + } + + /** + * Get the Webhook's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'webhook_delivery', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'duration' => array( + 'description' => __( 'The delivery duration, in seconds.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'summary' => array( + 'description' => __( 'A friendly summary of the response including the HTTP response code, message, and body.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'request_url' => array( + 'description' => __( 'The URL where the webhook was delivered.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'request_headers' => array( + 'description' => __( 'Request headers.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view' ), + 'readonly' => true, + 'items' => array( + 'type' => 'string', + ), + ), + 'request_body' => array( + 'description' => __( 'Request body.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'response_code' => array( + 'description' => __( 'The HTTP response code from the receiving server.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'response_message' => array( + 'description' => __( 'The HTTP response message from the receiving server.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'response_headers' => array( + 'description' => __( 'Array of the response headers from the receiving server.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view' ), + 'readonly' => true, + 'items' => array( + 'type' => 'string', + ), + ), + 'response_body' => array( + 'description' => __( 'The response body from the receiving server.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( "The date the webhook delivery was logged, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created_gmt' => array( + 'description' => __( 'The date the webhook delivery was logged, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/rest-api/Controllers/Version2/class-wc-rest-webhooks-v2-controller.php b/includes/rest-api/Controllers/Version2/class-wc-rest-webhooks-v2-controller.php new file mode 100644 index 00000000000..ede2c1d121a --- /dev/null +++ b/includes/rest-api/Controllers/Version2/class-wc-rest-webhooks-v2-controller.php @@ -0,0 +1,182 @@ +post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $data = array( + 'id' => $webhook->get_id(), + 'name' => $webhook->get_name(), + 'status' => $webhook->get_status(), + 'topic' => $webhook->get_topic(), + 'resource' => $webhook->get_resource(), + 'event' => $webhook->get_event(), + 'hooks' => $webhook->get_hooks(), + 'delivery_url' => $webhook->get_delivery_url(), + 'date_created' => wc_rest_prepare_date_response( $webhook->get_date_created(), false ), + 'date_created_gmt' => wc_rest_prepare_date_response( $webhook->get_date_created() ), + 'date_modified' => wc_rest_prepare_date_response( $webhook->get_date_modified(), false ), + 'date_modified_gmt' => wc_rest_prepare_date_response( $webhook->get_date_modified() ), + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $webhook->get_id(), $request ) ); + + /** + * Filter webhook object returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param WC_Webhook $webhook Webhook object used to create response. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->post_type}", $response, $webhook, $request ); + } + + /** + * Get the default REST API version. + * + * @since 3.0.0 + * @return string + */ + protected function get_default_api_version() { + return 'wp_api_v2'; + } + + /** + * Get the Webhook's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'webhook', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'A friendly name for the webhook.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'status' => array( + 'description' => __( 'Webhook status.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'active', + 'enum' => array_keys( wc_get_webhook_statuses() ), + 'context' => array( 'view', 'edit' ), + ), + 'topic' => array( + 'description' => __( 'Webhook topic.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'resource' => array( + 'description' => __( 'Webhook resource.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'event' => array( + 'description' => __( 'Webhook event.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'hooks' => array( + 'description' => __( 'WooCommerce action names associated with the webhook.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'string', + ), + ), + 'delivery_url' => array( + 'description' => __( 'The URL where the webhook payload is delivered.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'secret' => array( + 'description' => __( "Secret key used to generate a hash of the delivered webhook and provided in the request headers. This will default to a MD5 hash from the current user's ID|username if not provided.", 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'edit' ), + ), + 'date_created' => array( + 'description' => __( "The date the webhook was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created_gmt' => array( + 'description' => __( 'The date the webhook was created, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the webhook was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified_gmt' => array( + 'description' => __( 'The date the webhook was last modified, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-controller.php new file mode 100644 index 00000000000..9365efbe7b3 --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-controller.php @@ -0,0 +1,532 @@ + + * + * NOTE THAT ONLY CODE RELEVANT FOR MOST ENDPOINTS SHOULD BE INCLUDED INTO THIS CLASS. + * If necessary extend this class and create new abstract classes like `WC_REST_CRUD_Controller` or `WC_REST_Terms_Controller`. + * + * @class WC_REST_Controller + * @package Automattic/WooCommerce/RestApi + * @see https://developer.wordpress.org/rest-api/extending-the-rest-api/controller-classes/ + */ + +if ( ! defined( 'ABSPATH' ) ) { + exit; +} + +/** + * Abstract Rest Controller Class + * + * @package Automattic/WooCommerce/RestApi + * @extends WP_REST_Controller + * @version 2.6.0 + */ +abstract class WC_REST_Controller extends WP_REST_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + protected $namespace = 'wc/v1'; + + /** + * Route base. + * + * @var string + */ + protected $rest_base = ''; + + /** + * Add the schema from additional fields to an schema array. + * + * The type of object is inferred from the passed schema. + * + * @param array $schema Schema array. + * + * @return array + */ + protected function add_additional_fields_schema( $schema ) { + if ( empty( $schema['title'] ) ) { + return $schema; + } + + /** + * Can't use $this->get_object_type otherwise we cause an inf loop. + */ + $object_type = $schema['title']; + + $additional_fields = $this->get_additional_fields( $object_type ); + + foreach ( $additional_fields as $field_name => $field_options ) { + if ( ! $field_options['schema'] ) { + continue; + } + + $schema['properties'][ $field_name ] = $field_options['schema']; + } + + $schema['properties'] = apply_filters( 'woocommerce_rest_' . $object_type . '_schema', $schema['properties'] ); + + return $schema; + } + + /** + * Compatibility functions for WP 5.5, since custom types are not supported anymore. + * See @link https://core.trac.wordpress.org/changeset/48306 + * + * @param string $method Optional. HTTP method of the request. + * + * @return array Endpoint arguments. + */ + public function get_endpoint_args_for_item_schema( $method = WP_REST_Server::CREATABLE ) { + + $endpoint_args = parent::get_endpoint_args_for_item_schema( $method ); + + if ( false === strpos( WP_REST_Server::EDITABLE, $method ) ) { + return $endpoint_args; + } + + foreach ( $endpoint_args as $field_id => $params ) { + /** + * Custom types are not supported as of WP 5.5, this translates type => 'date-time' to type => 'string' with format date-time. + */ + if ( 'date-time' === $params['type'] ) { + $endpoint_args[ $field_id ]['type'] = 'string'; + $endpoint_args[ $field_id ]['format'] = 'date-time'; + } + } + + return $endpoint_args; + } + + /** + * Get normalized rest base. + * + * @return string + */ + protected function get_normalized_rest_base() { + return preg_replace( '/\(.*\)\//i', '', $this->rest_base ); + } + + /** + * Check batch limit. + * + * @param array $items Request items. + * @return bool|WP_Error + */ + protected function check_batch_limit( $items ) { + $limit = apply_filters( 'woocommerce_rest_batch_items_limit', 100, $this->get_normalized_rest_base() ); + $total = 0; + + if ( ! empty( $items['create'] ) ) { + $total += count( $items['create'] ); + } + + if ( ! empty( $items['update'] ) ) { + $total += count( $items['update'] ); + } + + if ( ! empty( $items['delete'] ) ) { + $total += count( $items['delete'] ); + } + + if ( $total > $limit ) { + /* translators: %s: items limit */ + return new WP_Error( 'woocommerce_rest_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'woocommerce' ), $limit ), array( 'status' => 413 ) ); + } + + return true; + } + + /** + * Bulk create, update and delete items. + * + * @param WP_REST_Request $request Full details about the request. + * @return array Of WP_Error or WP_REST_Response. + */ + public function batch_items( $request ) { + /** + * REST Server + * + * @var WP_REST_Server $wp_rest_server + */ + global $wp_rest_server; + + // Get the request params. + $items = array_filter( $request->get_params() ); + $query = $request->get_query_params(); + $response = array(); + + // Check batch limit. + $limit = $this->check_batch_limit( $items ); + if ( is_wp_error( $limit ) ) { + return $limit; + } + + if ( ! empty( $items['create'] ) ) { + foreach ( $items['create'] as $item ) { + $_item = new WP_REST_Request( 'POST' ); + + // Default parameters. + $defaults = array(); + $schema = $this->get_public_item_schema(); + foreach ( $schema['properties'] as $arg => $options ) { + if ( isset( $options['default'] ) ) { + $defaults[ $arg ] = $options['default']; + } + } + $_item->set_default_params( $defaults ); + + // Set request parameters. + $_item->set_body_params( $item ); + + // Set query (GET) parameters. + $_item->set_query_params( $query ); + + $_response = $this->create_item( $_item ); + + if ( is_wp_error( $_response ) ) { + $response['create'][] = array( + 'id' => 0, + 'error' => array( + 'code' => $_response->get_error_code(), + 'message' => $_response->get_error_message(), + 'data' => $_response->get_error_data(), + ), + ); + } else { + $response['create'][] = $wp_rest_server->response_to_data( $_response, '' ); + } + } + } + + if ( ! empty( $items['update'] ) ) { + foreach ( $items['update'] as $item ) { + $_item = new WP_REST_Request( 'PUT' ); + $_item->set_body_params( $item ); + $_response = $this->update_item( $_item ); + + if ( is_wp_error( $_response ) ) { + $response['update'][] = array( + 'id' => $item['id'], + 'error' => array( + 'code' => $_response->get_error_code(), + 'message' => $_response->get_error_message(), + 'data' => $_response->get_error_data(), + ), + ); + } else { + $response['update'][] = $wp_rest_server->response_to_data( $_response, '' ); + } + } + } + + if ( ! empty( $items['delete'] ) ) { + foreach ( $items['delete'] as $id ) { + $id = (int) $id; + + if ( 0 === $id ) { + continue; + } + + $_item = new WP_REST_Request( 'DELETE' ); + $_item->set_query_params( + array( + 'id' => $id, + 'force' => true, + ) + ); + $_response = $this->delete_item( $_item ); + + if ( is_wp_error( $_response ) ) { + $response['delete'][] = array( + 'id' => $id, + 'error' => array( + 'code' => $_response->get_error_code(), + 'message' => $_response->get_error_message(), + 'data' => $_response->get_error_data(), + ), + ); + } else { + $response['delete'][] = $wp_rest_server->response_to_data( $_response, '' ); + } + } + } + + return $response; + } + + /** + * Validate a text value for a text based setting. + * + * @since 3.0.0 + * @param string $value Value. + * @param array $setting Setting. + * @return string + */ + public function validate_setting_text_field( $value, $setting ) { + $value = is_null( $value ) ? '' : $value; + return wp_kses_post( trim( stripslashes( $value ) ) ); + } + + /** + * Validate select based settings. + * + * @since 3.0.0 + * @param string $value Value. + * @param array $setting Setting. + * @return string|WP_Error + */ + public function validate_setting_select_field( $value, $setting ) { + if ( array_key_exists( $value, $setting['options'] ) ) { + return $value; + } else { + return new WP_Error( 'rest_setting_value_invalid', __( 'An invalid setting value was passed.', 'woocommerce' ), array( 'status' => 400 ) ); + } + } + + /** + * Validate multiselect based settings. + * + * @since 3.0.0 + * @param array $values Values. + * @param array $setting Setting. + * @return array|WP_Error + */ + public function validate_setting_multiselect_field( $values, $setting ) { + if ( empty( $values ) ) { + return array(); + } + + if ( ! is_array( $values ) ) { + return new WP_Error( 'rest_setting_value_invalid', __( 'An invalid setting value was passed.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $final_values = array(); + foreach ( $values as $value ) { + if ( array_key_exists( $value, $setting['options'] ) ) { + $final_values[] = $value; + } + } + + return $final_values; + } + + /** + * Validate image_width based settings. + * + * @since 3.0.0 + * @param array $values Values. + * @param array $setting Setting. + * @return string|WP_Error + */ + public function validate_setting_image_width_field( $values, $setting ) { + if ( ! is_array( $values ) ) { + return new WP_Error( 'rest_setting_value_invalid', __( 'An invalid setting value was passed.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $current = $setting['value']; + if ( isset( $values['width'] ) ) { + $current['width'] = intval( $values['width'] ); + } + if ( isset( $values['height'] ) ) { + $current['height'] = intval( $values['height'] ); + } + if ( isset( $values['crop'] ) ) { + $current['crop'] = (bool) $values['crop']; + } + return $current; + } + + /** + * Validate radio based settings. + * + * @since 3.0.0 + * @param string $value Value. + * @param array $setting Setting. + * @return string|WP_Error + */ + public function validate_setting_radio_field( $value, $setting ) { + return $this->validate_setting_select_field( $value, $setting ); + } + + /** + * Validate checkbox based settings. + * + * @since 3.0.0 + * @param string $value Value. + * @param array $setting Setting. + * @return string|WP_Error + */ + public function validate_setting_checkbox_field( $value, $setting ) { + if ( in_array( $value, array( 'yes', 'no' ) ) ) { + return $value; + } elseif ( empty( $value ) ) { + $value = isset( $setting['default'] ) ? $setting['default'] : 'no'; + return $value; + } else { + return new WP_Error( 'rest_setting_value_invalid', __( 'An invalid setting value was passed.', 'woocommerce' ), array( 'status' => 400 ) ); + } + } + + /** + * Validate textarea based settings. + * + * @since 3.0.0 + * @param string $value Value. + * @param array $setting Setting. + * @return string + */ + public function validate_setting_textarea_field( $value, $setting ) { + $value = is_null( $value ) ? '' : $value; + return wp_kses( + trim( stripslashes( $value ) ), + array_merge( + array( + 'iframe' => array( + 'src' => true, + 'style' => true, + 'id' => true, + 'class' => true, + ), + ), + wp_kses_allowed_html( 'post' ) + ) + ); + } + + /** + * Add meta query. + * + * @since 3.0.0 + * @param array $args Query args. + * @param array $meta_query Meta query. + * @return array + */ + protected function add_meta_query( $args, $meta_query ) { + if ( empty( $args['meta_query'] ) ) { + $args['meta_query'] = array(); + } + + $args['meta_query'][] = $meta_query; + + return $args['meta_query']; + } + + /** + * Get the batch schema, conforming to JSON Schema. + * + * @return array + */ + public function get_public_batch_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'batch', + 'type' => 'object', + 'properties' => array( + 'create' => array( + 'description' => __( 'List of created resources.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + ), + ), + 'update' => array( + 'description' => __( 'List of updated resources.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + ), + ), + 'delete' => array( + 'description' => __( 'List of delete resources.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'integer', + ), + ), + ), + ); + + return $schema; + } + + /** + * Gets an array of fields to be included on the response. + * + * Included fields are based on item schema and `_fields=` request argument. + * Updated from WordPress 5.3, included into this class to support old versions. + * + * @since 3.5.0 + * @param WP_REST_Request $request Full details about the request. + * @return array Fields to be included in the response. + */ + public function get_fields_for_response( $request ) { + $schema = $this->get_item_schema(); + $properties = isset( $schema['properties'] ) ? $schema['properties'] : array(); + + $additional_fields = $this->get_additional_fields(); + foreach ( $additional_fields as $field_name => $field_options ) { + // For back-compat, include any field with an empty schema + // because it won't be present in $this->get_item_schema(). + if ( is_null( $field_options['schema'] ) ) { + $properties[ $field_name ] = $field_options; + } + } + + // Exclude fields that specify a different context than the request context. + $context = $request['context']; + if ( $context ) { + foreach ( $properties as $name => $options ) { + if ( ! empty( $options['context'] ) && ! in_array( $context, $options['context'], true ) ) { + unset( $properties[ $name ] ); + } + } + } + + $fields = array_keys( $properties ); + + if ( ! isset( $request['_fields'] ) ) { + return $fields; + } + $requested_fields = wp_parse_list( $request['_fields'] ); + if ( 0 === count( $requested_fields ) ) { + return $fields; + } + // Trim off outside whitespace from the comma delimited list. + $requested_fields = array_map( 'trim', $requested_fields ); + // Always persist 'id', because it can be needed for add_additional_fields_to_object(). + if ( in_array( 'id', $fields, true ) ) { + $requested_fields[] = 'id'; + } + // Return the list of all requested fields which appear in the schema. + return array_reduce( + $requested_fields, + function( $response_fields, $field ) use ( $fields ) { + if ( in_array( $field, $fields, true ) ) { + $response_fields[] = $field; + return $response_fields; + } + // Check for nested fields if $field is not a direct match. + $nested_fields = explode( '.', $field ); + // A nested field is included so long as its top-level property is + // present in the schema. + if ( in_array( $nested_fields[0], $fields, true ) ) { + $response_fields[] = $field; + } + return $response_fields; + }, + array() + ); + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-coupons-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-coupons-controller.php new file mode 100644 index 00000000000..0df4a6a3395 --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-coupons-controller.php @@ -0,0 +1,27 @@ + 405 ) ); + } + + /** + * Check if a given request has access to read an item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + $object = $this->get_object( (int) $request['id'] ); + + if ( $object && 0 !== $object->get_id() && ! wc_rest_check_post_permissions( $this->post_type, 'read', $object->get_id() ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to update an item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function update_item_permissions_check( $request ) { + $object = $this->get_object( (int) $request['id'] ); + + if ( $object && 0 !== $object->get_id() && ! wc_rest_check_post_permissions( $this->post_type, 'edit', $object->get_id() ) ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to delete an item. + * + * @param WP_REST_Request $request Full details about the request. + * @return bool|WP_Error + */ + public function delete_item_permissions_check( $request ) { + $object = $this->get_object( (int) $request['id'] ); + + if ( $object && 0 !== $object->get_id() && ! wc_rest_check_post_permissions( $this->post_type, 'delete', $object->get_id() ) ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + + + /** + * Get object permalink. + * + * @param object $object Object. + * @return string + */ + protected function get_permalink( $object ) { + return ''; + } + + /** + * Prepares the object for the REST response. + * + * @since 3.0.0 + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + * @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure. + */ + protected function prepare_object_for_response( $object, $request ) { + // translators: %s: Class method name. + return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass.", 'woocommerce' ), __METHOD__ ), array( 'status' => 405 ) ); + } + + /** + * Prepares one object for create or update operation. + * + * @since 3.0.0 + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + * @return WP_Error|WC_Data The prepared item, or WP_Error object on failure. + */ + protected function prepare_object_for_database( $request, $creating = false ) { + // translators: %s: Class method name. + return new WP_Error( 'invalid-method', sprintf( __( "Method '%s' not implemented. Must be overridden in subclass.", 'woocommerce' ), __METHOD__ ), array( 'status' => 405 ) ); + } + + /** + * Get a single item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + $object = $this->get_object( (int) $request['id'] ); + + if ( ! $object || 0 === $object->get_id() ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $data = $this->prepare_object_for_response( $object, $request ); + $response = rest_ensure_response( $data ); + + if ( $this->public ) { + $response->link_header( 'alternate', $this->get_permalink( $object ), array( 'type' => 'text/html' ) ); + } + + return $response; + } + + /** + * Save an object data. + * + * @since 3.0.0 + * @param WP_REST_Request $request Full details about the request. + * @param bool $creating If is creating a new object. + * @return WC_Data|WP_Error + */ + protected function save_object( $request, $creating = false ) { + try { + $object = $this->prepare_object_for_database( $request, $creating ); + + if ( is_wp_error( $object ) ) { + return $object; + } + + $object->save(); + + return $this->get_object( $object->get_id() ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); + } catch ( WC_REST_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Create a single item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + if ( ! empty( $request['id'] ) ) { + /* translators: %s: post type */ + return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) ); + } + + $object = $this->save_object( $request, true ); + + if ( is_wp_error( $object ) ) { + return $object; + } + + try { + $this->update_additional_fields_for_object( $object, $request ); + + /** + * Fires after a single object is created or updated via the REST API. + * + * @param WC_Data $object Inserted object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating object, false when updating. + */ + do_action( "woocommerce_rest_insert_{$this->post_type}_object", $object, $request, true ); + } catch ( WC_Data_Exception $e ) { + $object->delete(); + return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); + } catch ( WC_REST_Exception $e ) { + $object->delete(); + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_object_for_response( $object, $request ); + $response = rest_ensure_response( $response ); + $response->set_status( 201 ); + $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $object->get_id() ) ) ); + + return $response; + } + + /** + * Update a single post. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function update_item( $request ) { + $object = $this->get_object( (int) $request['id'] ); + + if ( ! $object || 0 === $object->get_id() ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $object = $this->save_object( $request, false ); + + if ( is_wp_error( $object ) ) { + return $object; + } + + try { + $this->update_additional_fields_for_object( $object, $request ); + + /** + * Fires after a single object is created or updated via the REST API. + * + * @param WC_Data $object Inserted object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating object, false when updating. + */ + do_action( "woocommerce_rest_insert_{$this->post_type}_object", $object, $request, false ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); + } catch ( WC_REST_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_object_for_response( $object, $request ); + return rest_ensure_response( $response ); + } + + /** + * Prepare objects query. + * + * @since 3.0.0 + * @param WP_REST_Request $request Full details about the request. + * @return array + */ + protected function prepare_objects_query( $request ) { + $args = array(); + $args['offset'] = $request['offset']; + $args['order'] = $request['order']; + $args['orderby'] = $request['orderby']; + $args['paged'] = $request['page']; + $args['post__in'] = $request['include']; + $args['post__not_in'] = $request['exclude']; + $args['posts_per_page'] = $request['per_page']; + $args['name'] = $request['slug']; + $args['post_parent__in'] = $request['parent']; + $args['post_parent__not_in'] = $request['parent_exclude']; + $args['s'] = $request['search']; + + if ( 'date' === $args['orderby'] ) { + $args['orderby'] = 'date ID'; + } + + $args['date_query'] = array(); + // Set before into date query. Date query must be specified as an array of an array. + if ( isset( $request['before'] ) ) { + $args['date_query'][0]['before'] = $request['before']; + } + + // Set after into date query. Date query must be specified as an array of an array. + if ( isset( $request['after'] ) ) { + $args['date_query'][0]['after'] = $request['after']; + } + + // Force the post_type argument, since it's not a user input variable. + $args['post_type'] = $this->post_type; + + /** + * Filter the query arguments for a request. + * + * Enables adding extra arguments or setting defaults for a post + * collection request. + * + * @param array $args Key value array of query var to query value. + * @param WP_REST_Request $request The request used. + */ + $args = apply_filters( "woocommerce_rest_{$this->post_type}_object_query", $args, $request ); + + return $this->prepare_items_query( $args, $request ); + } + + /** + * Get objects. + * + * @since 3.0.0 + * @param array $query_args Query args. + * @return array + */ + protected function get_objects( $query_args ) { + $query = new WP_Query(); + $result = $query->query( $query_args ); + + $total_posts = $query->found_posts; + if ( $total_posts < 1 ) { + // Out-of-bounds, run the query again without LIMIT for total count. + unset( $query_args['paged'] ); + $count_query = new WP_Query(); + $count_query->query( $query_args ); + $total_posts = $count_query->found_posts; + } + + return array( + 'objects' => array_filter( array_map( array( $this, 'get_object' ), $result ) ), + 'total' => (int) $total_posts, + 'pages' => (int) ceil( $total_posts / (int) $query->query_vars['posts_per_page'] ), + ); + } + + /** + * Get a collection of posts. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + $query_args = $this->prepare_objects_query( $request ); + $query_results = $this->get_objects( $query_args ); + + $objects = array(); + foreach ( $query_results['objects'] as $object ) { + if ( ! wc_rest_check_post_permissions( $this->post_type, 'read', $object->get_id() ) ) { + continue; + } + + $data = $this->prepare_object_for_response( $object, $request ); + $objects[] = $this->prepare_response_for_collection( $data ); + } + + $page = (int) $query_args['paged']; + $max_pages = $query_results['pages']; + + $response = rest_ensure_response( $objects ); + $response->header( 'X-WP-Total', $query_results['total'] ); + $response->header( 'X-WP-TotalPages', (int) $max_pages ); + + $base = $this->rest_base; + $attrib_prefix = '(?P<'; + if ( strpos( $base, $attrib_prefix ) !== false ) { + $attrib_names = array(); + preg_match( '/\(\?P<[^>]+>.*\)/', $base, $attrib_names, PREG_OFFSET_CAPTURE ); + foreach ( $attrib_names as $attrib_name_match ) { + $beginning_offset = strlen( $attrib_prefix ); + $attrib_name_end = strpos( $attrib_name_match[0], '>', $attrib_name_match[1] ); + $attrib_name = substr( $attrib_name_match[0], $beginning_offset, $attrib_name_end - $beginning_offset ); + if ( isset( $request[ $attrib_name ] ) ) { + $base = str_replace( "(?P<$attrib_name>[\d]+)", $request[ $attrib_name ], $base ); + } + } + } + $base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $base ) ) ); + + if ( $page > 1 ) { + $prev_page = $page - 1; + if ( $prev_page > $max_pages ) { + $prev_page = $max_pages; + } + $prev_link = add_query_arg( 'page', $prev_page, $base ); + $response->link_header( 'prev', $prev_link ); + } + if ( $max_pages > $page ) { + $next_page = $page + 1; + $next_link = add_query_arg( 'page', $next_page, $base ); + $response->link_header( 'next', $next_link ); + } + + return $response; + } + + /** + * Delete a single item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error + */ + public function delete_item( $request ) { + $force = (bool) $request['force']; + $object = $this->get_object( (int) $request['id'] ); + $result = false; + + if ( ! $object || 0 === $object->get_id() ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $supports_trash = EMPTY_TRASH_DAYS > 0 && is_callable( array( $object, 'get_status' ) ); + + /** + * Filter whether an object is trashable. + * + * Return false to disable trash support for the object. + * + * @param boolean $supports_trash Whether the object type support trashing. + * @param WC_Data $object The object being considered for trashing support. + */ + $supports_trash = apply_filters( "woocommerce_rest_{$this->post_type}_object_trashable", $supports_trash, $object ); + + if ( ! wc_rest_check_post_permissions( $this->post_type, 'delete', $object->get_id() ) ) { + /* translators: %s: post type */ + return new WP_Error( "woocommerce_rest_user_cannot_delete_{$this->post_type}", sprintf( __( 'Sorry, you are not allowed to delete %s.', 'woocommerce' ), $this->post_type ), array( 'status' => rest_authorization_required_code() ) ); + } + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_object_for_response( $object, $request ); + + // If we're forcing, then delete permanently. + if ( $force ) { + $object->delete( true ); + $result = 0 === $object->get_id(); + } else { + // If we don't support trashing for this type, error out. + if ( ! $supports_trash ) { + /* translators: %s: post type */ + return new WP_Error( 'woocommerce_rest_trash_not_supported', sprintf( __( 'The %s does not support trashing.', 'woocommerce' ), $this->post_type ), array( 'status' => 501 ) ); + } + + // Otherwise, only trash if we haven't already. + if ( is_callable( array( $object, 'get_status' ) ) ) { + if ( 'trash' === $object->get_status() ) { + /* translators: %s: post type */ + return new WP_Error( 'woocommerce_rest_already_trashed', sprintf( __( 'The %s has already been deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 410 ) ); + } + + $object->delete(); + $result = 'trash' === $object->get_status(); + } + } + + if ( ! $result ) { + /* translators: %s: post type */ + return new WP_Error( 'woocommerce_rest_cannot_delete', sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 500 ) ); + } + + /** + * Fires after a single object is deleted or trashed via the REST API. + * + * @param WC_Data $object The deleted or trashed object. + * @param WP_REST_Response $response The response data. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( "woocommerce_rest_delete_{$this->post_type}_object", $object, $response, $request ); + + return $response; + } + + /** + * Prepare links for the request. + * + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + * @return array Links for the given post. + */ + protected function prepare_links( $object, $request ) { + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $object->get_id() ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + + return $links; + } + + /** + * Get the query params for collections of attachments. + * + * @return array + */ + public function get_collection_params() { + $params = array(); + $params['context'] = $this->get_context_param(); + $params['context']['default'] = 'view'; + + $params['page'] = array( + 'description' => __( 'Current page of the collection.', 'woocommerce' ), + 'type' => 'integer', + 'default' => 1, + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + 'minimum' => 1, + ); + $params['per_page'] = array( + 'description' => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ), + 'type' => 'integer', + 'default' => 10, + 'minimum' => 1, + 'maximum' => 100, + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['search'] = array( + 'description' => __( 'Limit results to those matching a string.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['after'] = array( + 'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'date-time', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['before'] = array( + 'description' => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'date-time', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['exclude'] = array( + 'description' => __( 'Ensure result set excludes specific IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'default' => array(), + 'sanitize_callback' => 'wp_parse_id_list', + ); + $params['include'] = array( + 'description' => __( 'Limit result set to specific ids.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'default' => array(), + 'sanitize_callback' => 'wp_parse_id_list', + ); + $params['offset'] = array( + 'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['order'] = array( + 'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'desc', + 'enum' => array( 'asc', 'desc' ), + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['orderby'] = array( + 'description' => __( 'Sort collection by object attribute.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'date', + 'enum' => array( + 'date', + 'id', + 'include', + 'title', + 'slug', + 'modified', + ), + 'validate_callback' => 'rest_validate_request_arg', + ); + + if ( $this->hierarchical ) { + $params['parent'] = array( + 'description' => __( 'Limit result set to those of particular parent IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'sanitize_callback' => 'wp_parse_id_list', + 'default' => array(), + ); + $params['parent_exclude'] = array( + 'description' => __( 'Limit result set to all items except those of a particular parent ID.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'sanitize_callback' => 'wp_parse_id_list', + 'default' => array(), + ); + } + + /** + * Filter collection parameters for the posts controller. + * + * The dynamic part of the filter `$this->post_type` refers to the post + * type slug for the controller. + * + * This filter registers the collection parameter, but does not map the + * collection parameter to an internal WP_Query parameter. Use the + * `rest_{$this->post_type}_query` filter to set WP_Query parameters. + * + * @param array $query_params JSON Schema-formatted collection parameters. + * @param WP_Post_Type $post_type Post type object. + */ + return apply_filters( "rest_{$this->post_type}_collection_params", $params, $this->post_type ); + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-customer-downloads-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-customer-downloads-controller.php new file mode 100644 index 00000000000..78147fe9301 --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-customer-downloads-controller.php @@ -0,0 +1,27 @@ +/downloads endpoint. + * + * @package Automattic/WooCommerce/RestApi + * @since 2.6.0 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * REST API Customers controller class. + * + * @package Automattic/WooCommerce/RestApi + * @extends WC_REST_Customer_Downloads_V2_Controller + */ +class WC_REST_Customer_Downloads_Controller extends WC_REST_Customer_Downloads_V2_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + protected $namespace = 'wc/v3'; +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-customers-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-customers-controller.php new file mode 100644 index 00000000000..da926c02605 --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-customers-controller.php @@ -0,0 +1,307 @@ +get_data(); + $format_date = array( 'date_created', 'date_modified' ); + + // Format date values. + foreach ( $format_date as $key ) { + // Date created is stored UTC, date modified is stored WP local time. + $datetime = 'date_created' === $key ? get_date_from_gmt( gmdate( 'Y-m-d H:i:s', $data[ $key ]->getTimestamp() ) ) : $data[ $key ]; + $data[ $key ] = wc_rest_prepare_date_response( $datetime, false ); + $data[ $key . '_gmt' ] = wc_rest_prepare_date_response( $datetime ); + } + + return array( + 'id' => $object->get_id(), + 'date_created' => $data['date_created'], + 'date_created_gmt' => $data['date_created_gmt'], + 'date_modified' => $data['date_modified'], + 'date_modified_gmt' => $data['date_modified_gmt'], + 'email' => $data['email'], + 'first_name' => $data['first_name'], + 'last_name' => $data['last_name'], + 'role' => $data['role'], + 'username' => $data['username'], + 'billing' => $data['billing'], + 'shipping' => $data['shipping'], + 'is_paying_customer' => $data['is_paying_customer'], + 'avatar_url' => $object->get_avatar_url(), + 'meta_data' => $data['meta_data'], + ); + } + + /** + * Get the Customer's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'customer', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( "The date the customer was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created_gmt' => array( + 'description' => __( 'The date the customer was created, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the customer was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified_gmt' => array( + 'description' => __( 'The date the customer was last modified, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'email' => array( + 'description' => __( 'The email address for the customer.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'email', + 'context' => array( 'view', 'edit' ), + ), + 'first_name' => array( + 'description' => __( 'Customer first name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'last_name' => array( + 'description' => __( 'Customer last name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'role' => array( + 'description' => __( 'Customer role.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'username' => array( + 'description' => __( 'Customer login name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_user', + ), + ), + 'password' => array( + 'description' => __( 'Customer password.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'edit' ), + ), + 'billing' => array( + 'description' => __( 'List of billing address data.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'first_name' => array( + 'description' => __( 'First name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'last_name' => array( + 'description' => __( 'Last name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'company' => array( + 'description' => __( 'Company name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_1' => array( + 'description' => __( 'Address line 1', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_2' => array( + 'description' => __( 'Address line 2', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'city' => array( + 'description' => __( 'City name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'state' => array( + 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'postcode' => array( + 'description' => __( 'Postal code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'country' => array( + 'description' => __( 'ISO code of the country.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'email' => array( + 'description' => __( 'Email address.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'email', + 'context' => array( 'view', 'edit' ), + ), + 'phone' => array( + 'description' => __( 'Phone number.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'shipping' => array( + 'description' => __( 'List of shipping address data.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'first_name' => array( + 'description' => __( 'First name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'last_name' => array( + 'description' => __( 'Last name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'company' => array( + 'description' => __( 'Company name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_1' => array( + 'description' => __( 'Address line 1', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'address_2' => array( + 'description' => __( 'Address line 2', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'city' => array( + 'description' => __( 'City name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'state' => array( + 'description' => __( 'ISO code or name of the state, province or district.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'postcode' => array( + 'description' => __( 'Postal code.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'country' => array( + 'description' => __( 'ISO code of the country.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'is_paying_customer' => array( + 'description' => __( 'Is the customer a paying customer?', 'woocommerce' ), + 'type' => 'bool', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'avatar_url' => array( + 'description' => __( 'Avatar URL.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'meta_data' => array( + 'description' => __( 'Meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Meta ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => array( 'string', 'null' ), + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-data-continents-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-data-continents-controller.php new file mode 100644 index 00000000000..37d3a55630d --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-data-continents-controller.php @@ -0,0 +1,357 @@ +namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + register_rest_route( + $this->namespace, '/' . $this->rest_base . '/(?P[\w-]+)', array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => array( + 'continent' => array( + 'description' => __( '2 character continent code.', 'woocommerce' ), + 'type' => 'string', + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Return the list of countries and states for a given continent. + * + * @since 3.5.0 + * @param string $continent_code Continent code. + * @param WP_REST_Request $request Request data. + * @return array|mixed Response data, ready for insertion into collection data. + */ + public function get_continent( $continent_code = false, $request ) { + $continents = WC()->countries->get_continents(); + $countries = WC()->countries->get_countries(); + $states = WC()->countries->get_states(); + $locale_info = include WC()->plugin_path() . '/i18n/locale-info.php'; + $data = array(); + + if ( ! array_key_exists( $continent_code, $continents ) ) { + return false; + } + + $continent_list = $continents[ $continent_code ]; + + $continent = array( + 'code' => $continent_code, + 'name' => $continent_list['name'], + ); + + $local_countries = array(); + foreach ( $continent_list['countries'] as $country_code ) { + if ( isset( $countries[ $country_code ] ) ) { + $country = array( + 'code' => $country_code, + 'name' => $countries[ $country_code ], + ); + + // If we have detailed locale information include that in the response. + if ( array_key_exists( $country_code, $locale_info ) ) { + // Defensive programming against unexpected changes in locale-info.php. + $country_data = wp_parse_args( + $locale_info[ $country_code ], array( + 'currency_code' => 'USD', + 'currency_pos' => 'left', + 'decimal_sep' => '.', + 'dimension_unit' => 'in', + 'num_decimals' => 2, + 'thousand_sep' => ',', + 'weight_unit' => 'lbs', + ) + ); + + $country = array_merge( $country, $country_data ); + } + + $local_states = array(); + if ( isset( $states[ $country_code ] ) ) { + foreach ( $states[ $country_code ] as $state_code => $state_name ) { + $local_states[] = array( + 'code' => $state_code, + 'name' => $state_name, + ); + } + } + $country['states'] = $local_states; + + // Allow only desired keys (e.g. filter out tax rates). + $allowed = array( + 'code', + 'currency_code', + 'currency_pos', + 'decimal_sep', + 'dimension_unit', + 'name', + 'num_decimals', + 'states', + 'thousand_sep', + 'weight_unit', + ); + $country = array_intersect_key( $country, array_flip( $allowed ) ); + + $local_countries[] = $country; + } + } + + $continent['countries'] = $local_countries; + return $continent; + } + + /** + * Return the list of states for all continents. + * + * @since 3.5.0 + * @param WP_REST_Request $request Request data. + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + $continents = WC()->countries->get_continents(); + $data = array(); + + foreach ( array_keys( $continents ) as $continent_code ) { + $continent = $this->get_continent( $continent_code, $request ); + $response = $this->prepare_item_for_response( $continent, $request ); + $data[] = $this->prepare_response_for_collection( $response ); + } + + return rest_ensure_response( $data ); + } + + /** + * Return the list of locations for a given continent. + * + * @since 3.5.0 + * @param WP_REST_Request $request Request data. + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + $data = $this->get_continent( strtoupper( $request['location'] ), $request ); + if ( empty( $data ) ) { + return new WP_Error( 'woocommerce_rest_data_invalid_location', __( 'There are no locations matching these parameters.', 'woocommerce' ), array( 'status' => 404 ) ); + } + return $this->prepare_item_for_response( $data, $request ); + } + + /** + * Prepare the data object for response. + * + * @since 3.5.0 + * @param object $item Data object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $item, $request ) { + $data = $this->add_additional_fields_to_object( $item, $request ); + $data = $this->filter_response_by_context( $data, 'view' ); + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $item ) ); + + /** + * Filter the location list returned from the API. + * + * Allows modification of the loction data right before it is returned. + * + * @param WP_REST_Response $response The response object. + * @param array $item The original list of continent(s), countries, and states. + * @param WP_REST_Request $request Request used to generate the response. + */ + return apply_filters( 'woocommerce_rest_prepare_data_continent', $response, $item, $request ); + } + + /** + * Prepare links for the request. + * + * @param object $item Data object. + * @return array Links for the given continent. + */ + protected function prepare_links( $item ) { + $continent_code = strtolower( $item['code'] ); + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $continent_code ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + return $links; + } + + /** + * Get the location schema, conforming to JSON Schema. + * + * @since 3.5.0 + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'data_continents', + 'type' => 'object', + 'properties' => array( + 'code' => array( + 'type' => 'string', + 'description' => __( '2 character continent code.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'name' => array( + 'type' => 'string', + 'description' => __( 'Full name of continent.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'countries' => array( + 'type' => 'array', + 'description' => __( 'List of countries on this continent.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'context' => array( 'view' ), + 'readonly' => true, + 'properties' => array( + 'code' => array( + 'type' => 'string', + 'description' => __( 'ISO3166 alpha-2 country code.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'currency_code' => array( + 'type' => 'string', + 'description' => __( 'Default ISO4127 alpha-3 currency code for the country.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'currency_pos' => array( + 'type' => 'string', + 'description' => __( 'Currency symbol position for this country.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'decimal_sep' => array( + 'type' => 'string', + 'description' => __( 'Decimal separator for displayed prices for this country.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'dimension_unit' => array( + 'type' => 'string', + 'description' => __( 'The unit lengths are defined in for this country.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'name' => array( + 'type' => 'string', + 'description' => __( 'Full name of country.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'num_decimals' => array( + 'type' => 'integer', + 'description' => __( 'Number of decimal points shown in displayed prices for this country.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'states' => array( + 'type' => 'array', + 'description' => __( 'List of states in this country.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'context' => array( 'view' ), + 'readonly' => true, + 'properties' => array( + 'code' => array( + 'type' => 'string', + 'description' => __( 'State code.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'name' => array( + 'type' => 'string', + 'description' => __( 'Full name of state.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + ), + ), + ), + 'thousand_sep' => array( + 'type' => 'string', + 'description' => __( 'Thousands separator for displayed prices in this country.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'weight_unit' => array( + 'type' => 'string', + 'description' => __( 'The unit weights are defined in for this country.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + ), + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-data-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-data-controller.php new file mode 100644 index 00000000000..67a69f025a5 --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-data-controller.php @@ -0,0 +1,184 @@ +namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Check whether a given request has permission to read site data. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check whether a given request has permission to read site settings. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Return the list of data resources. + * + * @since 3.5.0 + * @param WP_REST_Request $request Request data. + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + $data = array(); + $resources = array( + array( + 'slug' => 'continents', + 'description' => __( 'List of supported continents, countries, and states.', 'woocommerce' ), + ), + array( + 'slug' => 'countries', + 'description' => __( 'List of supported states in a given country.', 'woocommerce' ), + ), + array( + 'slug' => 'currencies', + 'description' => __( 'List of supported currencies.', 'woocommerce' ), + ), + ); + + foreach ( $resources as $resource ) { + $item = $this->prepare_item_for_response( (object) $resource, $request ); + $data[] = $this->prepare_response_for_collection( $item ); + } + + return rest_ensure_response( $data ); + } + + /** + * Prepare a data resource object for serialization. + * + * @param stdClass $resource Resource data. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $resource, $request ) { + $data = array( + 'slug' => $resource->slug, + 'description' => $resource->description, + ); + + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, 'view' ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + $response->add_links( $this->prepare_links( $resource ) ); + + return $response; + } + + /** + * Prepare links for the request. + * + * @param object $item Data object. + * @return array Links for the given country. + */ + protected function prepare_links( $item ) { + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $item->slug ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + + return $links; + } + + /** + * Get the data index schema, conforming to JSON Schema. + * + * @since 3.5.0 + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'data_index', + 'type' => 'object', + 'properties' => array( + 'slug' => array( + 'description' => __( 'Data resource ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'description' => array( + 'description' => __( 'Data resource description.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-data-countries-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-data-countries-controller.php new file mode 100644 index 00000000000..d8219a77439 --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-data-countries-controller.php @@ -0,0 +1,240 @@ +namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + register_rest_route( + $this->namespace, '/' . $this->rest_base . '/(?P[\w-]+)', array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => array( + 'location' => array( + 'description' => __( 'ISO3166 alpha-2 country code.', 'woocommerce' ), + 'type' => 'string', + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Get a list of countries and states. + * + * @param string $country_code Country code. + * @param WP_REST_Request $request Request data. + * @return array|mixed Response data, ready for insertion into collection data. + */ + public function get_country( $country_code = false, $request ) { + $countries = WC()->countries->get_countries(); + $states = WC()->countries->get_states(); + $data = array(); + + if ( ! array_key_exists( $country_code, $countries ) ) { + return false; + } + + $country = array( + 'code' => $country_code, + 'name' => $countries[ $country_code ], + ); + + $local_states = array(); + if ( isset( $states[ $country_code ] ) ) { + foreach ( $states[ $country_code ] as $state_code => $state_name ) { + $local_states[] = array( + 'code' => $state_code, + 'name' => $state_name, + ); + } + } + $country['states'] = $local_states; + return $country; + } + + /** + * Return the list of states for all countries. + * + * @since 3.5.0 + * @param WP_REST_Request $request Request data. + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + $countries = WC()->countries->get_countries(); + $data = array(); + + foreach ( array_keys( $countries ) as $country_code ) { + $country = $this->get_country( $country_code, $request ); + $response = $this->prepare_item_for_response( $country, $request ); + $data[] = $this->prepare_response_for_collection( $response ); + } + + return rest_ensure_response( $data ); + } + + /** + * Return the list of states for a given country. + * + * @since 3.5.0 + * @param WP_REST_Request $request Request data. + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + $data = $this->get_country( strtoupper( $request['location'] ), $request ); + if ( empty( $data ) ) { + return new WP_Error( 'woocommerce_rest_data_invalid_location', __( 'There are no locations matching these parameters.', 'woocommerce' ), array( 'status' => 404 ) ); + } + return $this->prepare_item_for_response( $data, $request ); + } + + /** + * Prepare the data object for response. + * + * @since 3.5.0 + * @param object $item Data object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $item, $request ) { + $data = $this->add_additional_fields_to_object( $item, $request ); + $data = $this->filter_response_by_context( $data, 'view' ); + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $item ) ); + + /** + * Filter the states list for a country returned from the API. + * + * Allows modification of the loction data right before it is returned. + * + * @param WP_REST_Response $response The response object. + * @param array $data The original country's states list. + * @param WP_REST_Request $request Request used to generate the response. + */ + return apply_filters( 'woocommerce_rest_prepare_data_country', $response, $item, $request ); + } + + /** + * Prepare links for the request. + * + * @param object $item Data object. + * @return array Links for the given country. + */ + protected function prepare_links( $item ) { + $country_code = strtolower( $item['code'] ); + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $country_code ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + + return $links; + } + + + /** + * Get the location schema, conforming to JSON Schema. + * + * @since 3.5.0 + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'data_countries', + 'type' => 'object', + 'properties' => array( + 'code' => array( + 'type' => 'string', + 'description' => __( 'ISO3166 alpha-2 country code.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'name' => array( + 'type' => 'string', + 'description' => __( 'Full name of country.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'states' => array( + 'type' => 'array', + 'description' => __( 'List of states in this country.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + 'items' => array( + 'type' => 'object', + 'context' => array( 'view' ), + 'readonly' => true, + 'properties' => array( + 'code' => array( + 'type' => 'string', + 'description' => __( 'State code.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'name' => array( + 'type' => 'string', + 'description' => __( 'Full name of state.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + ), + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-data-currencies-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-data-currencies-controller.php new file mode 100644 index 00000000000..574d9d4c571 --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-data-currencies-controller.php @@ -0,0 +1,221 @@ +namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + register_rest_route( + $this->namespace, '/' . $this->rest_base . '/current', array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_current_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + register_rest_route( + $this->namespace, '/' . $this->rest_base . '/(?P[\w-]{3})', array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'location' => array( + 'description' => __( 'ISO4217 currency code.', 'woocommerce' ), + 'type' => 'string', + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Get currency information. + * + * @param string $code Currency code. + * @param WP_REST_Request $request Request data. + * @return array|mixed Response data, ready for insertion into collection data. + */ + public function get_currency( $code = false, $request ) { + $currencies = get_woocommerce_currencies(); + $data = array(); + + if ( ! array_key_exists( $code, $currencies ) ) { + return false; + } + + $currency = array( + 'code' => $code, + 'name' => $currencies[ $code ], + 'symbol' => get_woocommerce_currency_symbol( $code ), + ); + + return $currency; + } + + /** + * Return the list of currencies. + * + * @param WP_REST_Request $request Request data. + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + $currencies = get_woocommerce_currencies(); + foreach ( array_keys( $currencies ) as $code ) { + $currency = $this->get_currency( $code, $request ); + $response = $this->prepare_item_for_response( $currency, $request ); + $data[] = $this->prepare_response_for_collection( $response ); + } + + return rest_ensure_response( $data ); + } + + /** + * Return information for a specific currency. + * + * @param WP_REST_Request $request Request data. + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + $data = $this->get_currency( strtoupper( $request['currency'] ), $request ); + if ( empty( $data ) ) { + return new WP_Error( 'woocommerce_rest_data_invalid_currency', __( 'There are no currencies matching these parameters.', 'woocommerce' ), array( 'status' => 404 ) ); + } + return $this->prepare_item_for_response( $data, $request ); + } + + /** + * Return information for the current site currency. + * + * @param WP_REST_Request $request Request data. + * @return WP_Error|WP_REST_Response + */ + public function get_current_item( $request ) { + $currency = get_option( 'woocommerce_currency' ); + return $this->prepare_item_for_response( $this->get_currency( $currency, $request ), $request ); + } + + /** + * Prepare the data object for response. + * + * @param object $item Data object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $item, $request ) { + $data = $this->add_additional_fields_to_object( $item, $request ); + $data = $this->filter_response_by_context( $data, 'view' ); + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $item ) ); + + /** + * Filter currency returned from the API. + * + * @param WP_REST_Response $response The response object. + * @param array $item Currency data. + * @param WP_REST_Request $request Request used to generate the response. + */ + return apply_filters( 'woocommerce_rest_prepare_data_currency', $response, $item, $request ); + } + + /** + * Prepare links for the request. + * + * @param object $item Data object. + * @return array Links for the given currency. + */ + protected function prepare_links( $item ) { + $code = strtoupper( $item['code'] ); + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%s', $this->namespace, $this->rest_base, $code ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + + return $links; + } + + + /** + * Get the currency schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'data_currencies', + 'type' => 'object', + 'properties' => array( + 'code' => array( + 'type' => 'string', + 'description' => __( 'ISO4217 currency code.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'name' => array( + 'type' => 'string', + 'description' => __( 'Full name of currency.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'symbol' => array( + 'type' => 'string', + 'description' => __( 'Currency symbol.', 'woocommerce' ), + 'context' => array( 'view' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-network-orders-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-network-orders-controller.php new file mode 100644 index 00000000000..5b327a54bce --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-network-orders-controller.php @@ -0,0 +1,27 @@ +/notes endpoint. + * + * @package Automattic/WooCommerce/RestApi + * @since 2.6.0 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * REST API Order Notes controller class. + * + * @package Automattic/WooCommerce/RestApi + * @extends WC_REST_Order_Notes_V2_Controller + */ +class WC_REST_Order_Notes_Controller extends WC_REST_Order_Notes_V2_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + protected $namespace = 'wc/v3'; + + /** + * Prepare a single order note output for response. + * + * @param WP_Comment $note Order note object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $note, $request ) { + $data = array( + 'id' => (int) $note->comment_ID, + 'author' => __( 'woocommerce', 'woocommerce' ) === $note->comment_author ? 'system' : $note->comment_author, + 'date_created' => wc_rest_prepare_date_response( $note->comment_date ), + 'date_created_gmt' => wc_rest_prepare_date_response( $note->comment_date_gmt ), + 'note' => $note->comment_content, + 'customer_note' => (bool) get_comment_meta( $note->comment_ID, 'is_customer_note', true ), + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $note ) ); + + /** + * Filter order note object returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param WP_Comment $note Order note object used to create response. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'woocommerce_rest_prepare_order_note', $response, $note, $request ); + } + + /** + * Create a single order note. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + if ( ! empty( $request['id'] ) ) { + /* translators: %s: post type */ + return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) ); + } + + $order = wc_get_order( (int) $request['order_id'] ); + + if ( ! $order || $this->post_type !== $order->get_type() ) { + return new WP_Error( 'woocommerce_rest_order_invalid_id', __( 'Invalid order ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + // Create the note. + $note_id = $order->add_order_note( $request['note'], $request['customer_note'], $request['added_by_user'] ); + + if ( ! $note_id ) { + return new WP_Error( 'woocommerce_api_cannot_create_order_note', __( 'Cannot create order note, please try again.', 'woocommerce' ), array( 'status' => 500 ) ); + } + + $note = get_comment( $note_id ); + $this->update_additional_fields_for_object( $note, $request ); + + /** + * Fires after a order note is created or updated via the REST API. + * + * @param WP_Comment $note New order note object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating item, false when updating. + */ + do_action( 'woocommerce_rest_insert_order_note', $note, $request, true ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $note, $request ); + $response = rest_ensure_response( $response ); + $response->set_status( 201 ); + $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, str_replace( '(?P[\d]+)', $order->get_id(), $this->rest_base ), $note_id ) ) ); + + return $response; + } + + /** + * Get the Order Notes schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'order_note', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'author' => array( + 'description' => __( 'Order note author.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( "The date the order note was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created_gmt' => array( + 'description' => __( 'The date the order note was created, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'note' => array( + 'description' => __( 'Order note content.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'customer_note' => array( + 'description' => __( 'If true, the note will be shown to customers and they will be notified. If false, the note will be for admin reference only.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'added_by_user' => array( + 'description' => __( 'If true, this note will be attributed to the current user. If false, the note will be attributed to the system.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'edit' ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-order-refunds-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-order-refunds-controller.php new file mode 100644 index 00000000000..98731f5756c --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-order-refunds-controller.php @@ -0,0 +1,86 @@ +/refunds endpoint. + * + * @package Automattic/WooCommerce/RestApi + * @since 2.6.0 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * REST API Order Refunds controller class. + * + * @package Automattic/WooCommerce/RestApi + * @extends WC_REST_Order_Refunds_V2_Controller + */ +class WC_REST_Order_Refunds_Controller extends WC_REST_Order_Refunds_V2_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + protected $namespace = 'wc/v3'; + + /** + * Prepares one object for create or update operation. + * + * @since 3.0.0 + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + * @return WP_Error|WC_Data The prepared item, or WP_Error object on failure. + */ + protected function prepare_object_for_database( $request, $creating = false ) { + $order = wc_get_order( (int) $request['order_id'] ); + + if ( ! $order ) { + return new WP_Error( 'woocommerce_rest_invalid_order_id', __( 'Invalid order ID.', 'woocommerce' ), 404 ); + } + + if ( 0 > $request['amount'] ) { + return new WP_Error( 'woocommerce_rest_invalid_order_refund', __( 'Refund amount must be greater than zero.', 'woocommerce' ), 400 ); + } + + // Create the refund. + $refund = wc_create_refund( + array( + 'order_id' => $order->get_id(), + 'amount' => $request['amount'], + 'reason' => empty( $request['reason'] ) ? null : $request['reason'], + 'line_items' => empty( $request['line_items'] ) ? array() : $request['line_items'], + 'refund_payment' => is_bool( $request['api_refund'] ) ? $request['api_refund'] : true, + 'restock_items' => true, + ) + ); + + if ( is_wp_error( $refund ) ) { + return new WP_Error( 'woocommerce_rest_cannot_create_order_refund', $refund->get_error_message(), 500 ); + } + + if ( ! $refund ) { + return new WP_Error( 'woocommerce_rest_cannot_create_order_refund', __( 'Cannot create order refund, please try again.', 'woocommerce' ), 500 ); + } + + if ( ! empty( $request['meta_data'] ) && is_array( $request['meta_data'] ) ) { + foreach ( $request['meta_data'] as $meta ) { + $refund->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); + } + $refund->save_meta_data(); + } + + /** + * Filters an object before it is inserted via the REST API. + * + * The dynamic portion of the hook name, `$this->post_type`, + * refers to the object type slug. + * + * @param WC_Data $coupon Object object. + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + */ + return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $refund, $request, $creating ); + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-orders-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-orders-controller.php new file mode 100644 index 00000000000..5e4ea622fc8 --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-orders-controller.php @@ -0,0 +1,271 @@ +get_items( 'coupon' ) as $coupon ) { + $order->remove_coupon( $coupon->get_code() ); + } + + foreach ( $request['coupon_lines'] as $item ) { + if ( is_array( $item ) ) { + if ( empty( $item['id'] ) ) { + if ( empty( $item['code'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_coupon', __( 'Coupon code is required.', 'woocommerce' ), 400 ); + } + + $results = $order->apply_coupon( wc_clean( $item['code'] ) ); + + if ( is_wp_error( $results ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_' . $results->get_error_code(), $results->get_error_message(), 400 ); + } + } + } + } + + return true; + } + + /** + * Prepare a single order for create or update. + * + * @throws WC_REST_Exception When fails to set any item. + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + * @return WP_Error|WC_Data + */ + protected function prepare_object_for_database( $request, $creating = false ) { + $id = isset( $request['id'] ) ? absint( $request['id'] ) : 0; + $order = new WC_Order( $id ); + $schema = $this->get_item_schema(); + $data_keys = array_keys( array_filter( $schema['properties'], array( $this, 'filter_writable_props' ) ) ); + + // Handle all writable props. + foreach ( $data_keys as $key ) { + $value = $request[ $key ]; + + if ( ! is_null( $value ) ) { + switch ( $key ) { + case 'coupon_lines': + case 'status': + // Change should be done later so transitions have new data. + break; + case 'billing': + case 'shipping': + $this->update_address( $order, $value, $key ); + break; + case 'line_items': + case 'shipping_lines': + case 'fee_lines': + if ( is_array( $value ) ) { + foreach ( $value as $item ) { + if ( is_array( $item ) ) { + if ( $this->item_is_null( $item ) || ( isset( $item['quantity'] ) && 0 === $item['quantity'] ) ) { + $order->remove_item( $item['id'] ); + } else { + $this->set_item( $order, $key, $item ); + } + } + } + } + break; + case 'meta_data': + if ( is_array( $value ) ) { + foreach ( $value as $meta ) { + $order->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); + } + } + break; + default: + if ( is_callable( array( $order, "set_{$key}" ) ) ) { + $order->{"set_{$key}"}( $value ); + } + break; + } + } + } + + /** + * Filters an object before it is inserted via the REST API. + * + * The dynamic portion of the hook name, `$this->post_type`, + * refers to the object type slug. + * + * @param WC_Data $order Object object. + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + */ + return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $order, $request, $creating ); + } + + /** + * Save an object data. + * + * @since 3.0.0 + * @throws WC_REST_Exception But all errors are validated before returning any data. + * @param WP_REST_Request $request Full details about the request. + * @param bool $creating If is creating a new object. + * @return WC_Data|WP_Error + */ + protected function save_object( $request, $creating = false ) { + try { + $object = $this->prepare_object_for_database( $request, $creating ); + + if ( is_wp_error( $object ) ) { + return $object; + } + + // Make sure gateways are loaded so hooks from gateways fire on save/create. + WC()->payment_gateways(); + + if ( ! is_null( $request['customer_id'] ) && 0 !== $request['customer_id'] ) { + // Make sure customer exists. + if ( false === get_user_by( 'id', $request['customer_id'] ) ) { + throw new WC_REST_Exception( 'woocommerce_rest_invalid_customer_id', __( 'Customer ID is invalid.', 'woocommerce' ), 400 ); + } + + // Make sure customer is part of blog. + if ( is_multisite() && ! is_user_member_of_blog( $request['customer_id'] ) ) { + add_user_to_blog( get_current_blog_id(), $request['customer_id'], 'customer' ); + } + } + + if ( $creating ) { + $object->set_created_via( 'rest-api' ); + $object->set_prices_include_tax( 'yes' === get_option( 'woocommerce_prices_include_tax' ) ); + $object->calculate_totals(); + } else { + // If items have changed, recalculate order totals. + if ( isset( $request['billing'] ) || isset( $request['shipping'] ) || isset( $request['line_items'] ) || isset( $request['shipping_lines'] ) || isset( $request['fee_lines'] ) || isset( $request['coupon_lines'] ) ) { + $object->calculate_totals( true ); + } + } + + // Set coupons. + $this->calculate_coupons( $request, $object ); + + // Set status. + if ( ! empty( $request['status'] ) ) { + $object->set_status( $request['status'] ); + } + + $object->save(); + + // Actions for after the order is saved. + if ( true === $request['set_paid'] ) { + if ( $creating || $object->needs_payment() ) { + $object->payment_complete( $request['transaction_id'] ); + } + } + + return $this->get_object( $object->get_id() ); + } catch ( WC_Data_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), $e->getErrorData() ); + } catch ( WC_REST_Exception $e ) { + return new WP_Error( $e->getErrorCode(), $e->getMessage(), array( 'status' => $e->getCode() ) ); + } + } + + /** + * Prepare objects query. + * + * @since 3.0.0 + * @param WP_REST_Request $request Full details about the request. + * @return array + */ + protected function prepare_objects_query( $request ) { + // This is needed to get around an array to string notice in WC_REST_Orders_V2_Controller::prepare_objects_query. + $statuses = $request['status']; + unset( $request['status'] ); + $args = parent::prepare_objects_query( $request ); + + $args['post_status'] = array(); + foreach ( $statuses as $status ) { + if ( in_array( $status, $this->get_order_statuses(), true ) ) { + $args['post_status'][] = 'wc-' . $status; + } elseif ( 'any' === $status ) { + // Set status to "any" and short-circuit out. + $args['post_status'] = 'any'; + break; + } else { + $args['post_status'][] = $status; + } + } + + // Put the statuses back for further processing (next/prev links, etc). + $request['status'] = $statuses; + + return $args; + } + + /** + * Get the Order's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = parent::get_item_schema(); + + $schema['properties']['coupon_lines']['items']['properties']['discount']['readonly'] = true; + + return $schema; + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + $params['status'] = array( + 'default' => 'any', + 'description' => __( 'Limit result set to orders which have specific statuses.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'string', + 'enum' => array_merge( array( 'any', 'trash' ), $this->get_order_statuses() ), + ), + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-payment-gateways-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-payment-gateways-controller.php new file mode 100644 index 00000000000..5c0eb9c8ac9 --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-payment-gateways-controller.php @@ -0,0 +1,226 @@ + $gateway->id, + 'title' => $gateway->title, + 'description' => $gateway->description, + 'order' => isset( $order[ $gateway->id ] ) ? $order[ $gateway->id ] : '', + 'enabled' => ( 'yes' === $gateway->enabled ), + 'method_title' => $gateway->get_method_title(), + 'method_description' => $gateway->get_method_description(), + 'method_supports' => $gateway->supports, + 'settings' => $this->get_settings( $gateway ), + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $item, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + $response = rest_ensure_response( $data ); + $response->add_links( $this->prepare_links( $gateway, $request ) ); + + /** + * Filter payment gateway objects returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param WC_Payment_Gateway $gateway Payment gateway object. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'woocommerce_rest_prepare_payment_gateway', $response, $gateway, $request ); + } + + /** + * Return settings associated with this payment gateway. + * + * @param WC_Payment_Gateway $gateway Gateway instance. + * + * @return array + */ + public function get_settings( $gateway ) { + $settings = array(); + $gateway->init_form_fields(); + foreach ( $gateway->form_fields as $id => $field ) { + // Make sure we at least have a title and type. + if ( empty( $field['title'] ) || empty( $field['type'] ) ) { + continue; + } + + // Ignore 'enabled' and 'description' which get included elsewhere. + if ( in_array( $id, array( 'enabled', 'description' ), true ) ) { + continue; + } + + $data = array( + 'id' => $id, + 'label' => empty( $field['label'] ) ? $field['title'] : $field['label'], + 'description' => empty( $field['description'] ) ? '' : $field['description'], + 'type' => $field['type'], + 'value' => empty( $gateway->settings[ $id ] ) ? '' : $gateway->settings[ $id ], + 'default' => empty( $field['default'] ) ? '' : $field['default'], + 'tip' => empty( $field['description'] ) ? '' : $field['description'], + 'placeholder' => empty( $field['placeholder'] ) ? '' : $field['placeholder'], + ); + if ( ! empty( $field['options'] ) ) { + $data['options'] = $field['options']; + } + $settings[ $id ] = $data; + } + return $settings; + } + + /** + * Get the payment gateway schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'payment_gateway', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Payment gateway ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'title' => array( + 'description' => __( 'Payment gateway title on checkout.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'description' => array( + 'description' => __( 'Payment gateway description on checkout.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'order' => array( + 'description' => __( 'Payment gateway sort order.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'absint', + ), + ), + 'enabled' => array( + 'description' => __( 'Payment gateway enabled status.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + ), + 'method_title' => array( + 'description' => __( 'Payment gateway method title.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'method_description' => array( + 'description' => __( 'Payment gateway method description.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'method_supports' => array( + 'description' => __( 'Supported features for this payment gateway.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'items' => array( + 'type' => 'string', + ), + ), + 'settings' => array( + 'description' => __( 'Payment gateway settings.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'id' => array( + 'description' => __( 'A unique identifier for the setting.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'label' => array( + 'description' => __( 'A human readable label for the setting used in interfaces.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'description' => array( + 'description' => __( 'A human readable description for the setting used in interfaces.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'type' => array( + 'description' => __( 'Type of setting.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'enum' => array( 'text', 'email', 'number', 'color', 'password', 'textarea', 'select', 'multiselect', 'radio', 'image_width', 'checkbox' ), + 'readonly' => true, + ), + 'value' => array( + 'description' => __( 'Setting value.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'default' => array( + 'description' => __( 'Default value for the setting.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'tip' => array( + 'description' => __( 'Additional help text shown to the user about the setting.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'placeholder' => array( + 'description' => __( 'Placeholder text to be displayed in text inputs.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-posts-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-posts-controller.php new file mode 100644 index 00000000000..979df645d82 --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-posts-controller.php @@ -0,0 +1,724 @@ +post_type, 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to create an item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function create_item_permissions_check( $request ) { + if ( ! wc_rest_check_post_permissions( $this->post_type, 'create' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to read an item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + $post = get_post( (int) $request['id'] ); + + if ( $post && ! wc_rest_check_post_permissions( $this->post_type, 'read', $post->ID ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to update an item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function update_item_permissions_check( $request ) { + $post = get_post( (int) $request['id'] ); + + if ( $post && ! wc_rest_check_post_permissions( $this->post_type, 'edit', $post->ID ) ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to delete an item. + * + * @param WP_REST_Request $request Full details about the request. + * @return bool|WP_Error + */ + public function delete_item_permissions_check( $request ) { + $post = get_post( (int) $request['id'] ); + + if ( $post && ! wc_rest_check_post_permissions( $this->post_type, 'delete', $post->ID ) ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access batch create, update and delete items. + * + * @param WP_REST_Request $request Full details about the request. + * + * @return boolean|WP_Error + */ + public function batch_items_permissions_check( $request ) { + if ( ! wc_rest_check_post_permissions( $this->post_type, 'batch' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_batch', __( 'Sorry, you are not allowed to batch manipulate this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Get a single item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + $id = (int) $request['id']; + $post = get_post( $id ); + + if ( ! empty( $post->post_type ) && 'product_variation' === $post->post_type && 'product' === $this->post_type ) { + return new WP_Error( "woocommerce_rest_invalid_{$this->post_type}_id", __( 'To manipulate product variations you should use the /products/<product_id>/variations/<id> endpoint.', 'woocommerce' ), array( 'status' => 404 ) ); + } elseif ( empty( $id ) || empty( $post->ID ) || $post->post_type !== $this->post_type ) { + return new WP_Error( "woocommerce_rest_invalid_{$this->post_type}_id", __( 'Invalid ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $data = $this->prepare_item_for_response( $post, $request ); + $response = rest_ensure_response( $data ); + + if ( $this->public ) { + $response->link_header( 'alternate', get_permalink( $id ), array( 'type' => 'text/html' ) ); + } + + return $response; + } + + /** + * Create a single item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + if ( ! empty( $request['id'] ) ) { + /* translators: %s: post type */ + return new WP_Error( "woocommerce_rest_{$this->post_type}_exists", sprintf( __( 'Cannot create existing %s.', 'woocommerce' ), $this->post_type ), array( 'status' => 400 ) ); + } + + $post = $this->prepare_item_for_database( $request ); + if ( is_wp_error( $post ) ) { + return $post; + } + + $post->post_type = $this->post_type; + $post_id = wp_insert_post( $post, true ); + + if ( is_wp_error( $post_id ) ) { + + if ( in_array( $post_id->get_error_code(), array( 'db_insert_error' ) ) ) { + $post_id->add_data( array( 'status' => 500 ) ); + } else { + $post_id->add_data( array( 'status' => 400 ) ); + } + return $post_id; + } + $post->ID = $post_id; + $post = get_post( $post_id ); + + $this->update_additional_fields_for_object( $post, $request ); + + // Add meta fields. + $meta_fields = $this->add_post_meta_fields( $post, $request ); + if ( is_wp_error( $meta_fields ) ) { + // Remove post. + $this->delete_post( $post ); + + return $meta_fields; + } + + /** + * Fires after a single item is created or updated via the REST API. + * + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating item, false when updating. + */ + do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, true ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $post, $request ); + $response = rest_ensure_response( $response ); + $response->set_status( 201 ); + $response->header( 'Location', rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $post_id ) ) ); + + return $response; + } + + /** + * Add post meta fields. + * + * @param WP_Post $post Post Object. + * @param WP_REST_Request $request WP_REST_Request Object. + * @return bool|WP_Error + */ + protected function add_post_meta_fields( $post, $request ) { + return true; + } + + /** + * Delete post. + * + * @param WP_Post $post Post object. + */ + protected function delete_post( $post ) { + wp_delete_post( $post->ID, true ); + } + + /** + * Update a single post. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function update_item( $request ) { + $id = (int) $request['id']; + $post = get_post( $id ); + + if ( ! empty( $post->post_type ) && 'product_variation' === $post->post_type && 'product' === $this->post_type ) { + return new WP_Error( "woocommerce_rest_invalid_{$this->post_type}_id", __( 'To manipulate product variations you should use the /products/<product_id>/variations/<id> endpoint.', 'woocommerce' ), array( 'status' => 404 ) ); + } elseif ( empty( $id ) || empty( $post->ID ) || $post->post_type !== $this->post_type ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $post = $this->prepare_item_for_database( $request ); + if ( is_wp_error( $post ) ) { + return $post; + } + // Convert the post object to an array, otherwise wp_update_post will expect non-escaped input. + $post_id = wp_update_post( (array) $post, true ); + if ( is_wp_error( $post_id ) ) { + if ( in_array( $post_id->get_error_code(), array( 'db_update_error' ) ) ) { + $post_id->add_data( array( 'status' => 500 ) ); + } else { + $post_id->add_data( array( 'status' => 400 ) ); + } + return $post_id; + } + + $post = get_post( $post_id ); + $this->update_additional_fields_for_object( $post, $request ); + + // Update meta fields. + $meta_fields = $this->update_post_meta_fields( $post, $request ); + if ( is_wp_error( $meta_fields ) ) { + return $meta_fields; + } + + /** + * Fires after a single item is created or updated via the REST API. + * + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating item, false when updating. + */ + do_action( "woocommerce_rest_insert_{$this->post_type}", $post, $request, false ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $post, $request ); + return rest_ensure_response( $response ); + } + + /** + * Get a collection of posts. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_items( $request ) { + $args = array(); + $args['offset'] = $request['offset']; + $args['order'] = $request['order']; + $args['orderby'] = $request['orderby']; + $args['paged'] = $request['page']; + $args['post__in'] = $request['include']; + $args['post__not_in'] = $request['exclude']; + $args['posts_per_page'] = $request['per_page']; + $args['name'] = $request['slug']; + $args['post_parent__in'] = $request['parent']; + $args['post_parent__not_in'] = $request['parent_exclude']; + $args['s'] = $request['search']; + + $args['date_query'] = array(); + // Set before into date query. Date query must be specified as an array of an array. + if ( isset( $request['before'] ) ) { + $args['date_query'][0]['before'] = $request['before']; + } + + // Set after into date query. Date query must be specified as an array of an array. + if ( isset( $request['after'] ) ) { + $args['date_query'][0]['after'] = $request['after']; + } + + if ( 'wc/v1' === $this->namespace ) { + if ( is_array( $request['filter'] ) ) { + $args = array_merge( $args, $request['filter'] ); + unset( $args['filter'] ); + } + } + + // Force the post_type argument, since it's not a user input variable. + $args['post_type'] = $this->post_type; + + /** + * Filter the query arguments for a request. + * + * Enables adding extra arguments or setting defaults for a post + * collection request. + * + * @param array $args Key value array of query var to query value. + * @param WP_REST_Request $request The request used. + */ + $args = apply_filters( "woocommerce_rest_{$this->post_type}_query", $args, $request ); + $query_args = $this->prepare_items_query( $args, $request ); + + $posts_query = new WP_Query(); + $query_result = $posts_query->query( $query_args ); + + $posts = array(); + foreach ( $query_result as $post ) { + if ( ! wc_rest_check_post_permissions( $this->post_type, 'read', $post->ID ) ) { + continue; + } + + $data = $this->prepare_item_for_response( $post, $request ); + $posts[] = $this->prepare_response_for_collection( $data ); + } + + $page = (int) $query_args['paged']; + $total_posts = $posts_query->found_posts; + + if ( $total_posts < 1 ) { + // Out-of-bounds, run the query again without LIMIT for total count. + unset( $query_args['paged'] ); + $count_query = new WP_Query(); + $count_query->query( $query_args ); + $total_posts = $count_query->found_posts; + } + + $max_pages = ceil( $total_posts / (int) $query_args['posts_per_page'] ); + + $response = rest_ensure_response( $posts ); + $response->header( 'X-WP-Total', (int) $total_posts ); + $response->header( 'X-WP-TotalPages', (int) $max_pages ); + + $request_params = $request->get_query_params(); + if ( ! empty( $request_params['filter'] ) ) { + // Normalize the pagination params. + unset( $request_params['filter']['posts_per_page'] ); + unset( $request_params['filter']['paged'] ); + } + $base = add_query_arg( $request_params, rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ) ); + + if ( $page > 1 ) { + $prev_page = $page - 1; + if ( $prev_page > $max_pages ) { + $prev_page = $max_pages; + } + $prev_link = add_query_arg( 'page', $prev_page, $base ); + $response->link_header( 'prev', $prev_link ); + } + if ( $max_pages > $page ) { + $next_page = $page + 1; + $next_link = add_query_arg( 'page', $next_page, $base ); + $response->link_header( 'next', $next_link ); + } + + return $response; + } + + /** + * Delete a single item. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error + */ + public function delete_item( $request ) { + $id = (int) $request['id']; + $force = (bool) $request['force']; + $post = get_post( $id ); + + if ( empty( $id ) || empty( $post->ID ) || $post->post_type !== $this->post_type ) { + return new WP_Error( "woocommerce_rest_{$this->post_type}_invalid_id", __( 'ID is invalid.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $supports_trash = EMPTY_TRASH_DAYS > 0; + + /** + * Filter whether an item is trashable. + * + * Return false to disable trash support for the item. + * + * @param boolean $supports_trash Whether the item type support trashing. + * @param WP_Post $post The Post object being considered for trashing support. + */ + $supports_trash = apply_filters( "woocommerce_rest_{$this->post_type}_trashable", $supports_trash, $post ); + + if ( ! wc_rest_check_post_permissions( $this->post_type, 'delete', $post->ID ) ) { + /* translators: %s: post type */ + return new WP_Error( "woocommerce_rest_user_cannot_delete_{$this->post_type}", sprintf( __( 'Sorry, you are not allowed to delete %s.', 'woocommerce' ), $this->post_type ), array( 'status' => rest_authorization_required_code() ) ); + } + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $post, $request ); + + // If we're forcing, then delete permanently. + if ( $force ) { + $result = wp_delete_post( $id, true ); + } else { + // If we don't support trashing for this type, error out. + if ( ! $supports_trash ) { + /* translators: %s: post type */ + return new WP_Error( 'woocommerce_rest_trash_not_supported', sprintf( __( 'The %s does not support trashing.', 'woocommerce' ), $this->post_type ), array( 'status' => 501 ) ); + } + + // Otherwise, only trash if we haven't already. + if ( 'trash' === $post->post_status ) { + /* translators: %s: post type */ + return new WP_Error( 'woocommerce_rest_already_trashed', sprintf( __( 'The %s has already been deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 410 ) ); + } + + // (Note that internally this falls through to `wp_delete_post` if + // the trash is disabled.) + $result = wp_trash_post( $id ); + } + + if ( ! $result ) { + /* translators: %s: post type */ + return new WP_Error( 'woocommerce_rest_cannot_delete', sprintf( __( 'The %s cannot be deleted.', 'woocommerce' ), $this->post_type ), array( 'status' => 500 ) ); + } + + /** + * Fires after a single item is deleted or trashed via the REST API. + * + * @param object $post The deleted or trashed item. + * @param WP_REST_Response $response The response data. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( "woocommerce_rest_delete_{$this->post_type}", $post, $response, $request ); + + return $response; + } + + /** + * Prepare links for the request. + * + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + * @return array Links for the given post. + */ + protected function prepare_links( $post, $request ) { + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $post->ID ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + + return $links; + } + + /** + * Determine the allowed query_vars for a get_items() response and + * prepare for WP_Query. + * + * @param array $prepared_args Prepared arguments. + * @param WP_REST_Request $request Request object. + * @return array $query_args + */ + protected function prepare_items_query( $prepared_args = array(), $request = null ) { + + $valid_vars = array_flip( $this->get_allowed_query_vars() ); + $query_args = array(); + foreach ( $valid_vars as $var => $index ) { + if ( isset( $prepared_args[ $var ] ) ) { + /** + * Filter the query_vars used in `get_items` for the constructed query. + * + * The dynamic portion of the hook name, $var, refers to the query_var key. + * + * @param mixed $prepared_args[ $var ] The query_var value. + */ + $query_args[ $var ] = apply_filters( "woocommerce_rest_query_var-{$var}", $prepared_args[ $var ] ); + } + } + + $query_args['ignore_sticky_posts'] = true; + + if ( 'include' === $query_args['orderby'] ) { + $query_args['orderby'] = 'post__in'; + } elseif ( 'id' === $query_args['orderby'] ) { + $query_args['orderby'] = 'ID'; // ID must be capitalized. + } elseif ( 'slug' === $query_args['orderby'] ) { + $query_args['orderby'] = 'name'; + } + + return $query_args; + } + + /** + * Get all the WP Query vars that are allowed for the API request. + * + * @return array + */ + protected function get_allowed_query_vars() { + global $wp; + + /** + * Filter the publicly allowed query vars. + * + * Allows adjusting of the default query vars that are made public. + * + * @param array Array of allowed WP_Query query vars. + */ + $valid_vars = apply_filters( 'query_vars', $wp->public_query_vars ); + + $post_type_obj = get_post_type_object( $this->post_type ); + if ( current_user_can( $post_type_obj->cap->edit_posts ) ) { + /** + * Filter the allowed 'private' query vars for authorized users. + * + * If the user has the `edit_posts` capability, we also allow use of + * private query parameters, which are only undesirable on the + * frontend, but are safe for use in query strings. + * + * To disable anyway, use + * `add_filter( 'woocommerce_rest_private_query_vars', '__return_empty_array' );` + * + * @param array $private_query_vars Array of allowed query vars for authorized users. + * } + */ + $private = apply_filters( 'woocommerce_rest_private_query_vars', $wp->private_query_vars ); + $valid_vars = array_merge( $valid_vars, $private ); + } + // Define our own in addition to WP's normal vars. + $rest_valid = array( + 'date_query', + 'ignore_sticky_posts', + 'offset', + 'post__in', + 'post__not_in', + 'post_parent', + 'post_parent__in', + 'post_parent__not_in', + 'posts_per_page', + 'meta_query', + 'tax_query', + 'meta_key', + 'meta_value', + 'meta_compare', + 'meta_value_num', + ); + $valid_vars = array_merge( $valid_vars, $rest_valid ); + + /** + * Filter allowed query vars for the REST API. + * + * This filter allows you to add or remove query vars from the final allowed + * list for all requests, including unauthenticated ones. To alter the + * vars for editors only. + * + * @param array { + * Array of allowed WP_Query query vars. + * + * @param string $allowed_query_var The query var to allow. + * } + */ + $valid_vars = apply_filters( 'woocommerce_rest_query_vars', $valid_vars ); + + return $valid_vars; + } + + /** + * Get the query params for collections of attachments. + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + $params['context']['default'] = 'view'; + + $params['after'] = array( + 'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'date-time', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['before'] = array( + 'description' => __( 'Limit response to resources published before a given ISO8601 compliant date.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'date-time', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['exclude'] = array( + 'description' => __( 'Ensure result set excludes specific IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'default' => array(), + 'sanitize_callback' => 'wp_parse_id_list', + ); + $params['include'] = array( + 'description' => __( 'Limit result set to specific ids.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'default' => array(), + 'sanitize_callback' => 'wp_parse_id_list', + ); + $params['offset'] = array( + 'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['order'] = array( + 'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'desc', + 'enum' => array( 'asc', 'desc' ), + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['orderby'] = array( + 'description' => __( 'Sort collection by object attribute.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'date', + 'enum' => array( + 'date', + 'id', + 'include', + 'title', + 'slug', + 'modified', + ), + 'validate_callback' => 'rest_validate_request_arg', + ); + + $post_type_obj = get_post_type_object( $this->post_type ); + + if ( isset( $post_type_obj->hierarchical ) && $post_type_obj->hierarchical ) { + $params['parent'] = array( + 'description' => __( 'Limit result set to those of particular parent IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'sanitize_callback' => 'wp_parse_id_list', + 'default' => array(), + ); + $params['parent_exclude'] = array( + 'description' => __( 'Limit result set to all items except those of a particular parent ID.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'sanitize_callback' => 'wp_parse_id_list', + 'default' => array(), + ); + } + + if ( 'wc/v1' === $this->namespace ) { + $params['filter'] = array( + 'type' => 'object', + 'description' => __( 'Use WP Query arguments to modify the response; private query vars require appropriate authorization.', 'woocommerce' ), + ); + } + + return $params; + } + + /** + * Update post meta fields. + * + * @param WP_Post $post Post object. + * @param WP_REST_Request $request Request object. + * @return bool|WP_Error + */ + protected function update_post_meta_fields( $post, $request ) { + return true; + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-product-attribute-terms-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-product-attribute-terms-controller.php new file mode 100644 index 00000000000..bbbdb6eb822 --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-product-attribute-terms-controller.php @@ -0,0 +1,27 @@ +/terms endpoint. + * + * @package Automattic/WooCommerce/RestApi + * @since 2.6.0 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * REST API Product Attribute Terms controller class. + * + * @package Automattic/WooCommerce/RestApi + * @extends WC_REST_Product_Attribute_Terms_V2_Controller + */ +class WC_REST_Product_Attribute_Terms_Controller extends WC_REST_Product_Attribute_Terms_V2_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + protected $namespace = 'wc/v3'; +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-product-attributes-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-product-attributes-controller.php new file mode 100644 index 00000000000..3506306c130 --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-product-attributes-controller.php @@ -0,0 +1,27 @@ +term_id, 'display_type', true ); + + // Get category order. + $menu_order = get_term_meta( $item->term_id, 'order', true ); + + $data = array( + 'id' => (int) $item->term_id, + 'name' => $item->name, + 'slug' => $item->slug, + 'parent' => (int) $item->parent, + 'description' => $item->description, + 'display' => $display_type ? $display_type : 'default', + 'image' => null, + 'menu_order' => (int) $menu_order, + 'count' => (int) $item->count, + ); + + // Get category image. + $image_id = get_term_meta( $item->term_id, 'thumbnail_id', true ); + if ( $image_id ) { + $attachment = get_post( $image_id ); + + $data['image'] = array( + 'id' => (int) $image_id, + 'date_created' => wc_rest_prepare_date_response( $attachment->post_date ), + 'date_created_gmt' => wc_rest_prepare_date_response( $attachment->post_date_gmt ), + 'date_modified' => wc_rest_prepare_date_response( $attachment->post_modified ), + 'date_modified_gmt' => wc_rest_prepare_date_response( $attachment->post_modified_gmt ), + 'src' => wp_get_attachment_url( $image_id ), + 'name' => get_the_title( $attachment ), + 'alt' => get_post_meta( $image_id, '_wp_attachment_image_alt', true ), + ); + } + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $item, $request ) ); + + /** + * Filter a term item returned from the API. + * + * Allows modification of the term data right before it is returned. + * + * @param WP_REST_Response $response The response object. + * @param object $item The original term object. + * @param WP_REST_Request $request Request used to generate the response. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->taxonomy}", $response, $item, $request ); + } + + /** + * Get the Category schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => $this->taxonomy, + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Category name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'slug' => array( + 'description' => __( 'An alphanumeric identifier for the resource unique to its type.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_title', + ), + ), + 'parent' => array( + 'description' => __( 'The ID for the parent of the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'description' => array( + 'description' => __( 'HTML description of the resource.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'wp_filter_post_kses', + ), + ), + 'display' => array( + 'description' => __( 'Category archive display type.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'default', + 'enum' => array( 'default', 'products', 'subcategories', 'both' ), + 'context' => array( 'view', 'edit' ), + ), + 'image' => array( + 'description' => __( 'Image data.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'id' => array( + 'description' => __( 'Image ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'date_created' => array( + 'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created_gmt' => array( + 'description' => __( 'The date the image was created, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified_gmt' => array( + 'description' => __( 'The date the image was last modified, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'src' => array( + 'description' => __( 'Image URL.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Image name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'alt' => array( + 'description' => __( 'Image alternative text.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'menu_order' => array( + 'description' => __( 'Menu order, used to custom sort the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'count' => array( + 'description' => __( 'Number of published products for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Update term meta fields. + * + * @param WP_Term $term Term object. + * @param WP_REST_Request $request Request instance. + * @return bool|WP_Error + * + * @since 3.5.5 + */ + protected function update_term_meta_fields( $term, $request ) { + $id = (int) $term->term_id; + + if ( isset( $request['display'] ) ) { + update_term_meta( $id, 'display_type', 'default' === $request['display'] ? '' : $request['display'] ); + } + + if ( isset( $request['menu_order'] ) ) { + update_term_meta( $id, 'order', $request['menu_order'] ); + } + + if ( isset( $request['image'] ) ) { + if ( empty( $request['image']['id'] ) && ! empty( $request['image']['src'] ) ) { + $upload = wc_rest_upload_image_from_url( esc_url_raw( $request['image']['src'] ) ); + + if ( is_wp_error( $upload ) ) { + return $upload; + } + + $image_id = wc_rest_set_uploaded_image_as_attachment( $upload ); + } else { + $image_id = isset( $request['image']['id'] ) ? absint( $request['image']['id'] ) : 0; + } + + // Check if image_id is a valid image attachment before updating the term meta. + if ( $image_id && wp_attachment_is_image( $image_id ) ) { + update_term_meta( $id, 'thumbnail_id', $image_id ); + + // Set the image alt. + if ( ! empty( $request['image']['alt'] ) ) { + update_post_meta( $image_id, '_wp_attachment_image_alt', wc_clean( $request['image']['alt'] ) ); + } + + // Set the image title. + if ( ! empty( $request['image']['name'] ) ) { + wp_update_post( + array( + 'ID' => $image_id, + 'post_title' => wc_clean( $request['image']['name'] ), + ) + ); + } + } else { + delete_term_meta( $id, 'thumbnail_id' ); + } + } + + return true; + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-product-reviews-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-product-reviews-controller.php new file mode 100644 index 00000000000..7b4a12d2c6e --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-product-reviews-controller.php @@ -0,0 +1,1164 @@ +namespace, '/' . $this->rest_base, array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => array_merge( + $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), array( + 'product_id' => array( + 'required' => true, + 'description' => __( 'Unique identifier for the product.', 'woocommerce' ), + 'type' => 'integer', + ), + 'review' => array( + 'required' => true, + 'type' => 'string', + 'description' => __( 'Review content.', 'woocommerce' ), + ), + 'reviewer' => array( + 'required' => true, + 'type' => 'string', + 'description' => __( 'Name of the reviewer.', 'woocommerce' ), + ), + 'reviewer_email' => array( + 'required' => true, + 'type' => 'string', + 'description' => __( 'Email of the reviewer.', 'woocommerce' ), + ), + ) + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + register_rest_route( + $this->namespace, '/' . $this->rest_base . '/(?P[\d]+)', array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'type' => 'boolean', + 'description' => __( 'Whether to bypass trash and force deletion.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + register_rest_route( + $this->namespace, '/' . $this->rest_base . '/batch', array( + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'batch_items' ), + 'permission_callback' => array( $this, 'batch_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_batch_schema' ), + ) + ); + } + + /** + * Check whether a given request has permission to read webhook deliveries. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_rest_check_product_reviews_permissions( 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to read a product review. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + $id = (int) $request['id']; + $review = get_comment( $id ); + + if ( $review && ! wc_rest_check_product_reviews_permissions( 'read', $review->comment_ID ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to create a new product review. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function create_item_permissions_check( $request ) { + if ( ! wc_rest_check_product_reviews_permissions( 'create' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to update a product review. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function update_item_permissions_check( $request ) { + $id = (int) $request['id']; + $review = get_comment( $id ); + + if ( $review && ! wc_rest_check_product_reviews_permissions( 'edit', $review->comment_ID ) ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you cannot edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to delete a product review. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function delete_item_permissions_check( $request ) { + $id = (int) $request['id']; + $review = get_comment( $id ); + + if ( $review && ! wc_rest_check_product_reviews_permissions( 'delete', $review->comment_ID ) ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you cannot delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access batch create, update and delete items. + * + * @param WP_REST_Request $request Full details about the request. + * @return boolean|WP_Error + */ + public function batch_items_permissions_check( $request ) { + if ( ! wc_rest_check_product_reviews_permissions( 'create' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_batch', __( 'Sorry, you are not allowed to batch manipulate this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Get all reviews. + * + * @param WP_REST_Request $request Full details about the request. + * @return array|WP_Error + */ + public function get_items( $request ) { + // Retrieve the list of registered collection query parameters. + $registered = $this->get_collection_params(); + + /* + * This array defines mappings between public API query parameters whose + * values are accepted as-passed, and their internal WP_Query parameter + * name equivalents (some are the same). Only values which are also + * present in $registered will be set. + */ + $parameter_mappings = array( + 'reviewer' => 'author__in', + 'reviewer_email' => 'author_email', + 'reviewer_exclude' => 'author__not_in', + 'exclude' => 'comment__not_in', + 'include' => 'comment__in', + 'offset' => 'offset', + 'order' => 'order', + 'per_page' => 'number', + 'product' => 'post__in', + 'search' => 'search', + 'status' => 'status', + ); + + $prepared_args = array(); + + /* + * For each known parameter which is both registered and present in the request, + * set the parameter's value on the query $prepared_args. + */ + foreach ( $parameter_mappings as $api_param => $wp_param ) { + if ( isset( $registered[ $api_param ], $request[ $api_param ] ) ) { + $prepared_args[ $wp_param ] = $request[ $api_param ]; + } + } + + // Ensure certain parameter values default to empty strings. + foreach ( array( 'author_email', 'search' ) as $param ) { + if ( ! isset( $prepared_args[ $param ] ) ) { + $prepared_args[ $param ] = ''; + } + } + + if ( isset( $registered['orderby'] ) ) { + $prepared_args['orderby'] = $this->normalize_query_param( $request['orderby'] ); + } + + if ( isset( $prepared_args['status'] ) ) { + $prepared_args['status'] = 'approved' === $prepared_args['status'] ? 'approve' : $prepared_args['status']; + } + + $prepared_args['no_found_rows'] = false; + $prepared_args['date_query'] = array(); + + // Set before into date query. Date query must be specified as an array of an array. + if ( isset( $registered['before'], $request['before'] ) ) { + $prepared_args['date_query'][0]['before'] = $request['before']; + } + + // Set after into date query. Date query must be specified as an array of an array. + if ( isset( $registered['after'], $request['after'] ) ) { + $prepared_args['date_query'][0]['after'] = $request['after']; + } + + if ( isset( $registered['page'] ) && empty( $request['offset'] ) ) { + $prepared_args['offset'] = $prepared_args['number'] * ( absint( $request['page'] ) - 1 ); + } + + /** + * Filters arguments, before passing to WP_Comment_Query, when querying reviews via the REST API. + * + * @since 3.5.0 + * @link https://developer.wordpress.org/reference/classes/wp_comment_query/ + * @param array $prepared_args Array of arguments for WP_Comment_Query. + * @param WP_REST_Request $request The current request. + */ + $prepared_args = apply_filters( 'woocommerce_rest_product_review_query', $prepared_args, $request ); + + // Make sure that returns only reviews. + $prepared_args['type'] = 'review'; + + // Query reviews. + $query = new WP_Comment_Query(); + $query_result = $query->query( $prepared_args ); + $reviews = array(); + + foreach ( $query_result as $review ) { + if ( ! wc_rest_check_product_reviews_permissions( 'read', $review->comment_ID ) ) { + continue; + } + + $data = $this->prepare_item_for_response( $review, $request ); + $reviews[] = $this->prepare_response_for_collection( $data ); + } + + $total_reviews = (int) $query->found_comments; + $max_pages = (int) $query->max_num_pages; + + if ( $total_reviews < 1 ) { + // Out-of-bounds, run the query again without LIMIT for total count. + unset( $prepared_args['number'], $prepared_args['offset'] ); + + $query = new WP_Comment_Query(); + $prepared_args['count'] = true; + + $total_reviews = $query->query( $prepared_args ); + $max_pages = ceil( $total_reviews / $request['per_page'] ); + } + + $response = rest_ensure_response( $reviews ); + $response->header( 'X-WP-Total', $total_reviews ); + $response->header( 'X-WP-TotalPages', $max_pages ); + + $base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ) ); + + if ( $request['page'] > 1 ) { + $prev_page = $request['page'] - 1; + + if ( $prev_page > $max_pages ) { + $prev_page = $max_pages; + } + + $prev_link = add_query_arg( 'page', $prev_page, $base ); + $response->link_header( 'prev', $prev_link ); + } + + if ( $max_pages > $request['page'] ) { + $next_page = $request['page'] + 1; + $next_link = add_query_arg( 'page', $next_page, $base ); + + $response->link_header( 'next', $next_link ); + } + + return $response; + } + + /** + * Create a single review. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function create_item( $request ) { + if ( ! empty( $request['id'] ) ) { + return new WP_Error( 'woocommerce_rest_review_exists', __( 'Cannot create existing product review.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $product_id = (int) $request['product_id']; + + if ( 'product' !== get_post_type( $product_id ) ) { + return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $prepared_review = $this->prepare_item_for_database( $request ); + if ( is_wp_error( $prepared_review ) ) { + return $prepared_review; + } + + $prepared_review['comment_type'] = 'review'; + + /* + * Do not allow a comment to be created with missing or empty comment_content. See wp_handle_comment_submission(). + */ + if ( empty( $prepared_review['comment_content'] ) ) { + return new WP_Error( 'woocommerce_rest_review_content_invalid', __( 'Invalid review content.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + // Setting remaining values before wp_insert_comment so we can use wp_allow_comment(). + if ( ! isset( $prepared_review['comment_date_gmt'] ) ) { + $prepared_review['comment_date_gmt'] = current_time( 'mysql', true ); + } + + if ( ! empty( $_SERVER['REMOTE_ADDR'] ) && rest_is_ip_address( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) ) { // WPCS: input var ok, sanitization ok. + $prepared_review['comment_author_IP'] = wc_clean( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ); // WPCS: input var ok. + } else { + $prepared_review['comment_author_IP'] = '127.0.0.1'; + } + + if ( ! empty( $request['author_user_agent'] ) ) { + $prepared_review['comment_agent'] = $request['author_user_agent']; + } elseif ( $request->get_header( 'user_agent' ) ) { + $prepared_review['comment_agent'] = $request->get_header( 'user_agent' ); + } else { + $prepared_review['comment_agent'] = ''; + } + + $check_comment_lengths = wp_check_comment_data_max_lengths( $prepared_review ); + if ( is_wp_error( $check_comment_lengths ) ) { + $error_code = str_replace( array( 'comment_author', 'comment_content' ), array( 'reviewer', 'review_content' ), $check_comment_lengths->get_error_code() ); + return new WP_Error( 'woocommerce_rest_' . $error_code, __( 'Product review field exceeds maximum length allowed.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $prepared_review['comment_parent'] = 0; + $prepared_review['comment_author_url'] = ''; + $prepared_review['comment_approved'] = wp_allow_comment( $prepared_review, true ); + + if ( is_wp_error( $prepared_review['comment_approved'] ) ) { + $error_code = $prepared_review['comment_approved']->get_error_code(); + $error_message = $prepared_review['comment_approved']->get_error_message(); + + if ( 'comment_duplicate' === $error_code ) { + return new WP_Error( 'woocommerce_rest_' . $error_code, $error_message, array( 'status' => 409 ) ); + } + + if ( 'comment_flood' === $error_code ) { + return new WP_Error( 'woocommerce_rest_' . $error_code, $error_message, array( 'status' => 400 ) ); + } + + return $prepared_review['comment_approved']; + } + + /** + * Filters a review before it is inserted via the REST API. + * + * Allows modification of the review right before it is inserted via wp_insert_comment(). + * Returning a WP_Error value from the filter will shortcircuit insertion and allow + * skipping further processing. + * + * @since 3.5.0 + * @param array|WP_Error $prepared_review The prepared review data for wp_insert_comment(). + * @param WP_REST_Request $request Request used to insert the review. + */ + $prepared_review = apply_filters( 'woocommerce_rest_pre_insert_product_review', $prepared_review, $request ); + if ( is_wp_error( $prepared_review ) ) { + return $prepared_review; + } + + $review_id = wp_insert_comment( wp_filter_comment( wp_slash( (array) $prepared_review ) ) ); + + if ( ! $review_id ) { + return new WP_Error( 'woocommerce_rest_review_failed_create', __( 'Creating product review failed.', 'woocommerce' ), array( 'status' => 500 ) ); + } + + if ( isset( $request['status'] ) ) { + $this->handle_status_param( $request['status'], $review_id ); + } + + update_comment_meta( $review_id, 'rating', ! empty( $request['rating'] ) ? $request['rating'] : '0' ); + + $review = get_comment( $review_id ); + + /** + * Fires after a comment is created or updated via the REST API. + * + * @param WP_Comment $review Inserted or updated comment object. + * @param WP_REST_Request $request Request object. + * @param bool $creating True when creating a comment, false when updating. + */ + do_action( 'woocommerce_rest_insert_product_review', $review, $request, true ); + + $fields_update = $this->update_additional_fields_for_object( $review, $request ); + if ( is_wp_error( $fields_update ) ) { + return $fields_update; + } + + $context = current_user_can( 'moderate_comments' ) ? 'edit' : 'view'; + $request->set_param( 'context', $context ); + + $response = $this->prepare_item_for_response( $review, $request ); + $response = rest_ensure_response( $response ); + + $response->set_status( 201 ); + $response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $review_id ) ) ); + + return $response; + } + + /** + * Get a single product review. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response + */ + public function get_item( $request ) { + $review = $this->get_review( $request['id'] ); + if ( is_wp_error( $review ) ) { + return $review; + } + + $data = $this->prepare_item_for_response( $review, $request ); + $response = rest_ensure_response( $data ); + + return $response; + } + + /** + * Updates a review. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response Response object on success, or error object on failure. + */ + public function update_item( $request ) { + $review = $this->get_review( $request['id'] ); + if ( is_wp_error( $review ) ) { + return $review; + } + + $id = (int) $review->comment_ID; + + if ( isset( $request['type'] ) && 'review' !== get_comment_type( $id ) ) { + return new WP_Error( 'woocommerce_rest_review_invalid_type', __( 'Sorry, you are not allowed to change the comment type.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $prepared_args = $this->prepare_item_for_database( $request ); + if ( is_wp_error( $prepared_args ) ) { + return $prepared_args; + } + + if ( ! empty( $prepared_args['comment_post_ID'] ) ) { + if ( 'product' !== get_post_type( (int) $prepared_args['comment_post_ID'] ) ) { + return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + } + + if ( empty( $prepared_args ) && isset( $request['status'] ) ) { + // Only the comment status is being changed. + $change = $this->handle_status_param( $request['status'], $id ); + + if ( ! $change ) { + return new WP_Error( 'woocommerce_rest_review_failed_edit', __( 'Updating review status failed.', 'woocommerce' ), array( 'status' => 500 ) ); + } + } elseif ( ! empty( $prepared_args ) ) { + if ( is_wp_error( $prepared_args ) ) { + return $prepared_args; + } + + if ( isset( $prepared_args['comment_content'] ) && empty( $prepared_args['comment_content'] ) ) { + return new WP_Error( 'woocommerce_rest_review_content_invalid', __( 'Invalid review content.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $prepared_args['comment_ID'] = $id; + + $check_comment_lengths = wp_check_comment_data_max_lengths( $prepared_args ); + if ( is_wp_error( $check_comment_lengths ) ) { + $error_code = str_replace( array( 'comment_author', 'comment_content' ), array( 'reviewer', 'review_content' ), $check_comment_lengths->get_error_code() ); + return new WP_Error( 'woocommerce_rest_' . $error_code, __( 'Product review field exceeds maximum length allowed.', 'woocommerce' ), array( 'status' => 400 ) ); + } + + $updated = wp_update_comment( wp_slash( (array) $prepared_args ) ); + + if ( false === $updated ) { + return new WP_Error( 'woocommerce_rest_comment_failed_edit', __( 'Updating review failed.', 'woocommerce' ), array( 'status' => 500 ) ); + } + + if ( isset( $request['status'] ) ) { + $this->handle_status_param( $request['status'], $id ); + } + } + + if ( ! empty( $request['rating'] ) ) { + update_comment_meta( $id, 'rating', $request['rating'] ); + } + + $review = get_comment( $id ); + + /** This action is documented in includes/api/class-wc-rest-product-reviews-controller.php */ + do_action( 'woocommerce_rest_insert_product_review', $review, $request, false ); + + $fields_update = $this->update_additional_fields_for_object( $review, $request ); + + if ( is_wp_error( $fields_update ) ) { + return $fields_update; + } + + $request->set_param( 'context', 'edit' ); + + $response = $this->prepare_item_for_response( $review, $request ); + + return rest_ensure_response( $response ); + } + + /** + * Deletes a review. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|WP_REST_Response Response object on success, or error object on failure. + */ + public function delete_item( $request ) { + $review = $this->get_review( $request['id'] ); + if ( is_wp_error( $review ) ) { + return $review; + } + + $force = isset( $request['force'] ) ? (bool) $request['force'] : false; + + /** + * Filters whether a review can be trashed. + * + * Return false to disable trash support for the post. + * + * @since 3.5.0 + * @param bool $supports_trash Whether the post type support trashing. + * @param WP_Comment $review The review object being considered for trashing support. + */ + $supports_trash = apply_filters( 'woocommerce_rest_product_review_trashable', ( EMPTY_TRASH_DAYS > 0 ), $review ); + + $request->set_param( 'context', 'edit' ); + + if ( $force ) { + $previous = $this->prepare_item_for_response( $review, $request ); + $result = wp_delete_comment( $review->comment_ID, true ); + $response = new WP_REST_Response(); + $response->set_data( + array( + 'deleted' => true, + 'previous' => $previous->get_data(), + ) + ); + } else { + // If this type doesn't support trashing, error out. + if ( ! $supports_trash ) { + /* translators: %s: force=true */ + return new WP_Error( 'woocommerce_rest_trash_not_supported', sprintf( __( "The object does not support trashing. Set '%s' to delete.", 'woocommerce' ), 'force=true' ), array( 'status' => 501 ) ); + } + + if ( 'trash' === $review->comment_approved ) { + return new WP_Error( 'woocommerce_rest_already_trashed', __( 'The object has already been trashed.', 'woocommerce' ), array( 'status' => 410 ) ); + } + + $result = wp_trash_comment( $review->comment_ID ); + $review = get_comment( $review->comment_ID ); + $response = $this->prepare_item_for_response( $review, $request ); + } + + if ( ! $result ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'The object cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) ); + } + + /** + * Fires after a review is deleted via the REST API. + * + * @param WP_Comment $review The deleted review data. + * @param WP_REST_Response $response The response returned from the API. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( 'woocommerce_rest_delete_review', $review, $response, $request ); + + return $response; + } + + /** + * Prepare a single product review output for response. + * + * @param WP_Comment $review Product review object. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $review, $request ) { + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $fields = $this->get_fields_for_response( $request ); + $data = array(); + + if ( in_array( 'id', $fields, true ) ) { + $data['id'] = (int) $review->comment_ID; + } + if ( in_array( 'date_created', $fields, true ) ) { + $data['date_created'] = wc_rest_prepare_date_response( $review->comment_date ); + } + if ( in_array( 'date_created_gmt', $fields, true ) ) { + $data['date_created_gmt'] = wc_rest_prepare_date_response( $review->comment_date_gmt ); + } + if ( in_array( 'product_id', $fields, true ) ) { + $data['product_id'] = (int) $review->comment_post_ID; + } + if ( in_array( 'status', $fields, true ) ) { + $data['status'] = $this->prepare_status_response( (string) $review->comment_approved ); + } + if ( in_array( 'reviewer', $fields, true ) ) { + $data['reviewer'] = $review->comment_author; + } + if ( in_array( 'reviewer_email', $fields, true ) ) { + $data['reviewer_email'] = $review->comment_author_email; + } + if ( in_array( 'review', $fields, true ) ) { + $data['review'] = 'view' === $context ? wpautop( $review->comment_content ) : $review->comment_content; + } + if ( in_array( 'rating', $fields, true ) ) { + $data['rating'] = (int) get_comment_meta( $review->comment_ID, 'rating', true ); + } + if ( in_array( 'verified', $fields, true ) ) { + $data['verified'] = wc_review_is_from_verified_owner( $review->comment_ID ); + } + if ( in_array( 'reviewer_avatar_urls', $fields, true ) ) { + $data['reviewer_avatar_urls'] = rest_get_avatar_urls( $review->comment_author_email ); + } + + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + $response->add_links( $this->prepare_links( $review ) ); + + /** + * Filter product reviews object returned from the REST API. + * + * @param WP_REST_Response $response The response object. + * @param WP_Comment $review Product review object used to create response. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( 'woocommerce_rest_prepare_product_review', $response, $review, $request ); + } + + /** + * Prepare a single product review to be inserted into the database. + * + * @param WP_REST_Request $request Request object. + * @return array|WP_Error $prepared_review + */ + protected function prepare_item_for_database( $request ) { + if ( isset( $request['id'] ) ) { + $prepared_review['comment_ID'] = (int) $request['id']; + } + + if ( isset( $request['review'] ) ) { + $prepared_review['comment_content'] = $request['review']; + } + + if ( isset( $request['product_id'] ) ) { + $prepared_review['comment_post_ID'] = (int) $request['product_id']; + } + + if ( isset( $request['reviewer'] ) ) { + $prepared_review['comment_author'] = $request['reviewer']; + } + + if ( isset( $request['reviewer_email'] ) ) { + $prepared_review['comment_author_email'] = $request['reviewer_email']; + } + + if ( ! empty( $request['date_created'] ) ) { + $date_data = rest_get_date_with_gmt( $request['date_created'] ); + + if ( ! empty( $date_data ) ) { + list( $prepared_review['comment_date'], $prepared_review['comment_date_gmt'] ) = $date_data; + } + } elseif ( ! empty( $request['date_created_gmt'] ) ) { + $date_data = rest_get_date_with_gmt( $request['date_created_gmt'], true ); + + if ( ! empty( $date_data ) ) { + list( $prepared_review['comment_date'], $prepared_review['comment_date_gmt'] ) = $date_data; + } + } + + /** + * Filters a review after it is prepared for the database. + * + * Allows modification of the review right after it is prepared for the database. + * + * @since 3.5.0 + * @param array $prepared_review The prepared review data for `wp_insert_comment`. + * @param WP_REST_Request $request The current request. + */ + return apply_filters( 'woocommerce_rest_preprocess_product_review', $prepared_review, $request ); + } + + /** + * Prepare links for the request. + * + * @param WP_Comment $review Product review object. + * @return array Links for the given product review. + */ + protected function prepare_links( $review ) { + $links = array( + 'self' => array( + 'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $review->comment_ID ) ), + ), + 'collection' => array( + 'href' => rest_url( sprintf( '/%s/%s', $this->namespace, $this->rest_base ) ), + ), + ); + + if ( 0 !== (int) $review->comment_post_ID ) { + $links['up'] = array( + 'href' => rest_url( sprintf( '/%s/products/%d', $this->namespace, $review->comment_post_ID ) ), + ); + } + + if ( 0 !== (int) $review->user_id ) { + $links['reviewer'] = array( + 'href' => rest_url( 'wp/v2/users/' . $review->user_id ), + 'embeddable' => true, + ); + } + + return $links; + } + + /** + * Get the Product Review's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'product_review', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( "The date the review was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created_gmt' => array( + 'description' => __( 'The date the review was created, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'product_id' => array( + 'description' => __( 'Unique identifier for the product that the review belongs to.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'status' => array( + 'description' => __( 'Status of the review.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'approved', + 'enum' => array( 'approved', 'hold', 'spam', 'unspam', 'trash', 'untrash' ), + 'context' => array( 'view', 'edit' ), + ), + 'reviewer' => array( + 'description' => __( 'Reviewer name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'reviewer_email' => array( + 'description' => __( 'Reviewer email.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'email', + 'context' => array( 'view', 'edit' ), + ), + 'review' => array( + 'description' => __( 'The content of the review.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'arg_options' => array( + 'sanitize_callback' => 'wp_filter_post_kses', + ), + ), + 'rating' => array( + 'description' => __( 'Review rating (0 to 5).', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'verified' => array( + 'description' => __( 'Shows if the reviewer bought the product or not.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ); + + if ( get_option( 'show_avatars' ) ) { + $avatar_properties = array(); + $avatar_sizes = rest_get_avatar_sizes(); + + foreach ( $avatar_sizes as $size ) { + $avatar_properties[ $size ] = array( + /* translators: %d: avatar image size in pixels */ + 'description' => sprintf( __( 'Avatar URL with image size of %d pixels.', 'woocommerce' ), $size ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'embed', 'view', 'edit' ), + ); + } + $schema['properties']['reviewer_avatar_urls'] = array( + 'description' => __( 'Avatar URLs for the object reviewer.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + 'properties' => $avatar_properties, + ); + } + + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Get the query params for collections. + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + $params['context']['default'] = 'view'; + + $params['after'] = array( + 'description' => __( 'Limit response to resources published after a given ISO8601 compliant date.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'date-time', + ); + $params['before'] = array( + 'description' => __( 'Limit response to reviews published before a given ISO8601 compliant date.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'date-time', + ); + $params['exclude'] = array( + 'description' => __( 'Ensure result set excludes specific IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'default' => array(), + ); + $params['include'] = array( + 'description' => __( 'Limit result set to specific IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'default' => array(), + ); + $params['offset'] = array( + 'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ), + 'type' => 'integer', + ); + $params['order'] = array( + 'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'desc', + 'enum' => array( + 'asc', + 'desc', + ), + ); + $params['orderby'] = array( + 'description' => __( 'Sort collection by object attribute.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'date_gmt', + 'enum' => array( + 'date', + 'date_gmt', + 'id', + 'include', + 'product', + ), + ); + $params['reviewer'] = array( + 'description' => __( 'Limit result set to reviews assigned to specific user IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + ); + $params['reviewer_exclude'] = array( + 'description' => __( 'Ensure result set excludes reviews assigned to specific user IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + ); + $params['reviewer_email'] = array( + 'default' => null, + 'description' => __( 'Limit result set to that from a specific author email.', 'woocommerce' ), + 'format' => 'email', + 'type' => 'string', + ); + $params['product'] = array( + 'default' => array(), + 'description' => __( 'Limit result set to reviews assigned to specific product IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + ); + $params['status'] = array( + 'default' => 'approved', + 'description' => __( 'Limit result set to reviews assigned a specific status.', 'woocommerce' ), + 'sanitize_callback' => 'sanitize_key', + 'type' => 'string', + 'enum' => array( + 'all', + 'hold', + 'approved', + 'spam', + 'trash', + ), + ); + + /** + * Filter collection parameters for the reviews controller. + * + * This filter registers the collection parameter, but does not map the + * collection parameter to an internal WP_Comment_Query parameter. Use the + * `wc_rest_review_query` filter to set WP_Comment_Query parameters. + * + * @since 3.5.0 + * @param array $params JSON Schema-formatted collection parameters. + */ + return apply_filters( 'woocommerce_rest_product_review_collection_params', $params ); + } + + /** + * Get the reivew, if the ID is valid. + * + * @since 3.5.0 + * @param int $id Supplied ID. + * @return WP_Comment|WP_Error Comment object if ID is valid, WP_Error otherwise. + */ + protected function get_review( $id ) { + $id = (int) $id; + $error = new WP_Error( 'woocommerce_rest_review_invalid_id', __( 'Invalid review ID.', 'woocommerce' ), array( 'status' => 404 ) ); + + if ( 0 >= $id ) { + return $error; + } + + $review = get_comment( $id ); + if ( empty( $review ) ) { + return $error; + } + + if ( ! empty( $review->comment_post_ID ) ) { + $post = get_post( (int) $review->comment_post_ID ); + + if ( 'product' !== get_post_type( (int) $review->comment_post_ID ) ) { + return new WP_Error( 'woocommerce_rest_product_invalid_id', __( 'Invalid product ID.', 'woocommerce' ), array( 'status' => 404 ) ); + } + } + + return $review; + } + + /** + * Prepends internal property prefix to query parameters to match our response fields. + * + * @since 3.5.0 + * @param string $query_param Query parameter. + * @return string + */ + protected function normalize_query_param( $query_param ) { + $prefix = 'comment_'; + + switch ( $query_param ) { + case 'id': + $normalized = $prefix . 'ID'; + break; + case 'product': + $normalized = $prefix . 'post_ID'; + break; + case 'include': + $normalized = 'comment__in'; + break; + default: + $normalized = $prefix . $query_param; + break; + } + + return $normalized; + } + + /** + * Checks comment_approved to set comment status for single comment output. + * + * @since 3.5.0 + * @param string|int $comment_approved comment status. + * @return string Comment status. + */ + protected function prepare_status_response( $comment_approved ) { + switch ( $comment_approved ) { + case 'hold': + case '0': + $status = 'hold'; + break; + case 'approve': + case '1': + $status = 'approved'; + break; + case 'spam': + case 'trash': + default: + $status = $comment_approved; + break; + } + + return $status; + } + + /** + * Sets the comment_status of a given review object when creating or updating a review. + * + * @since 3.5.0 + * @param string|int $new_status New review status. + * @param int $id Review ID. + * @return bool Whether the status was changed. + */ + protected function handle_status_param( $new_status, $id ) { + $old_status = wp_get_comment_status( $id ); + + if ( $new_status === $old_status ) { + return false; + } + + switch ( $new_status ) { + case 'approved': + case 'approve': + case '1': + $changed = wp_set_comment_status( $id, 'approve' ); + break; + case 'hold': + case '0': + $changed = wp_set_comment_status( $id, 'hold' ); + break; + case 'spam': + $changed = wp_spam_comment( $id ); + break; + case 'unspam': + $changed = wp_unspam_comment( $id ); + break; + case 'trash': + $changed = wp_trash_comment( $id ); + break; + case 'untrash': + $changed = wp_untrash_comment( $id ); + break; + default: + $changed = false; + break; + } + + return $changed; + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-product-shipping-classes-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-product-shipping-classes-controller.php new file mode 100644 index 00000000000..716e40db7f4 --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-product-shipping-classes-controller.php @@ -0,0 +1,27 @@ +/variations endpoints. + * + * @package Automattic/WooCommerce/RestApi + * @since 3.0.0 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * REST API variations controller class. + * + * @package Automattic/WooCommerce/RestApi + * @extends WC_REST_Product_Variations_V2_Controller + */ +class WC_REST_Product_Variations_Controller extends WC_REST_Product_Variations_V2_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + protected $namespace = 'wc/v3'; + + /** + * Prepare a single variation output for response. + * + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response + */ + public function prepare_object_for_response( $object, $request ) { + $data = array( + 'id' => $object->get_id(), + 'date_created' => wc_rest_prepare_date_response( $object->get_date_created(), false ), + 'date_created_gmt' => wc_rest_prepare_date_response( $object->get_date_created() ), + 'date_modified' => wc_rest_prepare_date_response( $object->get_date_modified(), false ), + 'date_modified_gmt' => wc_rest_prepare_date_response( $object->get_date_modified() ), + 'description' => wc_format_content( $object->get_description() ), + 'permalink' => $object->get_permalink(), + 'sku' => $object->get_sku(), + 'price' => $object->get_price(), + 'regular_price' => $object->get_regular_price(), + 'sale_price' => $object->get_sale_price(), + 'date_on_sale_from' => wc_rest_prepare_date_response( $object->get_date_on_sale_from(), false ), + 'date_on_sale_from_gmt' => wc_rest_prepare_date_response( $object->get_date_on_sale_from() ), + 'date_on_sale_to' => wc_rest_prepare_date_response( $object->get_date_on_sale_to(), false ), + 'date_on_sale_to_gmt' => wc_rest_prepare_date_response( $object->get_date_on_sale_to() ), + 'on_sale' => $object->is_on_sale(), + 'status' => $object->get_status(), + 'purchasable' => $object->is_purchasable(), + 'virtual' => $object->is_virtual(), + 'downloadable' => $object->is_downloadable(), + 'downloads' => $this->get_downloads( $object ), + 'download_limit' => '' !== $object->get_download_limit() ? (int) $object->get_download_limit() : -1, + 'download_expiry' => '' !== $object->get_download_expiry() ? (int) $object->get_download_expiry() : -1, + 'tax_status' => $object->get_tax_status(), + 'tax_class' => $object->get_tax_class(), + 'manage_stock' => $object->managing_stock(), + 'stock_quantity' => $object->get_stock_quantity(), + 'stock_status' => $object->get_stock_status(), + 'backorders' => $object->get_backorders(), + 'backorders_allowed' => $object->backorders_allowed(), + 'backordered' => $object->is_on_backorder(), + 'weight' => $object->get_weight(), + 'dimensions' => array( + 'length' => $object->get_length(), + 'width' => $object->get_width(), + 'height' => $object->get_height(), + ), + 'shipping_class' => $object->get_shipping_class(), + 'shipping_class_id' => $object->get_shipping_class_id(), + 'image' => $this->get_image( $object ), + 'attributes' => $this->get_attributes( $object ), + 'menu_order' => $object->get_menu_order(), + 'meta_data' => $object->get_meta_data(), + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + $response = rest_ensure_response( $data ); + $response->add_links( $this->prepare_links( $object, $request ) ); + + /** + * Filter the data for a response. + * + * The dynamic portion of the hook name, $this->post_type, + * refers to object type being prepared for the response. + * + * @param WP_REST_Response $response The response object. + * @param WC_Data $object Object data. + * @param WP_REST_Request $request Request object. + */ + return apply_filters( "woocommerce_rest_prepare_{$this->post_type}_object", $response, $object, $request ); + } + + /** + * Prepare a single variation for create or update. + * + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + * @return WP_Error|WC_Data + */ + protected function prepare_object_for_database( $request, $creating = false ) { + if ( isset( $request['id'] ) ) { + $variation = wc_get_product( absint( $request['id'] ) ); + } else { + $variation = new WC_Product_Variation(); + } + + $variation->set_parent_id( absint( $request['product_id'] ) ); + + // Status. + if ( isset( $request['status'] ) ) { + $variation->set_status( get_post_status_object( $request['status'] ) ? $request['status'] : 'draft' ); + } + + // SKU. + if ( isset( $request['sku'] ) ) { + $variation->set_sku( wc_clean( $request['sku'] ) ); + } + + // Thumbnail. + if ( isset( $request['image'] ) ) { + if ( is_array( $request['image'] ) ) { + $variation = $this->set_variation_image( $variation, $request['image'] ); + } else { + $variation->set_image_id( '' ); + } + } + + // Virtual variation. + if ( isset( $request['virtual'] ) ) { + $variation->set_virtual( $request['virtual'] ); + } + + // Downloadable variation. + if ( isset( $request['downloadable'] ) ) { + $variation->set_downloadable( $request['downloadable'] ); + } + + // Downloads. + if ( $variation->get_downloadable() ) { + // Downloadable files. + if ( isset( $request['downloads'] ) && is_array( $request['downloads'] ) ) { + $variation = $this->save_downloadable_files( $variation, $request['downloads'] ); + } + + // Download limit. + if ( isset( $request['download_limit'] ) ) { + $variation->set_download_limit( $request['download_limit'] ); + } + + // Download expiry. + if ( isset( $request['download_expiry'] ) ) { + $variation->set_download_expiry( $request['download_expiry'] ); + } + } + + // Shipping data. + $variation = $this->save_product_shipping_data( $variation, $request ); + + // Stock handling. + if ( isset( $request['manage_stock'] ) ) { + $variation->set_manage_stock( $request['manage_stock'] ); + } + + if ( isset( $request['stock_status'] ) ) { + $variation->set_stock_status( $request['stock_status'] ); + } + + if ( isset( $request['backorders'] ) ) { + $variation->set_backorders( $request['backorders'] ); + } + + if ( $variation->get_manage_stock() ) { + if ( isset( $request['stock_quantity'] ) ) { + $variation->set_stock_quantity( $request['stock_quantity'] ); + } elseif ( isset( $request['inventory_delta'] ) ) { + $stock_quantity = wc_stock_amount( $variation->get_stock_quantity() ); + $stock_quantity += wc_stock_amount( $request['inventory_delta'] ); + $variation->set_stock_quantity( $stock_quantity ); + } + } else { + $variation->set_backorders( 'no' ); + $variation->set_stock_quantity( '' ); + } + + // Regular Price. + if ( isset( $request['regular_price'] ) ) { + $variation->set_regular_price( $request['regular_price'] ); + } + + // Sale Price. + if ( isset( $request['sale_price'] ) ) { + $variation->set_sale_price( $request['sale_price'] ); + } + + if ( isset( $request['date_on_sale_from'] ) ) { + $variation->set_date_on_sale_from( $request['date_on_sale_from'] ); + } + + if ( isset( $request['date_on_sale_from_gmt'] ) ) { + $variation->set_date_on_sale_from( $request['date_on_sale_from_gmt'] ? strtotime( $request['date_on_sale_from_gmt'] ) : null ); + } + + if ( isset( $request['date_on_sale_to'] ) ) { + $variation->set_date_on_sale_to( $request['date_on_sale_to'] ); + } + + if ( isset( $request['date_on_sale_to_gmt'] ) ) { + $variation->set_date_on_sale_to( $request['date_on_sale_to_gmt'] ? strtotime( $request['date_on_sale_to_gmt'] ) : null ); + } + + // Tax class. + if ( isset( $request['tax_class'] ) ) { + $variation->set_tax_class( $request['tax_class'] ); + } + + // Description. + if ( isset( $request['description'] ) ) { + $variation->set_description( wp_kses_post( $request['description'] ) ); + } + + // Update taxonomies. + if ( isset( $request['attributes'] ) ) { + $attributes = array(); + $parent = wc_get_product( $variation->get_parent_id() ); + + if ( ! $parent ) { + return new WP_Error( + // Translators: %d parent ID. + "woocommerce_rest_{$this->post_type}_invalid_parent", sprintf( __( 'Cannot set attributes due to invalid parent product.', 'woocommerce' ), $variation->get_parent_id() ), array( + 'status' => 404, + ) + ); + } + + $parent_attributes = $parent->get_attributes(); + + foreach ( $request['attributes'] as $attribute ) { + $attribute_id = 0; + $attribute_name = ''; + + // Check ID for global attributes or name for product attributes. + if ( ! empty( $attribute['id'] ) ) { + $attribute_id = absint( $attribute['id'] ); + $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); + } elseif ( ! empty( $attribute['name'] ) ) { + $attribute_name = sanitize_title( $attribute['name'] ); + } + + if ( ! $attribute_id && ! $attribute_name ) { + continue; + } + + if ( ! isset( $parent_attributes[ $attribute_name ] ) || ! $parent_attributes[ $attribute_name ]->get_variation() ) { + continue; + } + + $attribute_key = sanitize_title( $parent_attributes[ $attribute_name ]->get_name() ); + $attribute_value = isset( $attribute['option'] ) ? wc_clean( stripslashes( $attribute['option'] ) ) : ''; + + if ( $parent_attributes[ $attribute_name ]->is_taxonomy() ) { + // If dealing with a taxonomy, we need to get the slug from the name posted to the API. + $term = get_term_by( 'name', $attribute_value, $attribute_name ); + + if ( $term && ! is_wp_error( $term ) ) { + $attribute_value = $term->slug; + } else { + $attribute_value = sanitize_title( $attribute_value ); + } + } + + $attributes[ $attribute_key ] = $attribute_value; + } + + $variation->set_attributes( $attributes ); + } + + // Menu order. + if ( $request['menu_order'] ) { + $variation->set_menu_order( $request['menu_order'] ); + } + + // Meta data. + if ( is_array( $request['meta_data'] ) ) { + foreach ( $request['meta_data'] as $meta ) { + $variation->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); + } + } + + /** + * Filters an object before it is inserted via the REST API. + * + * The dynamic portion of the hook name, `$this->post_type`, + * refers to the object type slug. + * + * @param WC_Data $variation Object object. + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + */ + return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $variation, $request, $creating ); + } + + /** + * Get the image for a product variation. + * + * @param WC_Product_Variation $variation Variation data. + * @return array + */ + protected function get_image( $variation ) { + if ( ! $variation->get_image_id() ) { + return; + } + + $attachment_id = $variation->get_image_id(); + $attachment_post = get_post( $attachment_id ); + if ( is_null( $attachment_post ) ) { + return; + } + + $attachment = wp_get_attachment_image_src( $attachment_id, 'full' ); + if ( ! is_array( $attachment ) ) { + return; + } + + if ( ! isset( $image ) ) { + return array( + 'id' => (int) $attachment_id, + 'date_created' => wc_rest_prepare_date_response( $attachment_post->post_date, false ), + 'date_created_gmt' => wc_rest_prepare_date_response( strtotime( $attachment_post->post_date_gmt ) ), + 'date_modified' => wc_rest_prepare_date_response( $attachment_post->post_modified, false ), + 'date_modified_gmt' => wc_rest_prepare_date_response( strtotime( $attachment_post->post_modified_gmt ) ), + 'src' => current( $attachment ), + 'name' => get_the_title( $attachment_id ), + 'alt' => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ), + ); + } + } + + /** + * Set variation image. + * + * @throws WC_REST_Exception REST API exceptions. + * @param WC_Product_Variation $variation Variation instance. + * @param array $image Image data. + * @return WC_Product_Variation + */ + protected function set_variation_image( $variation, $image ) { + $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; + + if ( 0 === $attachment_id && isset( $image['src'] ) ) { + $upload = wc_rest_upload_image_from_url( esc_url_raw( $image['src'] ) ); + + if ( is_wp_error( $upload ) ) { + if ( ! apply_filters( 'woocommerce_rest_suppress_image_upload_error', false, $upload, $variation->get_id(), array( $image ) ) ) { + throw new WC_REST_Exception( 'woocommerce_variation_image_upload_error', $upload->get_error_message(), 400 ); + } + } + + $attachment_id = wc_rest_set_uploaded_image_as_attachment( $upload, $variation->get_id() ); + } + + if ( ! wp_attachment_is_image( $attachment_id ) ) { + /* translators: %s: attachment ID */ + throw new WC_REST_Exception( 'woocommerce_variation_invalid_image_id', sprintf( __( '#%s is an invalid image ID.', 'woocommerce' ), $attachment_id ), 400 ); + } + + $variation->set_image_id( $attachment_id ); + + // Set the image alt if present. + if ( ! empty( $image['alt'] ) ) { + update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) ); + } + + // Set the image name if present. + if ( ! empty( $image['name'] ) ) { + wp_update_post( + array( + 'ID' => $attachment_id, + 'post_title' => $image['name'], + ) + ); + } + + return $variation; + } + + /** + * Get the Variation's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $weight_unit = get_option( 'woocommerce_weight_unit' ); + $dimension_unit = get_option( 'woocommerce_dimension_unit' ); + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => $this->post_type, + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( "The date the variation was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the variation was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'description' => array( + 'description' => __( 'Variation description.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'permalink' => array( + 'description' => __( 'Variation URL.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'sku' => array( + 'description' => __( 'Unique identifier.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'price' => array( + 'description' => __( 'Current variation price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'regular_price' => array( + 'description' => __( 'Variation regular price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'sale_price' => array( + 'description' => __( 'Variation sale price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_from' => array( + 'description' => __( "Start date of sale price, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_from_gmt' => array( + 'description' => __( 'Start date of sale price, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_to' => array( + 'description' => __( "End date of sale price, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_to_gmt' => array( + 'description' => __( "End date of sale price, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'on_sale' => array( + 'description' => __( 'Shows if the variation is on sale.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'status' => array( + 'description' => __( 'Variation status.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'publish', + 'enum' => array_keys( get_post_statuses() ), + 'context' => array( 'view', 'edit' ), + ), + 'purchasable' => array( + 'description' => __( 'Shows if the variation can be bought.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'virtual' => array( + 'description' => __( 'If the variation is virtual.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'downloadable' => array( + 'description' => __( 'If the variation is downloadable.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'downloads' => array( + 'description' => __( 'List of downloadable files.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'File ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'File name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'file' => array( + 'description' => __( 'File URL.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'download_limit' => array( + 'description' => __( 'Number of times downloadable files can be downloaded after purchase.', 'woocommerce' ), + 'type' => 'integer', + 'default' => -1, + 'context' => array( 'view', 'edit' ), + ), + 'download_expiry' => array( + 'description' => __( 'Number of days until access to downloadable files expires.', 'woocommerce' ), + 'type' => 'integer', + 'default' => -1, + 'context' => array( 'view', 'edit' ), + ), + 'tax_status' => array( + 'description' => __( 'Tax status.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'taxable', + 'enum' => array( 'taxable', 'shipping', 'none' ), + 'context' => array( 'view', 'edit' ), + ), + 'tax_class' => array( + 'description' => __( 'Tax class.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'manage_stock' => array( + 'description' => __( 'Stock management at variation level.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'stock_quantity' => array( + 'description' => __( 'Stock quantity.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'stock_status' => array( + 'description' => __( 'Controls the stock status of the product.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'instock', + 'enum' => array_keys( wc_get_product_stock_status_options() ), + 'context' => array( 'view', 'edit' ), + ), + 'backorders' => array( + 'description' => __( 'If managing stock, this controls if backorders are allowed.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'no', + 'enum' => array( 'no', 'notify', 'yes' ), + 'context' => array( 'view', 'edit' ), + ), + 'backorders_allowed' => array( + 'description' => __( 'Shows if backorders are allowed.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'backordered' => array( + 'description' => __( 'Shows if the variation is on backordered.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'weight' => array( + /* translators: %s: weight unit */ + 'description' => sprintf( __( 'Variation weight (%s).', 'woocommerce' ), $weight_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'dimensions' => array( + 'description' => __( 'Variation dimensions.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'length' => array( + /* translators: %s: dimension unit */ + 'description' => sprintf( __( 'Variation length (%s).', 'woocommerce' ), $dimension_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'width' => array( + /* translators: %s: dimension unit */ + 'description' => sprintf( __( 'Variation width (%s).', 'woocommerce' ), $dimension_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'height' => array( + /* translators: %s: dimension unit */ + 'description' => sprintf( __( 'Variation height (%s).', 'woocommerce' ), $dimension_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'shipping_class' => array( + 'description' => __( 'Shipping class slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'shipping_class_id' => array( + 'description' => __( 'Shipping class ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'image' => array( + 'description' => __( 'Variation image data.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'id' => array( + 'description' => __( 'Image ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'date_created' => array( + 'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created_gmt' => array( + 'description' => __( 'The date the image was created, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified_gmt' => array( + 'description' => __( 'The date the image was last modified, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'src' => array( + 'description' => __( 'Image URL.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Image name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'alt' => array( + 'description' => __( 'Image alternative text.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'attributes' => array( + 'description' => __( 'List of attributes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Attribute ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Attribute name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'option' => array( + 'description' => __( 'Selected attribute term name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'menu_order' => array( + 'description' => __( 'Menu order, used to custom sort products.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'meta_data' => array( + 'description' => __( 'Meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Meta ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => array( 'string', 'null' ), + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + ), + ); + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Prepare objects query. + * + * @since 3.0.0 + * @param WP_REST_Request $request Full details about the request. + * @return array + */ + protected function prepare_objects_query( $request ) { + $args = WC_REST_CRUD_Controller::prepare_objects_query( $request ); + + // Set post_status. + $args['post_status'] = $request['status']; + + // Filter by sku. + if ( ! empty( $request['sku'] ) ) { + $skus = explode( ',', $request['sku'] ); + // Include the current string as a SKU too. + if ( 1 < count( $skus ) ) { + $skus[] = $request['sku']; + } + + $args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok. + $args, + array( + 'key' => '_sku', + 'value' => $skus, + 'compare' => 'IN', + ) + ); + } + + // Filter by tax class. + if ( ! empty( $request['tax_class'] ) ) { + $args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok. + $args, + array( + 'key' => '_tax_class', + 'value' => 'standard' !== $request['tax_class'] ? $request['tax_class'] : '', + ) + ); + } + + // Price filter. + if ( ! empty( $request['min_price'] ) || ! empty( $request['max_price'] ) ) { + $args['meta_query'] = $this->add_meta_query( $args, wc_get_min_max_price_meta_query( $request ) ); // WPCS: slow query ok. + } + + // Filter product based on stock_status. + if ( ! empty( $request['stock_status'] ) ) { + $args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok. + $args, + array( + 'key' => '_stock_status', + 'value' => $request['stock_status'], + ) + ); + } + + // Filter by on sale products. + if ( is_bool( $request['on_sale'] ) ) { + $on_sale_key = $request['on_sale'] ? 'post__in' : 'post__not_in'; + $on_sale_ids = wc_get_product_ids_on_sale(); + + // Use 0 when there's no on sale products to avoid return all products. + $on_sale_ids = empty( $on_sale_ids ) ? array( 0 ) : $on_sale_ids; + + $args[ $on_sale_key ] += $on_sale_ids; + } + + // Force the post_type argument, since it's not a user input variable. + if ( ! empty( $request['sku'] ) ) { + $args['post_type'] = array( 'product', 'product_variation' ); + } else { + $args['post_type'] = $this->post_type; + } + + $args['post_parent'] = $request['product_id']; + + return $args; + } + + /** + * Get the query params for collections of attachments. + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + unset( + $params['in_stock'], + $params['type'], + $params['featured'], + $params['category'], + $params['tag'], + $params['shipping_class'], + $params['attribute'], + $params['attribute_term'] + ); + + $params['stock_status'] = array( + 'description' => __( 'Limit result set to products with specified stock status.', 'woocommerce' ), + 'type' => 'string', + 'enum' => array_keys( wc_get_product_stock_status_options() ), + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php new file mode 100644 index 00000000000..5af8575a2bb --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-products-controller.php @@ -0,0 +1,1341 @@ +get_image_id() ) { + $attachment_ids[] = $product->get_image_id(); + } + + // Add gallery images. + $attachment_ids = array_merge( $attachment_ids, $product->get_gallery_image_ids() ); + + // Build image data. + foreach ( $attachment_ids as $attachment_id ) { + $attachment_post = get_post( $attachment_id ); + if ( is_null( $attachment_post ) ) { + continue; + } + + $attachment = wp_get_attachment_image_src( $attachment_id, 'full' ); + if ( ! is_array( $attachment ) ) { + continue; + } + + $images[] = array( + 'id' => (int) $attachment_id, + 'date_created' => wc_rest_prepare_date_response( $attachment_post->post_date, false ), + 'date_created_gmt' => wc_rest_prepare_date_response( strtotime( $attachment_post->post_date_gmt ) ), + 'date_modified' => wc_rest_prepare_date_response( $attachment_post->post_modified, false ), + 'date_modified_gmt' => wc_rest_prepare_date_response( strtotime( $attachment_post->post_modified_gmt ) ), + 'src' => current( $attachment ), + 'name' => get_the_title( $attachment_id ), + 'alt' => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ), + ); + } + + return $images; + } + + /** + * Make extra product orderby features supported by WooCommerce available to the WC API. + * This includes 'price', 'popularity', and 'rating'. + * + * @param WP_REST_Request $request Request data. + * @return array + */ + protected function prepare_objects_query( $request ) { + $args = WC_REST_CRUD_Controller::prepare_objects_query( $request ); + + // Set post_status. + $args['post_status'] = $request['status']; + + // Taxonomy query to filter products by type, category, + // tag, shipping class, and attribute. + $tax_query = array(); + + // Map between taxonomy name and arg's key. + $taxonomies = array( + 'product_cat' => 'category', + 'product_tag' => 'tag', + 'product_shipping_class' => 'shipping_class', + ); + + // Set tax_query for each passed arg. + foreach ( $taxonomies as $taxonomy => $key ) { + if ( ! empty( $request[ $key ] ) ) { + $tax_query[] = array( + 'taxonomy' => $taxonomy, + 'field' => 'term_id', + 'terms' => $request[ $key ], + ); + } + } + + // Filter product type by slug. + if ( ! empty( $request['type'] ) ) { + $tax_query[] = array( + 'taxonomy' => 'product_type', + 'field' => 'slug', + 'terms' => $request['type'], + ); + } + + // Filter by attribute and term. + if ( ! empty( $request['attribute'] ) && ! empty( $request['attribute_term'] ) ) { + if ( in_array( $request['attribute'], wc_get_attribute_taxonomy_names(), true ) ) { + $tax_query[] = array( + 'taxonomy' => $request['attribute'], + 'field' => 'term_id', + 'terms' => $request['attribute_term'], + ); + } + } + + // Build tax_query if taxonomies are set. + if ( ! empty( $tax_query ) ) { + if ( ! empty( $args['tax_query'] ) ) { + $args['tax_query'] = array_merge( $tax_query, $args['tax_query'] ); // WPCS: slow query ok. + } else { + $args['tax_query'] = $tax_query; // WPCS: slow query ok. + } + } + + // Filter featured. + if ( is_bool( $request['featured'] ) ) { + $args['tax_query'][] = array( + 'taxonomy' => 'product_visibility', + 'field' => 'name', + 'terms' => 'featured', + 'operator' => true === $request['featured'] ? 'IN' : 'NOT IN', + ); + } + + // Filter by sku. + if ( ! empty( $request['sku'] ) ) { + $skus = explode( ',', $request['sku'] ); + // Include the current string as a SKU too. + if ( 1 < count( $skus ) ) { + $skus[] = $request['sku']; + } + + $args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok. + $args, array( + 'key' => '_sku', + 'value' => $skus, + 'compare' => 'IN', + ) + ); + } + + // Filter by tax class. + if ( ! empty( $request['tax_class'] ) ) { + $args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok. + $args, array( + 'key' => '_tax_class', + 'value' => 'standard' !== $request['tax_class'] ? $request['tax_class'] : '', + ) + ); + } + + // Price filter. + if ( ! empty( $request['min_price'] ) || ! empty( $request['max_price'] ) ) { + $args['meta_query'] = $this->add_meta_query( $args, wc_get_min_max_price_meta_query( $request ) ); // WPCS: slow query ok. + } + + // Filter product by stock_status. + if ( ! empty( $request['stock_status'] ) ) { + $args['meta_query'] = $this->add_meta_query( // WPCS: slow query ok. + $args, array( + 'key' => '_stock_status', + 'value' => $request['stock_status'], + ) + ); + } + + // Filter by on sale products. + if ( is_bool( $request['on_sale'] ) ) { + $on_sale_key = $request['on_sale'] ? 'post__in' : 'post__not_in'; + $on_sale_ids = wc_get_product_ids_on_sale(); + + // Use 0 when there's no on sale products to avoid return all products. + $on_sale_ids = empty( $on_sale_ids ) ? array( 0 ) : $on_sale_ids; + + $args[ $on_sale_key ] += $on_sale_ids; + } + + // Force the post_type argument, since it's not a user input variable. + if ( ! empty( $request['sku'] ) ) { + $args['post_type'] = array( 'product', 'product_variation' ); + } else { + $args['post_type'] = $this->post_type; + } + + $orderby = $request->get_param( 'orderby' ); + $order = $request->get_param( 'order' ); + + $ordering_args = WC()->query->get_catalog_ordering_args( $orderby, $order ); + $args['orderby'] = $ordering_args['orderby']; + $args['order'] = $ordering_args['order']; + if ( $ordering_args['meta_key'] ) { + $args['meta_key'] = $ordering_args['meta_key']; // WPCS: slow query ok. + } + + return $args; + } + + /** + * Set product images. + * + * @throws WC_REST_Exception REST API exceptions. + * @param WC_Product $product Product instance. + * @param array $images Images data. + * @return WC_Product + */ + protected function set_product_images( $product, $images ) { + $images = is_array( $images ) ? array_filter( $images ) : array(); + + if ( ! empty( $images ) ) { + $gallery = array(); + + foreach ( $images as $index => $image ) { + $attachment_id = isset( $image['id'] ) ? absint( $image['id'] ) : 0; + + if ( 0 === $attachment_id && isset( $image['src'] ) ) { + $upload = wc_rest_upload_image_from_url( esc_url_raw( $image['src'] ) ); + + if ( is_wp_error( $upload ) ) { + if ( ! apply_filters( 'woocommerce_rest_suppress_image_upload_error', false, $upload, $product->get_id(), $images ) ) { + throw new WC_REST_Exception( 'woocommerce_product_image_upload_error', $upload->get_error_message(), 400 ); + } else { + continue; + } + } + + $attachment_id = wc_rest_set_uploaded_image_as_attachment( $upload, $product->get_id() ); + } + + if ( ! wp_attachment_is_image( $attachment_id ) ) { + /* translators: %s: image ID */ + throw new WC_REST_Exception( 'woocommerce_product_invalid_image_id', sprintf( __( '#%s is an invalid image ID.', 'woocommerce' ), $attachment_id ), 400 ); + } + + $featured_image = $product->get_image_id(); + + if ( 0 === $index ) { + $product->set_image_id( $attachment_id ); + } else { + $gallery[] = $attachment_id; + } + + // Set the image alt if present. + if ( ! empty( $image['alt'] ) ) { + update_post_meta( $attachment_id, '_wp_attachment_image_alt', wc_clean( $image['alt'] ) ); + } + + // Set the image name if present. + if ( ! empty( $image['name'] ) ) { + wp_update_post( + array( + 'ID' => $attachment_id, + 'post_title' => $image['name'], + ) + ); + } + } + + $product->set_gallery_image_ids( $gallery ); + } else { + $product->set_image_id( '' ); + $product->set_gallery_image_ids( array() ); + } + + return $product; + } + + /** + * Prepare a single product for create or update. + * + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + * @return WP_Error|WC_Data + */ + protected function prepare_object_for_database( $request, $creating = false ) { + $id = isset( $request['id'] ) ? absint( $request['id'] ) : 0; + + // Type is the most important part here because we need to be using the correct class and methods. + if ( isset( $request['type'] ) ) { + $classname = WC_Product_Factory::get_classname_from_product_type( $request['type'] ); + + if ( ! class_exists( $classname ) ) { + $classname = 'WC_Product_Simple'; + } + + $product = new $classname( $id ); + } elseif ( isset( $request['id'] ) ) { + $product = wc_get_product( $id ); + } else { + $product = new WC_Product_Simple(); + } + + if ( 'variation' === $product->get_type() ) { + return new WP_Error( + "woocommerce_rest_invalid_{$this->post_type}_id", __( 'To manipulate product variations you should use the /products/<product_id>/variations/<id> endpoint.', 'woocommerce' ), array( + 'status' => 404, + ) + ); + } + + // Post title. + if ( isset( $request['name'] ) ) { + $product->set_name( wp_filter_post_kses( $request['name'] ) ); + } + + // Post content. + if ( isset( $request['description'] ) ) { + $product->set_description( wp_filter_post_kses( $request['description'] ) ); + } + + // Post excerpt. + if ( isset( $request['short_description'] ) ) { + $product->set_short_description( wp_filter_post_kses( $request['short_description'] ) ); + } + + // Post status. + if ( isset( $request['status'] ) ) { + $product->set_status( get_post_status_object( $request['status'] ) ? $request['status'] : 'draft' ); + } + + // Post slug. + if ( isset( $request['slug'] ) ) { + $product->set_slug( $request['slug'] ); + } + + // Menu order. + if ( isset( $request['menu_order'] ) ) { + $product->set_menu_order( $request['menu_order'] ); + } + + // Comment status. + if ( isset( $request['reviews_allowed'] ) ) { + $product->set_reviews_allowed( $request['reviews_allowed'] ); + } + + // Virtual. + if ( isset( $request['virtual'] ) ) { + $product->set_virtual( $request['virtual'] ); + } + + // Tax status. + if ( isset( $request['tax_status'] ) ) { + $product->set_tax_status( $request['tax_status'] ); + } + + // Tax Class. + if ( isset( $request['tax_class'] ) ) { + $product->set_tax_class( $request['tax_class'] ); + } + + // Catalog Visibility. + if ( isset( $request['catalog_visibility'] ) ) { + $product->set_catalog_visibility( $request['catalog_visibility'] ); + } + + // Purchase Note. + if ( isset( $request['purchase_note'] ) ) { + $product->set_purchase_note( wp_kses_post( wp_unslash( $request['purchase_note'] ) ) ); + } + + // Featured Product. + if ( isset( $request['featured'] ) ) { + $product->set_featured( $request['featured'] ); + } + + // Shipping data. + $product = $this->save_product_shipping_data( $product, $request ); + + // SKU. + if ( isset( $request['sku'] ) ) { + $product->set_sku( wc_clean( $request['sku'] ) ); + } + + // Attributes. + if ( isset( $request['attributes'] ) ) { + $attributes = array(); + + foreach ( $request['attributes'] as $attribute ) { + $attribute_id = 0; + $attribute_name = ''; + + // Check ID for global attributes or name for product attributes. + if ( ! empty( $attribute['id'] ) ) { + $attribute_id = absint( $attribute['id'] ); + $attribute_name = wc_attribute_taxonomy_name_by_id( $attribute_id ); + } elseif ( ! empty( $attribute['name'] ) ) { + $attribute_name = wc_clean( $attribute['name'] ); + } + + if ( ! $attribute_id && ! $attribute_name ) { + continue; + } + + if ( $attribute_id ) { + + if ( isset( $attribute['options'] ) ) { + $options = $attribute['options']; + + if ( ! is_array( $attribute['options'] ) ) { + // Text based attributes - Posted values are term names. + $options = explode( WC_DELIMITER, $options ); + } + + $values = array_map( 'wc_sanitize_term_text_based', $options ); + $values = array_filter( $values, 'strlen' ); + } else { + $values = array(); + } + + if ( ! empty( $values ) ) { + // Add attribute to array, but don't set values. + $attribute_object = new WC_Product_Attribute(); + $attribute_object->set_id( $attribute_id ); + $attribute_object->set_name( $attribute_name ); + $attribute_object->set_options( $values ); + $attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' ); + $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); + $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); + $attributes[] = $attribute_object; + } + } elseif ( isset( $attribute['options'] ) ) { + // Custom attribute - Add attribute to array and set the values. + if ( is_array( $attribute['options'] ) ) { + $values = $attribute['options']; + } else { + $values = explode( WC_DELIMITER, $attribute['options'] ); + } + $attribute_object = new WC_Product_Attribute(); + $attribute_object->set_name( $attribute_name ); + $attribute_object->set_options( $values ); + $attribute_object->set_position( isset( $attribute['position'] ) ? (string) absint( $attribute['position'] ) : '0' ); + $attribute_object->set_visible( ( isset( $attribute['visible'] ) && $attribute['visible'] ) ? 1 : 0 ); + $attribute_object->set_variation( ( isset( $attribute['variation'] ) && $attribute['variation'] ) ? 1 : 0 ); + $attributes[] = $attribute_object; + } + } + $product->set_attributes( $attributes ); + } + + // Sales and prices. + if ( in_array( $product->get_type(), array( 'variable', 'grouped' ), true ) ) { + $product->set_regular_price( '' ); + $product->set_sale_price( '' ); + $product->set_date_on_sale_to( '' ); + $product->set_date_on_sale_from( '' ); + $product->set_price( '' ); + } else { + // Regular Price. + if ( isset( $request['regular_price'] ) ) { + $product->set_regular_price( $request['regular_price'] ); + } + + // Sale Price. + if ( isset( $request['sale_price'] ) ) { + $product->set_sale_price( $request['sale_price'] ); + } + + if ( isset( $request['date_on_sale_from'] ) ) { + $product->set_date_on_sale_from( $request['date_on_sale_from'] ); + } + + if ( isset( $request['date_on_sale_from_gmt'] ) ) { + $product->set_date_on_sale_from( $request['date_on_sale_from_gmt'] ? strtotime( $request['date_on_sale_from_gmt'] ) : null ); + } + + if ( isset( $request['date_on_sale_to'] ) ) { + $product->set_date_on_sale_to( $request['date_on_sale_to'] ); + } + + if ( isset( $request['date_on_sale_to_gmt'] ) ) { + $product->set_date_on_sale_to( $request['date_on_sale_to_gmt'] ? strtotime( $request['date_on_sale_to_gmt'] ) : null ); + } + } + + // Product parent ID. + if ( isset( $request['parent_id'] ) ) { + $product->set_parent_id( $request['parent_id'] ); + } + + // Sold individually. + if ( isset( $request['sold_individually'] ) ) { + $product->set_sold_individually( $request['sold_individually'] ); + } + + // Stock status; stock_status has priority over in_stock. + if ( isset( $request['stock_status'] ) ) { + $stock_status = $request['stock_status']; + } else { + $stock_status = $product->get_stock_status(); + } + + // Stock data. + if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) { + // Manage stock. + if ( isset( $request['manage_stock'] ) ) { + $product->set_manage_stock( $request['manage_stock'] ); + } + + // Backorders. + if ( isset( $request['backorders'] ) ) { + $product->set_backorders( $request['backorders'] ); + } + + if ( $product->is_type( 'grouped' ) ) { + $product->set_manage_stock( 'no' ); + $product->set_backorders( 'no' ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( $stock_status ); + } elseif ( $product->is_type( 'external' ) ) { + $product->set_manage_stock( 'no' ); + $product->set_backorders( 'no' ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( 'instock' ); + } elseif ( $product->get_manage_stock() ) { + // Stock status is always determined by children so sync later. + if ( ! $product->is_type( 'variable' ) ) { + $product->set_stock_status( $stock_status ); + } + + // Stock quantity. + if ( isset( $request['stock_quantity'] ) ) { + $product->set_stock_quantity( wc_stock_amount( $request['stock_quantity'] ) ); + } elseif ( isset( $request['inventory_delta'] ) ) { + $stock_quantity = wc_stock_amount( $product->get_stock_quantity() ); + $stock_quantity += wc_stock_amount( $request['inventory_delta'] ); + $product->set_stock_quantity( wc_stock_amount( $stock_quantity ) ); + } + } else { + // Don't manage stock. + $product->set_manage_stock( 'no' ); + $product->set_stock_quantity( '' ); + $product->set_stock_status( $stock_status ); + } + } elseif ( ! $product->is_type( 'variable' ) ) { + $product->set_stock_status( $stock_status ); + } + + // Upsells. + if ( isset( $request['upsell_ids'] ) ) { + $upsells = array(); + $ids = $request['upsell_ids']; + + if ( ! empty( $ids ) ) { + foreach ( $ids as $id ) { + if ( $id && $id > 0 ) { + $upsells[] = $id; + } + } + } + + $product->set_upsell_ids( $upsells ); + } + + // Cross sells. + if ( isset( $request['cross_sell_ids'] ) ) { + $crosssells = array(); + $ids = $request['cross_sell_ids']; + + if ( ! empty( $ids ) ) { + foreach ( $ids as $id ) { + if ( $id && $id > 0 ) { + $crosssells[] = $id; + } + } + } + + $product->set_cross_sell_ids( $crosssells ); + } + + // Product categories. + if ( isset( $request['categories'] ) && is_array( $request['categories'] ) ) { + $product = $this->save_taxonomy_terms( $product, $request['categories'] ); + } + + // Product tags. + if ( isset( $request['tags'] ) && is_array( $request['tags'] ) ) { + $product = $this->save_taxonomy_terms( $product, $request['tags'], 'tag' ); + } + + // Downloadable. + if ( isset( $request['downloadable'] ) ) { + $product->set_downloadable( $request['downloadable'] ); + } + + // Downloadable options. + if ( $product->get_downloadable() ) { + + // Downloadable files. + if ( isset( $request['downloads'] ) && is_array( $request['downloads'] ) ) { + $product = $this->save_downloadable_files( $product, $request['downloads'] ); + } + + // Download limit. + if ( isset( $request['download_limit'] ) ) { + $product->set_download_limit( $request['download_limit'] ); + } + + // Download expiry. + if ( isset( $request['download_expiry'] ) ) { + $product->set_download_expiry( $request['download_expiry'] ); + } + } + + // Product url and button text for external products. + if ( $product->is_type( 'external' ) ) { + if ( isset( $request['external_url'] ) ) { + $product->set_product_url( $request['external_url'] ); + } + + if ( isset( $request['button_text'] ) ) { + $product->set_button_text( $request['button_text'] ); + } + } + + // Save default attributes for variable products. + if ( $product->is_type( 'variable' ) ) { + $product = $this->save_default_attributes( $product, $request ); + } + + // Set children for a grouped product. + if ( $product->is_type( 'grouped' ) && isset( $request['grouped_products'] ) ) { + $product->set_children( $request['grouped_products'] ); + } + + // Check for featured/gallery images, upload it and set it. + if ( isset( $request['images'] ) ) { + $product = $this->set_product_images( $product, $request['images'] ); + } + + // Allow set meta_data. + if ( is_array( $request['meta_data'] ) ) { + foreach ( $request['meta_data'] as $meta ) { + $product->update_meta_data( $meta['key'], $meta['value'], isset( $meta['id'] ) ? $meta['id'] : '' ); + } + } + + if ( ! empty( $request['date_created'] ) ) { + $date = rest_parse_date( $request['date_created'] ); + + if ( $date ) { + $product->set_date_created( $date ); + } + } + + if ( ! empty( $request['date_created_gmt'] ) ) { + $date = rest_parse_date( $request['date_created_gmt'], true ); + + if ( $date ) { + $product->set_date_created( $date ); + } + } + + /** + * Filters an object before it is inserted via the REST API. + * + * The dynamic portion of the hook name, `$this->post_type`, + * refers to the object type slug. + * + * @param WC_Data $product Object object. + * @param WP_REST_Request $request Request object. + * @param bool $creating If is creating a new object. + */ + return apply_filters( "woocommerce_rest_pre_insert_{$this->post_type}_object", $product, $request, $creating ); + } + + /** + * Get the Product's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $weight_unit = get_option( 'woocommerce_weight_unit' ); + $dimension_unit = get_option( 'woocommerce_dimension_unit' ); + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => $this->post_type, + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Product name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'slug' => array( + 'description' => __( 'Product slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'permalink' => array( + 'description' => __( 'Product URL.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created' => array( + 'description' => __( "The date the product was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'date_created_gmt' => array( + 'description' => __( 'The date the product was created, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'date_modified' => array( + 'description' => __( "The date the product was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified_gmt' => array( + 'description' => __( 'The date the product was last modified, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'type' => array( + 'description' => __( 'Product type.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'simple', + 'enum' => array_keys( wc_get_product_types() ), + 'context' => array( 'view', 'edit' ), + ), + 'status' => array( + 'description' => __( 'Product status (post status).', 'woocommerce' ), + 'type' => 'string', + 'default' => 'publish', + 'enum' => array_merge( array_keys( get_post_statuses() ), array( 'future' ) ), + 'context' => array( 'view', 'edit' ), + ), + 'featured' => array( + 'description' => __( 'Featured product.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'catalog_visibility' => array( + 'description' => __( 'Catalog visibility.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'visible', + 'enum' => array( 'visible', 'catalog', 'search', 'hidden' ), + 'context' => array( 'view', 'edit' ), + ), + 'description' => array( + 'description' => __( 'Product description.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'short_description' => array( + 'description' => __( 'Product short description.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'sku' => array( + 'description' => __( 'Unique identifier.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'price' => array( + 'description' => __( 'Current product price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'regular_price' => array( + 'description' => __( 'Product regular price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'sale_price' => array( + 'description' => __( 'Product sale price.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_from' => array( + 'description' => __( "Start date of sale price, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_from_gmt' => array( + 'description' => __( 'Start date of sale price, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_to' => array( + 'description' => __( "End date of sale price, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'date_on_sale_to_gmt' => array( + 'description' => __( "End date of sale price, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + ), + 'price_html' => array( + 'description' => __( 'Price formatted in HTML.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'on_sale' => array( + 'description' => __( 'Shows if the product is on sale.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'purchasable' => array( + 'description' => __( 'Shows if the product can be bought.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'total_sales' => array( + 'description' => __( 'Amount of sales.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'virtual' => array( + 'description' => __( 'If the product is virtual.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'downloadable' => array( + 'description' => __( 'If the product is downloadable.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'downloads' => array( + 'description' => __( 'List of downloadable files.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'File ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'File name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'file' => array( + 'description' => __( 'File URL.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'download_limit' => array( + 'description' => __( 'Number of times downloadable files can be downloaded after purchase.', 'woocommerce' ), + 'type' => 'integer', + 'default' => -1, + 'context' => array( 'view', 'edit' ), + ), + 'download_expiry' => array( + 'description' => __( 'Number of days until access to downloadable files expires.', 'woocommerce' ), + 'type' => 'integer', + 'default' => -1, + 'context' => array( 'view', 'edit' ), + ), + 'external_url' => array( + 'description' => __( 'Product external URL. Only for external products.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + ), + 'button_text' => array( + 'description' => __( 'Product external button text. Only for external products.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'tax_status' => array( + 'description' => __( 'Tax status.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'taxable', + 'enum' => array( 'taxable', 'shipping', 'none' ), + 'context' => array( 'view', 'edit' ), + ), + 'tax_class' => array( + 'description' => __( 'Tax class.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'manage_stock' => array( + 'description' => __( 'Stock management at product level.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'stock_quantity' => array( + 'description' => __( 'Stock quantity.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'stock_status' => array( + 'description' => __( 'Controls the stock status of the product.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'instock', + 'enum' => array_keys( wc_get_product_stock_status_options() ), + 'context' => array( 'view', 'edit' ), + ), + 'backorders' => array( + 'description' => __( 'If managing stock, this controls if backorders are allowed.', 'woocommerce' ), + 'type' => 'string', + 'default' => 'no', + 'enum' => array( 'no', 'notify', 'yes' ), + 'context' => array( 'view', 'edit' ), + ), + 'backorders_allowed' => array( + 'description' => __( 'Shows if backorders are allowed.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'backordered' => array( + 'description' => __( 'Shows if the product is on backordered.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'sold_individually' => array( + 'description' => __( 'Allow one item to be bought in a single order.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'weight' => array( + /* translators: %s: weight unit */ + 'description' => sprintf( __( 'Product weight (%s).', 'woocommerce' ), $weight_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'dimensions' => array( + 'description' => __( 'Product dimensions.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'properties' => array( + 'length' => array( + /* translators: %s: dimension unit */ + 'description' => sprintf( __( 'Product length (%s).', 'woocommerce' ), $dimension_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'width' => array( + /* translators: %s: dimension unit */ + 'description' => sprintf( __( 'Product width (%s).', 'woocommerce' ), $dimension_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'height' => array( + /* translators: %s: dimension unit */ + 'description' => sprintf( __( 'Product height (%s).', 'woocommerce' ), $dimension_unit ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + 'shipping_required' => array( + 'description' => __( 'Shows if the product need to be shipped.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'shipping_taxable' => array( + 'description' => __( 'Shows whether or not the product shipping is taxable.', 'woocommerce' ), + 'type' => 'boolean', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'shipping_class' => array( + 'description' => __( 'Shipping class slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'shipping_class_id' => array( + 'description' => __( 'Shipping class ID.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'reviews_allowed' => array( + 'description' => __( 'Allow reviews.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => true, + 'context' => array( 'view', 'edit' ), + ), + 'average_rating' => array( + 'description' => __( 'Reviews average rating.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'rating_count' => array( + 'description' => __( 'Amount of reviews that the product have.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'related_ids' => array( + 'description' => __( 'List of related products IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'upsell_ids' => array( + 'description' => __( 'List of up-sell products IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + ), + 'cross_sell_ids' => array( + 'description' => __( 'List of cross-sell products IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + ), + 'parent_id' => array( + 'description' => __( 'Product parent ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'purchase_note' => array( + 'description' => __( 'Optional note to send the customer after purchase.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'categories' => array( + 'description' => __( 'List of categories.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Category ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Category name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'slug' => array( + 'description' => __( 'Category slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + 'tags' => array( + 'description' => __( 'List of tags.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Tag ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Tag name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'slug' => array( + 'description' => __( 'Tag slug.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ), + ), + 'images' => array( + 'description' => __( 'List of images.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Image ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'date_created' => array( + 'description' => __( "The date the image was created, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_created_gmt' => array( + 'description' => __( 'The date the image was created, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified' => array( + 'description' => __( "The date the image was last modified, in the site's timezone.", 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'date_modified_gmt' => array( + 'description' => __( 'The date the image was last modified, as GMT.', 'woocommerce' ), + 'type' => 'date-time', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'src' => array( + 'description' => __( 'Image URL.', 'woocommerce' ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Image name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'alt' => array( + 'description' => __( 'Image alternative text.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'attributes' => array( + 'description' => __( 'List of attributes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Attribute ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Attribute name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'position' => array( + 'description' => __( 'Attribute position.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'visible' => array( + 'description' => __( "Define if the attribute is visible on the \"Additional information\" tab in the product's page.", 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'variation' => array( + 'description' => __( 'Define if the attribute can be used as variation.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'context' => array( 'view', 'edit' ), + ), + 'options' => array( + 'description' => __( 'List of available term names of the attribute.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'default_attributes' => array( + 'description' => __( 'Defaults variation attributes.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Attribute ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'name' => array( + 'description' => __( 'Attribute name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'option' => array( + 'description' => __( 'Selected attribute term name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + 'variations' => array( + 'description' => __( 'List of variations IDs.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'integer', + ), + 'readonly' => true, + ), + 'grouped_products' => array( + 'description' => __( 'List of grouped products ID.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'menu_order' => array( + 'description' => __( 'Menu order, used to custom sort products.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + ), + 'meta_data' => array( + 'description' => __( 'Meta data.', 'woocommerce' ), + 'type' => 'array', + 'context' => array( 'view', 'edit' ), + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'Meta ID.', 'woocommerce' ), + 'type' => 'integer', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'key' => array( + 'description' => __( 'Meta key.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'value' => array( + 'description' => __( 'Meta value.', 'woocommerce' ), + 'type' => array( 'string', 'null' ), + 'context' => array( 'view', 'edit' ), + ), + ), + ), + ), + ), + ); + return $this->add_additional_fields_schema( $schema ); + } + + /** + * Add new options for 'orderby' to the collection params. + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + $params['orderby']['enum'] = array_merge( $params['orderby']['enum'], array( 'price', 'popularity', 'rating' ) ); + + unset( $params['in_stock'] ); + $params['stock_status'] = array( + 'description' => __( 'Limit result set to products with specified stock status.', 'woocommerce' ), + 'type' => 'string', + 'enum' => array_keys( wc_get_product_stock_status_options() ), + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } + + /** + * Get product data. + * + * @param WC_Product $product Product instance. + * @param string $context Request context. + * Options: 'view' and 'edit'. + * @return array + */ + protected function get_product_data( $product, $context = 'view' ) { + $data = parent::get_product_data( $product, $context ); + + // Replace in_stock with stock_status. + $pos = array_search( 'in_stock', array_keys( $data ), true ); + $array_section_1 = array_slice( $data, 0, $pos, true ); + $array_section_2 = array_slice( $data, $pos + 1, null, true ); + + return $array_section_1 + array( 'stock_status' => $product->get_stock_status( $context ) ) + $array_section_2; + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-report-coupons-totals-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-report-coupons-totals-controller.php new file mode 100644 index 00000000000..b6bfe4304b3 --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-report-coupons-totals-controller.php @@ -0,0 +1,143 @@ + $name ) { + $results = $wpdb->get_results( + $wpdb->prepare( " + SELECT count(meta_id) AS total + FROM $wpdb->postmeta + WHERE meta_key = 'discount_type' + AND meta_value = %s + ", $slug ) + ); + + $total = isset( $results[0] ) ? (int) $results[0]->total : 0; + + $data[] = array( + 'slug' => $slug, + 'name' => $name, + 'total' => $total, + ); + } + + set_transient( 'rest_api_coupons_type_count', $data, YEAR_IN_SECONDS ); + + return $data; + } + + /** + * Prepare a report object for serialization. + * + * @param stdClass $report Report data. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $report, $request ) { + $data = array( + 'slug' => $report->slug, + 'name' => $report->name, + 'total' => $report->total, + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + /** + * Filter a report returned from the API. + * + * Allows modification of the report data right before it is returned. + * + * @param WP_REST_Response $response The response object. + * @param object $report The original report object. + * @param WP_REST_Request $request Request used to generate the response. + */ + return apply_filters( 'woocommerce_rest_prepare_report_coupons_count', $response, $report, $request ); + } + + /** + * Get the Report's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'report_coupon_total', + 'type' => 'object', + 'properties' => array( + 'slug' => array( + 'description' => __( 'An alphanumeric identifier for the resource.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Coupon type name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Amount of coupons.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-report-customers-totals-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-report-customers-totals-controller.php new file mode 100644 index 00000000000..929a2f3c73a --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-report-customers-totals-controller.php @@ -0,0 +1,154 @@ + $total ) { + if ( in_array( $role, array( 'administrator', 'shop_manager' ), true ) ) { + continue; + } + + $total_customers += (int) $total; + } + + $customers_query = new WP_User_Query( + array( + 'role__not_in' => array( 'administrator', 'shop_manager' ), + 'number' => 0, + 'fields' => 'ID', + 'count_total' => true, + 'meta_query' => array( // WPCS: slow query ok. + array( + 'key' => 'paying_customer', + 'value' => 1, + 'compare' => '=', + ), + ), + ) + ); + + $total_paying = (int) $customers_query->get_total(); + + $data = array( + array( + 'slug' => 'paying', + 'name' => __( 'Paying customer', 'woocommerce' ), + 'total' => $total_paying, + ), + array( + 'slug' => 'non_paying', + 'name' => __( 'Non-paying customer', 'woocommerce' ), + 'total' => $total_customers - $total_paying, + ), + ); + + return $data; + } + + /** + * Prepare a report object for serialization. + * + * @param stdClass $report Report data. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $report, $request ) { + $data = array( + 'slug' => $report->slug, + 'name' => $report->name, + 'total' => $report->total, + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + /** + * Filter a report returned from the API. + * + * Allows modification of the report data right before it is returned. + * + * @param WP_REST_Response $response The response object. + * @param object $report The original report object. + * @param WP_REST_Request $request Request used to generate the response. + */ + return apply_filters( 'woocommerce_rest_prepare_report_customers_count', $response, $report, $request ); + } + + /** + * Get the Report's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'report_customer_total', + 'type' => 'object', + 'properties' => array( + 'slug' => array( + 'description' => __( 'An alphanumeric identifier for the resource.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Customer type name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Amount of customers.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-report-orders-totals-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-report-orders-totals-controller.php new file mode 100644 index 00000000000..f70ebe6a5a4 --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-report-orders-totals-controller.php @@ -0,0 +1,127 @@ + $name ) { + if ( ! isset( $totals->$slug ) ) { + continue; + } + + $data[] = array( + 'slug' => str_replace( 'wc-', '', $slug ), + 'name' => $name, + 'total' => (int) $totals->$slug, + ); + } + + return $data; + } + + /** + * Prepare a report object for serialization. + * + * @param stdClass $report Report data. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $report, $request ) { + $data = array( + 'slug' => $report->slug, + 'name' => $report->name, + 'total' => $report->total, + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + /** + * Filter a report returned from the API. + * + * Allows modification of the report data right before it is returned. + * + * @param WP_REST_Response $response The response object. + * @param object $report The original report object. + * @param WP_REST_Request $request Request used to generate the response. + */ + return apply_filters( 'woocommerce_rest_prepare_report_orders_count', $response, $report, $request ); + } + + /** + * Get the Report's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'report_order_total', + 'type' => 'object', + 'properties' => array( + 'slug' => array( + 'description' => __( 'An alphanumeric identifier for the resource.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Order status name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Amount of orders.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-report-products-totals-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-report-products-totals-controller.php new file mode 100644 index 00000000000..c45671c50d9 --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-report-products-totals-controller.php @@ -0,0 +1,133 @@ + 'product_type', + 'hide_empty' => false, + ) + ); + $data = array(); + + foreach ( $terms as $product_type ) { + if ( ! isset( $types[ $product_type->name ] ) ) { + continue; + } + + $data[] = array( + 'slug' => $product_type->name, + 'name' => $types[ $product_type->name ], + 'total' => (int) $product_type->count, + ); + } + + return $data; + } + + /** + * Prepare a report object for serialization. + * + * @param stdClass $report Report data. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $report, $request ) { + $data = array( + 'slug' => $report->slug, + 'name' => $report->name, + 'total' => $report->total, + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + /** + * Filter a report returned from the API. + * + * Allows modification of the report data right before it is returned. + * + * @param WP_REST_Response $response The response object. + * @param object $report The original report object. + * @param WP_REST_Request $request Request used to generate the response. + */ + return apply_filters( 'woocommerce_rest_prepare_report_products_count', $response, $report, $request ); + } + + /** + * Get the Report's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'report_product_total', + 'type' => 'object', + 'properties' => array( + 'slug' => array( + 'description' => __( 'An alphanumeric identifier for the resource.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Product type name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Amount of products.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-report-reviews-totals-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-report-reviews-totals-controller.php new file mode 100644 index 00000000000..c7b5cf2249a --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-report-reviews-totals-controller.php @@ -0,0 +1,132 @@ + true, + 'post_type' => 'product', + 'meta_key' => 'rating', // WPCS: slow query ok. + 'meta_value' => '', // WPCS: slow query ok. + ); + + for ( $i = 1; $i <= 5; $i++ ) { + $query_data['meta_value'] = $i; + + $data[] = array( + 'slug' => 'rated_' . $i . '_out_of_5', + /* translators: %s: average rating */ + 'name' => sprintf( __( 'Rated %s out of 5', 'woocommerce' ), $i ), + 'total' => (int) get_comments( $query_data ), + ); + } + + return $data; + } + + /** + * Prepare a report object for serialization. + * + * @param stdClass $report Report data. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response $response Response data. + */ + public function prepare_item_for_response( $report, $request ) { + $data = array( + 'slug' => $report->slug, + 'name' => $report->name, + 'total' => $report->total, + ); + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + /** + * Filter a report returned from the API. + * + * Allows modification of the report data right before it is returned. + * + * @param WP_REST_Response $response The response object. + * @param object $report The original report object. + * @param WP_REST_Request $request Request used to generate the response. + */ + return apply_filters( 'woocommerce_rest_prepare_report_reviews_count', $response, $report, $request ); + } + + /** + * Get the Report's schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'report_review_total', + 'type' => 'object', + 'properties' => array( + 'slug' => array( + 'description' => __( 'An alphanumeric identifier for the resource.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'Review type name.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + 'total' => array( + 'description' => __( 'Amount of reviews.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-report-sales-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-report-sales-controller.php new file mode 100644 index 00000000000..bde4bd0ae51 --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-report-sales-controller.php @@ -0,0 +1,27 @@ + 'orders/totals', + 'description' => __( 'Orders totals.', 'woocommerce' ), + ); + $reports[] = array( + 'slug' => 'products/totals', + 'description' => __( 'Products totals.', 'woocommerce' ), + ); + $reports[] = array( + 'slug' => 'customers/totals', + 'description' => __( 'Customers totals.', 'woocommerce' ), + ); + $reports[] = array( + 'slug' => 'coupons/totals', + 'description' => __( 'Coupons totals.', 'woocommerce' ), + ); + $reports[] = array( + 'slug' => 'reviews/totals', + 'description' => __( 'Reviews totals.', 'woocommerce' ), + ); + $reports[] = array( + 'slug' => 'categories/totals', + 'description' => __( 'Categories totals.', 'woocommerce' ), + ); + $reports[] = array( + 'slug' => 'tags/totals', + 'description' => __( 'Tags totals.', 'woocommerce' ), + ); + $reports[] = array( + 'slug' => 'attributes/totals', + 'description' => __( 'Attributes totals.', 'woocommerce' ), + ); + + return $reports; + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-setting-options-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-setting-options-controller.php new file mode 100644 index 00000000000..22afa819a38 --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-setting-options-controller.php @@ -0,0 +1,256 @@ + 404 ) ); + } + + $settings = apply_filters( 'woocommerce_settings-' . $group_id, array() ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores + + if ( empty( $settings ) ) { + return new WP_Error( 'rest_setting_setting_group_invalid', __( 'Invalid setting group.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + $filtered_settings = array(); + foreach ( $settings as $setting ) { + $option_key = $setting['option_key']; + $setting = $this->filter_setting( $setting ); + $default = isset( $setting['default'] ) ? $setting['default'] : ''; + // Get the option value. + if ( is_array( $option_key ) ) { + $option = get_option( $option_key[0] ); + $setting['value'] = isset( $option[ $option_key[1] ] ) ? $option[ $option_key[1] ] : $default; + } else { + $admin_setting_value = WC_Admin_Settings::get_option( $option_key, $default ); + $setting['value'] = $admin_setting_value; + } + + if ( 'multi_select_countries' === $setting['type'] ) { + $setting['options'] = WC()->countries->get_countries(); + $setting['type'] = 'multiselect'; + } elseif ( 'single_select_country' === $setting['type'] ) { + $setting['type'] = 'select'; + $setting['options'] = $this->get_countries_and_states(); + } elseif ( 'single_select_page' === $setting['type'] ) { + $pages = get_pages( + array( + 'sort_column' => 'menu_order', + 'sort_order' => 'ASC', + 'hierarchical' => 0, + ) + ); + $options = array(); + foreach ( $pages as $page ) { + $options[ $page->ID ] = ! empty( $page->post_title ) ? $page->post_title : '#' . $page->ID; + } + $setting['type'] = 'select'; + $setting['options'] = $options; + } + + $filtered_settings[] = $setting; + } + + return $filtered_settings; + } + + /** + * Returns a list of countries and states for use in the base location setting. + * + * @since 3.0.7 + * @return array Array of states and countries. + */ + private function get_countries_and_states() { + $countries = WC()->countries->get_countries(); + if ( ! $countries ) { + return array(); + } + $output = array(); + foreach ( $countries as $key => $value ) { + $states = WC()->countries->get_states( $key ); + + if ( $states ) { + foreach ( $states as $state_key => $state_value ) { + $output[ $key . ':' . $state_key ] = $value . ' - ' . $state_value; + } + } else { + $output[ $key ] = $value; + } + } + return $output; + } + + /** + * Get the settings schema, conforming to JSON Schema. + * + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'setting', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'A unique identifier for the setting.', 'woocommerce' ), + 'type' => 'string', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_title', + ), + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'group_id' => array( + 'description' => __( 'An identifier for the group this setting belongs to.', 'woocommerce' ), + 'type' => 'string', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_title', + ), + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'label' => array( + 'description' => __( 'A human readable label for the setting used in interfaces.', 'woocommerce' ), + 'type' => 'string', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'description' => array( + 'description' => __( 'A human readable description for the setting used in interfaces.', 'woocommerce' ), + 'type' => 'string', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'value' => array( + 'description' => __( 'Setting value.', 'woocommerce' ), + 'type' => array( 'string', 'array', 'null' ), + 'items' => array( + 'type' => array( 'string', 'null' ), + ), + 'context' => array( 'view', 'edit' ), + ), + 'default' => array( + 'description' => __( 'Default value for the setting.', 'woocommerce' ), + 'type' => array( 'string', 'array', 'null' ), + 'items' => array( + 'type' => array( 'string', 'null' ), + ), + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'tip' => array( + 'description' => __( 'Additional help text shown to the user about the setting.', 'woocommerce' ), + 'type' => 'string', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'placeholder' => array( + 'description' => __( 'Placeholder text to be displayed in text inputs.', 'woocommerce' ), + 'type' => 'string', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + 'type' => array( + 'description' => __( 'Type of setting.', 'woocommerce' ), + 'type' => 'string', + 'arg_options' => array( + 'sanitize_callback' => 'sanitize_text_field', + ), + 'context' => array( 'view', 'edit' ), + 'enum' => array( 'text', 'email', 'number', 'color', 'password', 'textarea', 'select', 'multiselect', 'radio', 'image_width', 'checkbox' ), + 'readonly' => true, + ), + 'options' => array( + 'description' => __( 'Array of options (key value pairs) for inputs such as select, multiselect, and radio buttons.', 'woocommerce' ), + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-settings-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-settings-controller.php new file mode 100644 index 00000000000..77aebd681d4 --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-settings-controller.php @@ -0,0 +1,112 @@ +namespace, '/' . $this->rest_base . '/batch', array( + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'batch_items' ), + 'permission_callback' => array( $this, 'update_items_permissions_check' ), + ), + 'schema' => array( $this, 'get_public_batch_schema' ), + ) ); + } + + /** + * Makes sure the current user has access to WRITE the settings APIs. + * + * @param WP_REST_Request $request Full data about the request. + * @return WP_Error|bool + */ + public function update_items_permissions_check( $request ) { + if ( ! wc_rest_check_manager_permissions( 'settings', 'edit' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you cannot edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Update a setting. + * + * @param WP_REST_Request $request Request data. + * @return WP_Error|WP_REST_Response + */ + public function update_item( $request ) { + $options_controller = new WC_REST_Setting_Options_Controller(); + $response = $options_controller->update_item( $request ); + + return $response; + } + + /** + * Get the groups schema, conforming to JSON Schema. + * + * @since 3.0.0 + * @return array + */ + public function get_item_schema() { + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'setting_group', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'description' => __( 'A unique identifier that can be used to link settings together.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'label' => array( + 'description' => __( 'A human readable label for the setting used in interfaces.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'description' => array( + 'description' => __( 'A human readable description for the setting used in interfaces.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'parent_id' => array( + 'description' => __( 'ID of parent grouping.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + 'sub_groups' => array( + 'description' => __( 'IDs for settings sub groups.', 'woocommerce' ), + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ), + ); + + return $this->add_additional_fields_schema( $schema ); + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-methods-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-methods-controller.php new file mode 100644 index 00000000000..297943ae544 --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-methods-controller.php @@ -0,0 +1,27 @@ +/locations endpoint. + * + * @package Automattic/WooCommerce/RestApi + * @since 3.0.0 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * REST API Shipping Zone Locations class. + * + * @package Automattic/WooCommerce/RestApi + * @extends WC_REST_Shipping_Zone_Locations_V2_Controller + */ +class WC_REST_Shipping_Zone_Locations_Controller extends WC_REST_Shipping_Zone_Locations_V2_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + protected $namespace = 'wc/v3'; +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zone-methods-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zone-methods-controller.php new file mode 100644 index 00000000000..efb56f49afd --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zone-methods-controller.php @@ -0,0 +1,27 @@ +/methods endpoint. + * + * @package Automattic/WooCommerce/RestApi + * @since 3.0.0 + */ + +defined( 'ABSPATH' ) || exit; + +/** + * REST API Shipping Zone Methods class. + * + * @package Automattic/WooCommerce/RestApi + * @extends WC_REST_Shipping_Zone_Methods_V2_Controller + */ +class WC_REST_Shipping_Zone_Methods_Controller extends WC_REST_Shipping_Zone_Methods_V2_Controller { + + /** + * Endpoint namespace. + * + * @var string + */ + protected $namespace = 'wc/v3'; +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zones-controller-base.php b/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zones-controller-base.php new file mode 100644 index 00000000000..871c5199e31 --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zones-controller-base.php @@ -0,0 +1,125 @@ + 404 ) ); + } + + return $zone; + } + + /** + * Check whether a given request has permission to read Shipping Zones. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + if ( ! wc_shipping_enabled() ) { + return new WP_Error( 'rest_no_route', __( 'Shipping is disabled.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + if ( ! wc_rest_check_manager_permissions( 'settings', 'read' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to create Shipping Zones. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function create_item_permissions_check( $request ) { + if ( ! wc_shipping_enabled() ) { + return new WP_Error( 'rest_no_route', __( 'Shipping is disabled.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + if ( ! wc_rest_check_manager_permissions( 'settings', 'edit' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check whether a given request has permission to edit Shipping Zones. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function update_items_permissions_check( $request ) { + if ( ! wc_shipping_enabled() ) { + return new WP_Error( 'rest_no_route', __( 'Shipping is disabled.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + if ( ! wc_rest_check_manager_permissions( 'settings', 'edit' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check whether a given request has permission to delete Shipping Zones. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function delete_items_permissions_check( $request ) { + if ( ! wc_shipping_enabled() ) { + return new WP_Error( 'rest_no_route', __( 'Shipping is disabled.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + if ( ! wc_rest_check_manager_permissions( 'settings', 'delete' ) ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zones-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zones-controller.php new file mode 100644 index 00000000000..881d18dc3c1 --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-shipping-zones-controller.php @@ -0,0 +1,27 @@ +namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( $this, 'create_item_permissions_check' ), + 'args' => array_merge( + $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), + array( + 'name' => array( + 'type' => 'string', + 'description' => __( 'Name for the resource.', 'woocommerce' ), + 'required' => true, + ), + ) + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P[\d]+)', + array( + 'args' => array( + 'id' => array( + 'description' => __( 'Unique identifier for the resource.', 'woocommerce' ), + 'type' => 'integer', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'update_item' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'delete_item' ), + 'permission_callback' => array( $this, 'delete_item_permissions_check' ), + 'args' => array( + 'force' => array( + 'default' => false, + 'type' => 'boolean', + 'description' => __( 'Required to be true, as resource does not support trashing.', 'woocommerce' ), + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/batch', + array( + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'batch_items' ), + 'permission_callback' => array( $this, 'batch_items_permissions_check' ), + 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), + ), + 'schema' => array( $this, 'get_public_batch_schema' ), + ) + ); + } + + /** + * Check if a given request has access to read the terms. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_items_permissions_check( $request ) { + $permissions = $this->check_permissions( $request, 'read' ); + if ( is_wp_error( $permissions ) ) { + return $permissions; + } + + if ( ! $permissions ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot list resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to create a term. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function create_item_permissions_check( $request ) { + $permissions = $this->check_permissions( $request, 'create' ); + if ( is_wp_error( $permissions ) ) { + return $permissions; + } + + if ( ! $permissions ) { + return new WP_Error( 'woocommerce_rest_cannot_create', __( 'Sorry, you are not allowed to create resources.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to read a term. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function get_item_permissions_check( $request ) { + $permissions = $this->check_permissions( $request, 'read' ); + if ( is_wp_error( $permissions ) ) { + return $permissions; + } + + if ( ! $permissions ) { + return new WP_Error( 'woocommerce_rest_cannot_view', __( 'Sorry, you cannot view this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to update a term. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function update_item_permissions_check( $request ) { + $permissions = $this->check_permissions( $request, 'edit' ); + if ( is_wp_error( $permissions ) ) { + return $permissions; + } + + if ( ! $permissions ) { + return new WP_Error( 'woocommerce_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access to delete a term. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|boolean + */ + public function delete_item_permissions_check( $request ) { + $permissions = $this->check_permissions( $request, 'delete' ); + if ( is_wp_error( $permissions ) ) { + return $permissions; + } + + if ( ! $permissions ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Sorry, you are not allowed to delete this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check if a given request has access batch create, update and delete items. + * + * @param WP_REST_Request $request Full details about the request. + * @return boolean|WP_Error + */ + public function batch_items_permissions_check( $request ) { + $permissions = $this->check_permissions( $request, 'batch' ); + if ( is_wp_error( $permissions ) ) { + return $permissions; + } + + if ( ! $permissions ) { + return new WP_Error( 'woocommerce_rest_cannot_batch', __( 'Sorry, you are not allowed to batch manipulate this resource.', 'woocommerce' ), array( 'status' => rest_authorization_required_code() ) ); + } + + return true; + } + + /** + * Check permissions. + * + * @param WP_REST_Request $request Full details about the request. + * @param string $context Request context. + * @return bool|WP_Error + */ + protected function check_permissions( $request, $context = 'read' ) { + // Get taxonomy. + $taxonomy = $this->get_taxonomy( $request ); + if ( ! $taxonomy || ! taxonomy_exists( $taxonomy ) ) { + return new WP_Error( 'woocommerce_rest_taxonomy_invalid', __( 'Taxonomy does not exist.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + // Check permissions for a single term. + $id = intval( $request['id'] ); + if ( $id ) { + $term = get_term( $id, $taxonomy ); + + if ( is_wp_error( $term ) || ! $term || $term->taxonomy !== $taxonomy ) { + return new WP_Error( 'woocommerce_rest_term_invalid', __( 'Resource does not exist.', 'woocommerce' ), array( 'status' => 404 ) ); + } + + return wc_rest_check_product_term_permissions( $taxonomy, $context, $term->term_id ); + } + + return wc_rest_check_product_term_permissions( $taxonomy, $context ); + } + + /** + * Get terms associated with a taxonomy. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error + */ + public function get_items( $request ) { + $taxonomy = $this->get_taxonomy( $request ); + $prepared_args = array( + 'exclude' => $request['exclude'], + 'include' => $request['include'], + 'order' => $request['order'], + 'orderby' => $request['orderby'], + 'product' => $request['product'], + 'hide_empty' => $request['hide_empty'], + 'number' => $request['per_page'], + 'search' => $request['search'], + 'slug' => $request['slug'], + ); + + if ( ! empty( $request['offset'] ) ) { + $prepared_args['offset'] = $request['offset']; + } else { + $prepared_args['offset'] = ( $request['page'] - 1 ) * $prepared_args['number']; + } + + $taxonomy_obj = get_taxonomy( $taxonomy ); + + if ( $taxonomy_obj->hierarchical && isset( $request['parent'] ) ) { + if ( 0 === $request['parent'] ) { + // Only query top-level terms. + $prepared_args['parent'] = 0; + } else { + if ( $request['parent'] ) { + $prepared_args['parent'] = $request['parent']; + } + } + } + + /** + * Filter the query arguments, before passing them to `get_terms()`. + * + * Enables adding extra arguments or setting defaults for a terms + * collection request. + * + * @see https://developer.wordpress.org/reference/functions/get_terms/ + * + * @param array $prepared_args Array of arguments to be + * passed to get_terms. + * @param WP_REST_Request $request The current request. + */ + $prepared_args = apply_filters( "woocommerce_rest_{$taxonomy}_query", $prepared_args, $request ); + + if ( ! empty( $prepared_args['product'] ) ) { + $query_result = $this->get_terms_for_product( $prepared_args, $request ); + $total_terms = $this->total_terms; + } else { + $query_result = get_terms( $taxonomy, $prepared_args ); + + $count_args = $prepared_args; + unset( $count_args['number'] ); + unset( $count_args['offset'] ); + $total_terms = wp_count_terms( $taxonomy, $count_args ); + + // Ensure we don't return results when offset is out of bounds. + // See https://core.trac.wordpress.org/ticket/35935. + if ( $prepared_args['offset'] && $prepared_args['offset'] >= $total_terms ) { + $query_result = array(); + } + + // wp_count_terms can return a falsy value when the term has no children. + if ( ! $total_terms ) { + $total_terms = 0; + } + } + $response = array(); + foreach ( $query_result as $term ) { + $data = $this->prepare_item_for_response( $term, $request ); + $response[] = $this->prepare_response_for_collection( $data ); + } + + $response = rest_ensure_response( $response ); + + // Store pagination values for headers then unset for count query. + $per_page = (int) $prepared_args['number']; + $page = ceil( ( ( (int) $prepared_args['offset'] ) / $per_page ) + 1 ); + + $response->header( 'X-WP-Total', (int) $total_terms ); + $max_pages = ceil( $total_terms / $per_page ); + $response->header( 'X-WP-TotalPages', (int) $max_pages ); + + $base = str_replace( '(?P[\d]+)', $request['attribute_id'], $this->rest_base ); + $base = add_query_arg( $request->get_query_params(), rest_url( '/' . $this->namespace . '/' . $base ) ); + if ( $page > 1 ) { + $prev_page = $page - 1; + if ( $prev_page > $max_pages ) { + $prev_page = $max_pages; + } + $prev_link = add_query_arg( 'page', $prev_page, $base ); + $response->link_header( 'prev', $prev_link ); + } + if ( $max_pages > $page ) { + $next_page = $page + 1; + $next_link = add_query_arg( 'page', $next_page, $base ); + $response->link_header( 'next', $next_link ); + } + + return $response; + } + + /** + * Create a single term for a taxonomy. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Request|WP_Error + */ + public function create_item( $request ) { + $taxonomy = $this->get_taxonomy( $request ); + $name = $request['name']; + $args = array(); + $schema = $this->get_item_schema(); + + if ( ! empty( $schema['properties']['description'] ) && isset( $request['description'] ) ) { + $args['description'] = $request['description']; + } + if ( isset( $request['slug'] ) ) { + $args['slug'] = $request['slug']; + } + if ( isset( $request['parent'] ) ) { + if ( ! is_taxonomy_hierarchical( $taxonomy ) ) { + return new WP_Error( 'woocommerce_rest_taxonomy_not_hierarchical', __( 'Can not set resource parent, taxonomy is not hierarchical.', 'woocommerce' ), array( 'status' => 400 ) ); + } + $args['parent'] = $request['parent']; + } + + $term = wp_insert_term( $name, $taxonomy, $args ); + if ( is_wp_error( $term ) ) { + $error_data = array( 'status' => 400 ); + + // If we're going to inform the client that the term exists, + // give them the identifier they can actually use. + $term_id = $term->get_error_data( 'term_exists' ); + if ( $term_id ) { + $error_data['resource_id'] = $term_id; + } + + return new WP_Error( $term->get_error_code(), $term->get_error_message(), $error_data ); + } + + $term = get_term( $term['term_id'], $taxonomy ); + + $this->update_additional_fields_for_object( $term, $request ); + + // Add term data. + $meta_fields = $this->update_term_meta_fields( $term, $request ); + if ( is_wp_error( $meta_fields ) ) { + wp_delete_term( $term->term_id, $taxonomy ); + + return $meta_fields; + } + + /** + * Fires after a single term is created or updated via the REST API. + * + * @param WP_Term $term Inserted Term object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating term, false when updating. + */ + do_action( "woocommerce_rest_insert_{$taxonomy}", $term, $request, true ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $term, $request ); + $response = rest_ensure_response( $response ); + $response->set_status( 201 ); + + $base = '/' . $this->namespace . '/' . $this->rest_base; + if ( ! empty( $request['attribute_id'] ) ) { + $base = str_replace( '(?P[\d]+)', (int) $request['attribute_id'], $base ); + } + + $response->header( 'Location', rest_url( $base . '/' . $term->term_id ) ); + + return $response; + } + + /** + * Get a single term from a taxonomy. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Request|WP_Error + */ + public function get_item( $request ) { + $taxonomy = $this->get_taxonomy( $request ); + $term = get_term( (int) $request['id'], $taxonomy ); + + if ( is_wp_error( $term ) ) { + return $term; + } + + $response = $this->prepare_item_for_response( $term, $request ); + + return rest_ensure_response( $response ); + } + + /** + * Update a single term from a taxonomy. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Request|WP_Error + */ + public function update_item( $request ) { + $taxonomy = $this->get_taxonomy( $request ); + $term = get_term( (int) $request['id'], $taxonomy ); + $schema = $this->get_item_schema(); + $prepared_args = array(); + + if ( isset( $request['name'] ) ) { + $prepared_args['name'] = $request['name']; + } + if ( ! empty( $schema['properties']['description'] ) && isset( $request['description'] ) ) { + $prepared_args['description'] = $request['description']; + } + if ( isset( $request['slug'] ) ) { + $prepared_args['slug'] = $request['slug']; + } + if ( isset( $request['parent'] ) ) { + if ( ! is_taxonomy_hierarchical( $taxonomy ) ) { + return new WP_Error( 'woocommerce_rest_taxonomy_not_hierarchical', __( 'Can not set resource parent, taxonomy is not hierarchical.', 'woocommerce' ), array( 'status' => 400 ) ); + } + $prepared_args['parent'] = $request['parent']; + } + + // Only update the term if we haz something to update. + if ( ! empty( $prepared_args ) ) { + $update = wp_update_term( $term->term_id, $term->taxonomy, $prepared_args ); + if ( is_wp_error( $update ) ) { + return $update; + } + } + + $term = get_term( (int) $request['id'], $taxonomy ); + + $this->update_additional_fields_for_object( $term, $request ); + + // Update term data. + $meta_fields = $this->update_term_meta_fields( $term, $request ); + if ( is_wp_error( $meta_fields ) ) { + return $meta_fields; + } + + /** + * Fires after a single term is created or updated via the REST API. + * + * @param WP_Term $term Inserted Term object. + * @param WP_REST_Request $request Request object. + * @param boolean $creating True when creating term, false when updating. + */ + do_action( "woocommerce_rest_insert_{$taxonomy}", $term, $request, false ); + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $term, $request ); + return rest_ensure_response( $response ); + } + + /** + * Delete a single term from a taxonomy. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error + */ + public function delete_item( $request ) { + $taxonomy = $this->get_taxonomy( $request ); + $force = isset( $request['force'] ) ? (bool) $request['force'] : false; + + // We don't support trashing for this type, error out. + if ( ! $force ) { + return new WP_Error( 'woocommerce_rest_trash_not_supported', __( 'Resource does not support trashing.', 'woocommerce' ), array( 'status' => 501 ) ); + } + + $term = get_term( (int) $request['id'], $taxonomy ); + // Get default category id. + $default_category_id = absint( get_option( 'default_product_cat', 0 ) ); + + // Prevent deleting the default product category. + if ( $default_category_id === (int) $request['id'] ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'Default product category cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) ); + } + + $request->set_param( 'context', 'edit' ); + $response = $this->prepare_item_for_response( $term, $request ); + + $retval = wp_delete_term( $term->term_id, $term->taxonomy ); + if ( ! $retval ) { + return new WP_Error( 'woocommerce_rest_cannot_delete', __( 'The resource cannot be deleted.', 'woocommerce' ), array( 'status' => 500 ) ); + } + + /** + * Fires after a single term is deleted via the REST API. + * + * @param WP_Term $term The deleted term. + * @param WP_REST_Response $response The response data. + * @param WP_REST_Request $request The request sent to the API. + */ + do_action( "woocommerce_rest_delete_{$taxonomy}", $term, $response, $request ); + + return $response; + } + + /** + * Prepare links for the request. + * + * @param object $term Term object. + * @param WP_REST_Request $request Full details about the request. + * @return array Links for the given term. + */ + protected function prepare_links( $term, $request ) { + $base = '/' . $this->namespace . '/' . $this->rest_base; + + if ( ! empty( $request['attribute_id'] ) ) { + $base = str_replace( '(?P[\d]+)', (int) $request['attribute_id'], $base ); + } + + $links = array( + 'self' => array( + 'href' => rest_url( trailingslashit( $base ) . $term->term_id ), + ), + 'collection' => array( + 'href' => rest_url( $base ), + ), + ); + + if ( $term->parent ) { + $parent_term = get_term( (int) $term->parent, $term->taxonomy ); + if ( $parent_term ) { + $links['up'] = array( + 'href' => rest_url( trailingslashit( $base ) . $parent_term->term_id ), + ); + } + } + + return $links; + } + + /** + * Update term meta fields. + * + * @param WP_Term $term Term object. + * @param WP_REST_Request $request Full details about the request. + * @return bool|WP_Error + */ + protected function update_term_meta_fields( $term, $request ) { + return true; + } + + /** + * Get the terms attached to a product. + * + * This is an alternative to `get_terms()` that uses `get_the_terms()` + * instead, which hits the object cache. There are a few things not + * supported, notably `include`, `exclude`. In `self::get_items()` these + * are instead treated as a full query. + * + * @param array $prepared_args Arguments for `get_terms()`. + * @param WP_REST_Request $request Full details about the request. + * @return array List of term objects. (Total count in `$this->total_terms`). + */ + protected function get_terms_for_product( $prepared_args, $request ) { + $taxonomy = $this->get_taxonomy( $request ); + + $query_result = get_the_terms( $prepared_args['product'], $taxonomy ); + if ( empty( $query_result ) ) { + $this->total_terms = 0; + return array(); + } + + // get_items() verifies that we don't have `include` set, and default. + // ordering is by `name`. + if ( ! in_array( $prepared_args['orderby'], array( 'name', 'none', 'include' ), true ) ) { + switch ( $prepared_args['orderby'] ) { + case 'id': + $this->sort_column = 'term_id'; + break; + case 'slug': + case 'term_group': + case 'description': + case 'count': + $this->sort_column = $prepared_args['orderby']; + break; + } + usort( $query_result, array( $this, 'compare_terms' ) ); + } + if ( strtolower( $prepared_args['order'] ) !== 'asc' ) { + $query_result = array_reverse( $query_result ); + } + + // Pagination. + $this->total_terms = count( $query_result ); + $query_result = array_slice( $query_result, $prepared_args['offset'], $prepared_args['number'] ); + + return $query_result; + } + + /** + * Comparison function for sorting terms by a column. + * + * Uses `$this->sort_column` to determine field to sort by. + * + * @param stdClass $left Term object. + * @param stdClass $right Term object. + * @return int <0 if left is higher "priority" than right, 0 if equal, >0 if right is higher "priority" than left. + */ + protected function compare_terms( $left, $right ) { + $col = $this->sort_column; + $left_val = $left->$col; + $right_val = $right->$col; + + if ( is_int( $left_val ) && is_int( $right_val ) ) { + return $left_val - $right_val; + } + + return strcmp( $left_val, $right_val ); + } + + /** + * Get the query params for collections + * + * @return array + */ + public function get_collection_params() { + $params = parent::get_collection_params(); + + if ( '' !== $this->taxonomy && taxonomy_exists( $this->taxonomy ) ) { + $taxonomy = get_taxonomy( $this->taxonomy ); + } else { + $taxonomy = new stdClass(); + $taxonomy->hierarchical = true; + } + + $params['context']['default'] = 'view'; + + $params['exclude'] = array( + 'description' => __( 'Ensure result set excludes specific IDs.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'default' => array(), + 'sanitize_callback' => 'wp_parse_id_list', + ); + $params['include'] = array( + 'description' => __( 'Limit result set to specific ids.', 'woocommerce' ), + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + ), + 'default' => array(), + 'sanitize_callback' => 'wp_parse_id_list', + ); + if ( ! $taxonomy->hierarchical ) { + $params['offset'] = array( + 'description' => __( 'Offset the result set by a specific number of items.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + } + $params['order'] = array( + 'description' => __( 'Order sort attribute ascending or descending.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_key', + 'default' => 'asc', + 'enum' => array( + 'asc', + 'desc', + ), + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['orderby'] = array( + 'description' => __( 'Sort collection by resource attribute.', 'woocommerce' ), + 'type' => 'string', + 'sanitize_callback' => 'sanitize_key', + 'default' => 'name', + 'enum' => array( + 'id', + 'include', + 'name', + 'slug', + 'term_group', + 'description', + 'count', + ), + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['hide_empty'] = array( + 'description' => __( 'Whether to hide resources not assigned to any products.', 'woocommerce' ), + 'type' => 'boolean', + 'default' => false, + 'validate_callback' => 'rest_validate_request_arg', + ); + if ( $taxonomy->hierarchical ) { + $params['parent'] = array( + 'description' => __( 'Limit result set to resources assigned to a specific parent.', 'woocommerce' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + 'validate_callback' => 'rest_validate_request_arg', + ); + } + $params['product'] = array( + 'description' => __( 'Limit result set to resources assigned to a specific product.', 'woocommerce' ), + 'type' => 'integer', + 'default' => null, + 'validate_callback' => 'rest_validate_request_arg', + ); + $params['slug'] = array( + 'description' => __( 'Limit result set to resources with a specific slug.', 'woocommerce' ), + 'type' => 'string', + 'validate_callback' => 'rest_validate_request_arg', + ); + + return $params; + } + + /** + * Get taxonomy. + * + * @param WP_REST_Request $request Full details about the request. + * @return int|WP_Error + */ + protected function get_taxonomy( $request ) { + // Check if taxonomy is defined. + // Prevents check for attribute taxonomy more than one time for each query. + if ( '' !== $this->taxonomy ) { + return $this->taxonomy; + } + + if ( ! empty( $request['attribute_id'] ) ) { + $taxonomy = wc_attribute_taxonomy_name_by_id( (int) $request['attribute_id'] ); + + $this->taxonomy = $taxonomy; + } + + return $this->taxonomy; + } +} diff --git a/includes/rest-api/Controllers/Version3/class-wc-rest-webhooks-controller.php b/includes/rest-api/Controllers/Version3/class-wc-rest-webhooks-controller.php new file mode 100644 index 00000000000..7b2817290fa --- /dev/null +++ b/includes/rest-api/Controllers/Version3/class-wc-rest-webhooks-controller.php @@ -0,0 +1,37 @@ +init() + */ + public static function init() { + wc_deprecated_function( 'Automattic\WooCommerce\RestApi\Server::instance()->init()', '4.5.0' ); + \Automattic\WooCommerce\RestApi\Server::instance()->init(); + } + + /** + * Return the version of the package. + * + * @deprecated since 4.5.0. This tracks WooCommerce version now. + * @return string + */ + public static function get_version() { + wc_deprecated_function( 'WC()->version', '4.5.0' ); + return WC()->version; + } + + /** + * Return the path to the package. + * + * @deprecated since 4.5.0. Directly call Automattic\WooCommerce\RestApi\Server::get_path() + * @return string + */ + public static function get_path() { + wc_deprecated_function( 'Automattic\WooCommerce\RestApi\Server::get_path()', '4.5.0' ); + return \Automattic\WooCommerce\RestApi\Server::get_path(); + } +} diff --git a/includes/rest-api/Server.php b/includes/rest-api/Server.php new file mode 100644 index 00000000000..4eb3384baa3 --- /dev/null +++ b/includes/rest-api/Server.php @@ -0,0 +1,190 @@ +get_rest_namespaces() as $namespace => $controllers ) { + foreach ( $controllers as $controller_name => $controller_class ) { + $this->controllers[ $namespace ][ $controller_name ] = new $controller_class(); + $this->controllers[ $namespace ][ $controller_name ]->register_routes(); + } + } + } + + /** + * Get API namespaces - new namespaces should be registered here. + * + * @return array List of Namespaces and Main controller classes. + */ + protected function get_rest_namespaces() { + return apply_filters( + 'woocommerce_rest_api_get_rest_namespaces', + array( + 'wc/v1' => $this->get_v1_controllers(), + 'wc/v2' => $this->get_v2_controllers(), + 'wc/v3' => $this->get_v3_controllers(), + ) + ); + } + + /** + * List of controllers in the wc/v1 namespace. + * + * @return array + */ + protected function get_v1_controllers() { + return array( + 'coupons' => 'WC_REST_Coupons_V1_Controller', + 'customer-downloads' => 'WC_REST_Customer_Downloads_V1_Controller', + 'customers' => 'WC_REST_Customers_V1_Controller', + 'order-notes' => 'WC_REST_Order_Notes_V1_Controller', + 'order-refunds' => 'WC_REST_Order_Refunds_V1_Controller', + 'orders' => 'WC_REST_Orders_V1_Controller', + 'product-attribute-terms' => 'WC_REST_Product_Attribute_Terms_V1_Controller', + 'product-attributes' => 'WC_REST_Product_Attributes_V1_Controller', + 'product-categories' => 'WC_REST_Product_Categories_V1_Controller', + 'product-reviews' => 'WC_REST_Product_Reviews_V1_Controller', + 'product-shipping-classes' => 'WC_REST_Product_Shipping_Classes_V1_Controller', + 'product-tags' => 'WC_REST_Product_Tags_V1_Controller', + 'products' => 'WC_REST_Products_V1_Controller', + 'reports-sales' => 'WC_REST_Report_Sales_V1_Controller', + 'reports-top-sellers' => 'WC_REST_Report_Top_Sellers_V1_Controller', + 'reports' => 'WC_REST_Reports_V1_Controller', + 'tax-classes' => 'WC_REST_Tax_Classes_V1_Controller', + 'taxes' => 'WC_REST_Taxes_V1_Controller', + 'webhooks' => 'WC_REST_Webhooks_V1_Controller', + 'webhook-deliveries' => 'WC_REST_Webhook_Deliveries_V1_Controller', + ); + } + + /** + * List of controllers in the wc/v2 namespace. + * + * @return array + */ + protected function get_v2_controllers() { + return array( + 'coupons' => 'WC_REST_Coupons_V2_Controller', + 'customer-downloads' => 'WC_REST_Customer_Downloads_V2_Controller', + 'customers' => 'WC_REST_Customers_V2_Controller', + 'network-orders' => 'WC_REST_Network_Orders_V2_Controller', + 'order-notes' => 'WC_REST_Order_Notes_V2_Controller', + 'order-refunds' => 'WC_REST_Order_Refunds_V2_Controller', + 'orders' => 'WC_REST_Orders_V2_Controller', + 'product-attribute-terms' => 'WC_REST_Product_Attribute_Terms_V2_Controller', + 'product-attributes' => 'WC_REST_Product_Attributes_V2_Controller', + 'product-categories' => 'WC_REST_Product_Categories_V2_Controller', + 'product-reviews' => 'WC_REST_Product_Reviews_V2_Controller', + 'product-shipping-classes' => 'WC_REST_Product_Shipping_Classes_V2_Controller', + 'product-tags' => 'WC_REST_Product_Tags_V2_Controller', + 'products' => 'WC_REST_Products_V2_Controller', + 'product-variations' => 'WC_REST_Product_Variations_V2_Controller', + 'reports-sales' => 'WC_REST_Report_Sales_V2_Controller', + 'reports-top-sellers' => 'WC_REST_Report_Top_Sellers_V2_Controller', + 'reports' => 'WC_REST_Reports_V2_Controller', + 'settings' => 'WC_REST_Settings_V2_Controller', + 'settings-options' => 'WC_REST_Setting_Options_V2_Controller', + 'shipping-zones' => 'WC_REST_Shipping_Zones_V2_Controller', + 'shipping-zone-locations' => 'WC_REST_Shipping_Zone_Locations_V2_Controller', + 'shipping-zone-methods' => 'WC_REST_Shipping_Zone_Methods_V2_Controller', + 'tax-classes' => 'WC_REST_Tax_Classes_V2_Controller', + 'taxes' => 'WC_REST_Taxes_V2_Controller', + 'webhooks' => 'WC_REST_Webhooks_V2_Controller', + 'webhook-deliveries' => 'WC_REST_Webhook_Deliveries_V2_Controller', + 'system-status' => 'WC_REST_System_Status_V2_Controller', + 'system-status-tools' => 'WC_REST_System_Status_Tools_V2_Controller', + 'shipping-methods' => 'WC_REST_Shipping_Methods_V2_Controller', + 'payment-gateways' => 'WC_REST_Payment_Gateways_V2_Controller', + ); + } + + /** + * List of controllers in the wc/v3 namespace. + * + * @return array + */ + protected function get_v3_controllers() { + return array( + 'coupons' => 'WC_REST_Coupons_Controller', + 'customer-downloads' => 'WC_REST_Customer_Downloads_Controller', + 'customers' => 'WC_REST_Customers_Controller', + 'network-orders' => 'WC_REST_Network_Orders_Controller', + 'order-notes' => 'WC_REST_Order_Notes_Controller', + 'order-refunds' => 'WC_REST_Order_Refunds_Controller', + 'orders' => 'WC_REST_Orders_Controller', + 'product-attribute-terms' => 'WC_REST_Product_Attribute_Terms_Controller', + 'product-attributes' => 'WC_REST_Product_Attributes_Controller', + 'product-categories' => 'WC_REST_Product_Categories_Controller', + 'product-reviews' => 'WC_REST_Product_Reviews_Controller', + 'product-shipping-classes' => 'WC_REST_Product_Shipping_Classes_Controller', + 'product-tags' => 'WC_REST_Product_Tags_Controller', + 'products' => 'WC_REST_Products_Controller', + 'product-variations' => 'WC_REST_Product_Variations_Controller', + 'reports-sales' => 'WC_REST_Report_Sales_Controller', + 'reports-top-sellers' => 'WC_REST_Report_Top_Sellers_Controller', + 'reports-orders-totals' => 'WC_REST_Report_Orders_Totals_Controller', + 'reports-products-totals' => 'WC_REST_Report_Products_Totals_Controller', + 'reports-customers-totals' => 'WC_REST_Report_Customers_Totals_Controller', + 'reports-coupons-totals' => 'WC_REST_Report_Coupons_Totals_Controller', + 'reports-reviews-totals' => 'WC_REST_Report_Reviews_Totals_Controller', + 'reports' => 'WC_REST_Reports_Controller', + 'settings' => 'WC_REST_Settings_Controller', + 'settings-options' => 'WC_REST_Setting_Options_Controller', + 'shipping-zones' => 'WC_REST_Shipping_Zones_Controller', + 'shipping-zone-locations' => 'WC_REST_Shipping_Zone_Locations_Controller', + 'shipping-zone-methods' => 'WC_REST_Shipping_Zone_Methods_Controller', + 'tax-classes' => 'WC_REST_Tax_Classes_Controller', + 'taxes' => 'WC_REST_Taxes_Controller', + 'webhooks' => 'WC_REST_Webhooks_Controller', + 'system-status' => 'WC_REST_System_Status_Controller', + 'system-status-tools' => 'WC_REST_System_Status_Tools_Controller', + 'shipping-methods' => 'WC_REST_Shipping_Methods_Controller', + 'payment-gateways' => 'WC_REST_Payment_Gateways_Controller', + 'data' => 'WC_REST_Data_Controller', + 'data-continents' => 'WC_REST_Data_Continents_Controller', + 'data-countries' => 'WC_REST_Data_Countries_Controller', + 'data-currencies' => 'WC_REST_Data_Currencies_Controller', + ); + } + + /** + * Return the path to the package. + * + * @return string + */ + public static function get_path() { + return dirname( __DIR__ ); + } +} diff --git a/includes/rest-api/Utilities/ImageAttachment.php b/includes/rest-api/Utilities/ImageAttachment.php new file mode 100644 index 00000000000..a9fab68b8c7 --- /dev/null +++ b/includes/rest-api/Utilities/ImageAttachment.php @@ -0,0 +1,93 @@ +id = (int) $id; + $this->object_id = (int) $object_id; + } + + /** + * Upload an attachment file. + * + * @throws \WC_REST_Exception REST API exceptions. + * @param string $src URL to file. + */ + public function upload_image_from_src( $src ) { + $upload = wc_rest_upload_image_from_url( esc_url_raw( $src ) ); + + if ( is_wp_error( $upload ) ) { + if ( ! apply_filters( 'woocommerce_rest_suppress_image_upload_error', false, $upload, $this->object_id, $images ) ) { + throw new \WC_REST_Exception( 'woocommerce_product_image_upload_error', $upload->get_error_message(), 400 ); + } else { + return; + } + } + + $this->id = wc_rest_set_uploaded_image_as_attachment( $upload, $this->object_id ); + + if ( ! wp_attachment_is_image( $this->id ) ) { + /* translators: %s: image ID */ + throw new \WC_REST_Exception( 'woocommerce_product_invalid_image_id', sprintf( __( '#%s is an invalid image ID.', 'woocommerce' ), $this->id ), 400 ); + } + } + + /** + * Update attachment alt text. + * + * @param string $text Text to set. + */ + public function update_alt_text( $text ) { + if ( ! $this->id ) { + return; + } + update_post_meta( $this->id, '_wp_attachment_image_alt', wc_clean( $text ) ); + } + + /** + * Update attachment name. + * + * @param string $text Text to set. + */ + public function update_name( $text ) { + if ( ! $this->id ) { + return; + } + wp_update_post( + array( + 'ID' => $this->id, + 'post_title' => $text, + ) + ); + } +} diff --git a/includes/rest-api/Utilities/SingletonTrait.php b/includes/rest-api/Utilities/SingletonTrait.php new file mode 100644 index 00000000000..37aef2e35c7 --- /dev/null +++ b/includes/rest-api/Utilities/SingletonTrait.php @@ -0,0 +1,49 @@ +has_status( 'cancelled' ) && ! $has_recorded ) { $action = 'increase'; $order->get_data_store()->set_recorded_coupon_usage_counts( $order, true ); + } elseif ( $order->has_status( 'cancelled' ) ) { + $order->get_data_store()->release_held_coupons( $order, true ); + return; } else { return; } diff --git a/includes/wc-order-item-functions.php b/includes/wc-order-item-functions.php index 825a22ac454..5f7c6a1f942 100644 --- a/includes/wc-order-item-functions.php +++ b/includes/wc-order-item-functions.php @@ -4,7 +4,7 @@ * * Functions for order specific things. * - * @package WooCommerce/Functions + * @package WooCommerce\Functions * @version 3.4.0 */ diff --git a/includes/wc-product-functions.php b/includes/wc-product-functions.php index e7ee0359e46..7edeef07136 100644 --- a/includes/wc-product-functions.php +++ b/includes/wc-product-functions.php @@ -4,7 +4,7 @@ * * Functions for product specific things. * - * @package WooCommerce/Functions + * @package WooCommerce\Functions * @version 3.0.0 */ diff --git a/includes/wc-rest-functions.php b/includes/wc-rest-functions.php index 065b079ed3f..54029b4074e 100644 --- a/includes/wc-rest-functions.php +++ b/includes/wc-rest-functions.php @@ -4,7 +4,7 @@ * * Functions for REST specific things. * - * @package WooCommerce/Functions + * @package WooCommerce\Functions * @version 2.6.0 */ diff --git a/includes/wc-stock-functions.php b/includes/wc-stock-functions.php index c92e474d1a1..ccac0eac374 100644 --- a/includes/wc-stock-functions.php +++ b/includes/wc-stock-functions.php @@ -4,7 +4,7 @@ * * Functions used to manage product stock levels. * - * @package WooCommerce/Functions + * @package WooCommerce\Functions * @version 3.4.0 */ @@ -64,8 +64,8 @@ function wc_update_product_stock( $product, $stock_quantity = null, $operation = /** * Update a product's stock status. * - * @param int $product_id Product ID. - * @param string $status Status. + * @param int $product_id Product ID. + * @param string $status Status. */ function wc_update_product_stock_status( $product_id, $status ) { $product = wc_get_product( $product_id ); @@ -170,6 +170,13 @@ function wc_reduce_stock_levels( $order_id ) { continue; } + /** + * Filter order item quantity. + * + * @param int|float $quantity Quantity. + * @param WC_Order $order Order data. + * @param WC_Order_Item_Product $item Order item data. + */ $qty = apply_filters( 'woocommerce_order_item_quantity', $item->get_quantity(), $order, $item ); $item_name = $product->get_formatted_name(); $new_stock = wc_update_product_stock( $product, $qty, 'decrease' ); diff --git a/includes/wc-template-hooks.php b/includes/wc-template-hooks.php index f45ea6a2d49..cc0b5189f24 100644 --- a/includes/wc-template-hooks.php +++ b/includes/wc-template-hooks.php @@ -4,7 +4,7 @@ * * Action/filter hooks used for WooCommerce functions/templates. * - * @package WooCommerce/Templates + * @package WooCommerce\Templates * @version 2.1.0 */ diff --git a/includes/wc-term-functions.php b/includes/wc-term-functions.php index 9d5032d8f5f..f40a89fb6b8 100644 --- a/includes/wc-term-functions.php +++ b/includes/wc-term-functions.php @@ -4,7 +4,7 @@ * * Functions for handling terms/term meta. * - * @package WooCommerce/Functions + * @package WooCommerce\Functions * @version 2.1.0 */ diff --git a/includes/wc-update-functions.php b/includes/wc-update-functions.php index 076aa177b14..2ebb79abdac 100644 --- a/includes/wc-update-functions.php +++ b/includes/wc-update-functions.php @@ -4,7 +4,7 @@ * * Functions for updating data, used by the background updater. * - * @package WooCommerce/Functions + * @package WooCommerce\Functions * @version 3.3.0 */ @@ -2175,3 +2175,70 @@ function wc_update_440_insert_attribute_terms_for_variable_products() { function wc_update_440_db_version() { WC_Install::update_db_version( '4.4.0' ); } + +/** + * Update DB version to 4.5.0. + */ +function wc_update_450_db_version() { + WC_Install::update_db_version( '4.5.0' ); +} + +/** + * Sanitize all coupons code. + * + * @return bool True to run again, false if completed. + */ +function wc_update_450_sanitize_coupons_code() { + global $wpdb; + + $coupon_id = 0; + $last_coupon_id = get_option( 'woocommerce_update_450_last_coupon_id', '0' ); + + $coupons = $wpdb->get_results( + $wpdb->prepare( + "SELECT ID, post_title FROM $wpdb->posts WHERE ID > %d AND post_type = 'shop_coupon' LIMIT 10", + $last_coupon_id + ), + ARRAY_A + ); + + if ( empty( $coupons ) ) { + delete_option( 'woocommerce_update_450_last_coupon_id' ); + return false; + } + + foreach ( $coupons as $key => $data ) { + $coupon_id = intval( $data['ID'] ); + $code = trim( wp_filter_kses( $data['post_title'] ) ); + + if ( ! empty( $code ) && $data['post_title'] !== $code ) { + $wpdb->update( + $wpdb->posts, + array( + 'post_title' => $code, + ), + array( + 'ID' => $coupon_id, + ), + array( + '%s', + ), + array( + '%d', + ) + ); + + // Clean cache. + clean_post_cache( $coupon_id ); + wp_cache_delete( WC_Cache_Helper::get_cache_prefix( 'coupons' ) . 'coupon_id_from_code_' . $data['post_title'], 'coupons' ); + } + } + + // Start the run again. + if ( $coupon_id ) { + return update_option( 'woocommerce_update_450_last_coupon_id', $coupon_id ); + } + + delete_option( 'woocommerce_update_450_last_coupon_id' ); + return false; +} diff --git a/includes/wc-user-functions.php b/includes/wc-user-functions.php index 5d49e84ae37..282582cf624 100644 --- a/includes/wc-user-functions.php +++ b/includes/wc-user-functions.php @@ -4,7 +4,7 @@ * * Functions for customers. * - * @package WooCommerce/Functions + * @package WooCommerce\Functions * @version 2.2.0 */ diff --git a/includes/wc-webhook-functions.php b/includes/wc-webhook-functions.php index b5205c6607b..bc1d15998eb 100644 --- a/includes/wc-webhook-functions.php +++ b/includes/wc-webhook-functions.php @@ -2,7 +2,7 @@ /** * WooCommerce Webhook functions * - * @package WooCommerce/Functions + * @package WooCommerce\Functions * @version 3.3.0 */ diff --git a/includes/wc-widget-functions.php b/includes/wc-widget-functions.php index 2972680371f..49da8f74a93 100644 --- a/includes/wc-widget-functions.php +++ b/includes/wc-widget-functions.php @@ -4,7 +4,7 @@ * * Widget related functions and widget registration. * - * @package WooCommerce/Functions + * @package WooCommerce\Functions * @version 2.3.0 */ diff --git a/includes/wccom-site/class-wc-wccom-site-installer-requirements-check.php b/includes/wccom-site/class-wc-wccom-site-installer-requirements-check.php index 8e54f3665ba..bfb300d83f1 100644 --- a/includes/wccom-site/class-wc-wccom-site-installer-requirements-check.php +++ b/includes/wccom-site/class-wc-wccom-site-installer-requirements-check.php @@ -2,7 +2,7 @@ /** * WooCommerce.com Product Installation Requirements Check. * - * @package WooCommerce\WooCommerce_Site + * @package WooCommerce\WCCom * @since 3.8.0 */ diff --git a/includes/wccom-site/class-wc-wccom-site-installer.php b/includes/wccom-site/class-wc-wccom-site-installer.php index efe4c48c05b..e066141f129 100644 --- a/includes/wccom-site/class-wc-wccom-site-installer.php +++ b/includes/wccom-site/class-wc-wccom-site-installer.php @@ -2,7 +2,7 @@ /** * WooCommerce.com Product Installation. * - * @package WooCommerce\WooCommerce_Site + * @package WooCommerce\WCCom * @since 3.7.0 */ diff --git a/includes/wccom-site/class-wc-wccom-site.php b/includes/wccom-site/class-wc-wccom-site.php index d710578a095..d09cb06982b 100644 --- a/includes/wccom-site/class-wc-wccom-site.php +++ b/includes/wccom-site/class-wc-wccom-site.php @@ -2,7 +2,7 @@ /** * WooCommerce.com Product Installation. * - * @package WooCommerce\WooCommerce_Site + * @package WooCommerce\WCCom * @since 3.7.0 */ diff --git a/includes/wccom-site/rest-api/class-wc-rest-wccom-site-installer-errors.php b/includes/wccom-site/rest-api/class-wc-rest-wccom-site-installer-errors.php index 0fb46ded4a8..366b9d071b0 100644 --- a/includes/wccom-site/rest-api/class-wc-rest-wccom-site-installer-errors.php +++ b/includes/wccom-site/rest-api/class-wc-rest-wccom-site-installer-errors.php @@ -2,7 +2,7 @@ /** * WCCOM Site Installer Errors Class * - * @package WooCommerce\WooCommerce_Site\Rest_Api + * @package WooCommerce\WCCom\API * @since 3.9.0 */ diff --git a/includes/wccom-site/rest-api/endpoints/class-wc-rest-wccom-site-installer-controller.php b/includes/wccom-site/rest-api/endpoints/class-wc-rest-wccom-site-installer-controller.php index d4f5dda0b0b..03daf78cc82 100644 --- a/includes/wccom-site/rest-api/endpoints/class-wc-rest-wccom-site-installer-controller.php +++ b/includes/wccom-site/rest-api/endpoints/class-wc-rest-wccom-site-installer-controller.php @@ -4,7 +4,7 @@ * * Handles requests to /installer. * - * @package WooCommerce\WooCommerce_Site\Rest_Api + * @package WooCommerce\WCCom\API * @since 3.7.0 */ @@ -13,7 +13,6 @@ defined( 'ABSPATH' ) || exit; /** * REST API WCCOM Site Installer Controller Class. * - * @package WooCommerce/WCCOM_Site/REST_API * @extends WC_REST_Controller */ class WC_REST_WCCOM_Site_Installer_Controller extends WC_REST_Controller { diff --git a/includes/widgets/class-wc-widget-cart.php b/includes/widgets/class-wc-widget-cart.php index 0f4cb8a2a31..cc76280b5a7 100644 --- a/includes/widgets/class-wc-widget-cart.php +++ b/includes/widgets/class-wc-widget-cart.php @@ -4,7 +4,7 @@ * * Displays shopping cart widget. * - * @package WooCommerce/Widgets + * @package WooCommerce\Widgets * @version 2.3.0 */ diff --git a/includes/widgets/class-wc-widget-layered-nav-filters.php b/includes/widgets/class-wc-widget-layered-nav-filters.php index 68ebfd4a320..a333081c097 100644 --- a/includes/widgets/class-wc-widget-layered-nav-filters.php +++ b/includes/widgets/class-wc-widget-layered-nav-filters.php @@ -2,7 +2,7 @@ /** * Layered Navigation Filters Widget. * - * @package WooCommerce/Widgets + * @package WooCommerce\Widgets * @version 2.3.0 */ diff --git a/includes/widgets/class-wc-widget-layered-nav.php b/includes/widgets/class-wc-widget-layered-nav.php index ea8e65c6265..8a8dbbab11e 100644 --- a/includes/widgets/class-wc-widget-layered-nav.php +++ b/includes/widgets/class-wc-widget-layered-nav.php @@ -2,7 +2,7 @@ /** * Layered nav widget * - * @package WooCommerce/Widgets + * @package WooCommerce\Widgets * @version 2.6.0 */ diff --git a/includes/widgets/class-wc-widget-price-filter.php b/includes/widgets/class-wc-widget-price-filter.php index a27bbf1b048..cd8c5907ce8 100644 --- a/includes/widgets/class-wc-widget-price-filter.php +++ b/includes/widgets/class-wc-widget-price-filter.php @@ -4,7 +4,7 @@ * * Generates a range slider to filter products by price. * - * @package WooCommerce/Widgets + * @package WooCommerce\Widgets * @version 2.3.0 */ diff --git a/includes/widgets/class-wc-widget-product-categories.php b/includes/widgets/class-wc-widget-product-categories.php index 5025968523c..26b590bcdf7 100644 --- a/includes/widgets/class-wc-widget-product-categories.php +++ b/includes/widgets/class-wc-widget-product-categories.php @@ -2,7 +2,7 @@ /** * Product Categories Widget * - * @package WooCommerce/Widgets + * @package WooCommerce\Widgets * @version 2.3.0 */ diff --git a/includes/widgets/class-wc-widget-product-search.php b/includes/widgets/class-wc-widget-product-search.php index 2df11926494..c27debbeb83 100644 --- a/includes/widgets/class-wc-widget-product-search.php +++ b/includes/widgets/class-wc-widget-product-search.php @@ -2,7 +2,7 @@ /** * Product Search Widget. * - * @package WooCommerce/Widgets + * @package WooCommerce\Widgets * @version 2.3.0 */ diff --git a/includes/widgets/class-wc-widget-product-tag-cloud.php b/includes/widgets/class-wc-widget-product-tag-cloud.php index c6f04988374..4d85bc0e45d 100644 --- a/includes/widgets/class-wc-widget-product-tag-cloud.php +++ b/includes/widgets/class-wc-widget-product-tag-cloud.php @@ -2,7 +2,7 @@ /** * Tag Cloud Widget. * - * @package WooCommerce/Widgets + * @package WooCommerce\Widgets * @version 3.4.0 */ diff --git a/includes/widgets/class-wc-widget-products.php b/includes/widgets/class-wc-widget-products.php index d9242f299ac..be373488920 100644 --- a/includes/widgets/class-wc-widget-products.php +++ b/includes/widgets/class-wc-widget-products.php @@ -2,7 +2,7 @@ /** * List products. One widget to rule them all. * - * @package WooCommerce/Widgets + * @package WooCommerce\Widgets * @version 3.3.0 */ diff --git a/includes/widgets/class-wc-widget-rating-filter.php b/includes/widgets/class-wc-widget-rating-filter.php index 9570f90deb5..669038860b1 100644 --- a/includes/widgets/class-wc-widget-rating-filter.php +++ b/includes/widgets/class-wc-widget-rating-filter.php @@ -2,7 +2,7 @@ /** * Rating Filter Widget and related functions. * - * @package WooCommerce/Widgets + * @package WooCommerce\Widgets * @version 2.6.0 */ diff --git a/includes/widgets/class-wc-widget-recent-reviews.php b/includes/widgets/class-wc-widget-recent-reviews.php index da2934400d1..635e26975b6 100644 --- a/includes/widgets/class-wc-widget-recent-reviews.php +++ b/includes/widgets/class-wc-widget-recent-reviews.php @@ -2,7 +2,7 @@ /** * Recent Reviews Widget. * - * @package WooCommerce/Widgets + * @package WooCommerce\Widgets * @version 2.3.0 */ diff --git a/includes/widgets/class-wc-widget-recently-viewed.php b/includes/widgets/class-wc-widget-recently-viewed.php index b721110f3be..e023927f6aa 100644 --- a/includes/widgets/class-wc-widget-recently-viewed.php +++ b/includes/widgets/class-wc-widget-recently-viewed.php @@ -2,7 +2,7 @@ /** * Recent Products Widget. * - * @package WooCommerce/Widgets + * @package WooCommerce\Widgets * @version 3.3.0 */ diff --git a/includes/widgets/class-wc-widget-top-rated-products.php b/includes/widgets/class-wc-widget-top-rated-products.php index 8af6b332dad..a5241ece94c 100644 --- a/includes/widgets/class-wc-widget-top-rated-products.php +++ b/includes/widgets/class-wc-widget-top-rated-products.php @@ -3,7 +3,7 @@ * Top Rated Products Widget. * Gets and displays top rated products in an unordered list. * - * @package WooCommerce/Widgets + * @package WooCommerce\Widgets * @version 3.3.0 */ diff --git a/package-lock.json b/package-lock.json index d15bf963e8a..27f3b90f3e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3668,6 +3668,24 @@ "regenerator-runtime": "^0.13.2" } }, + "@babel/runtime-corejs3": { + "version": "7.11.2", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.11.2.tgz", + "integrity": "sha512-qh5IR+8VgFz83VBa6OkaET6uN/mJOhHONuy3m1sgF0CV6mXdPSEBdA7e1eUbVvyNtANjMbg22JUv71BaDXLY6A==", + "dev": true, + "requires": { + "core-js-pure": "^3.0.0", + "regenerator-runtime": "^0.13.4" + }, + "dependencies": { + "regenerator-runtime": { + "version": "0.13.7", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", + "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==", + "dev": true + } + } + }, "@babel/template": { "version": "7.4.4", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.4.4.tgz", @@ -7645,6 +7663,12 @@ "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", "dev": true }, + "@types/eslint-visitor-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", + "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==", + "dev": true + }, "@types/events": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", @@ -7806,6 +7830,104 @@ "integrity": "sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw==", "dev": true }, + "@typescript-eslint/eslint-plugin": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.1.0.tgz", + "integrity": "sha512-D52KwdgkjYc+fmTZKW7CZpH5ZBJREJKZXRrveMiRCmlzZ+Rw9wRVJ1JAmHQ9b/+Ehy1ZeaylofDB9wwXUt83wg==", + "dev": true, + "requires": { + "@typescript-eslint/experimental-utils": "3.1.0", + "functional-red-black-tree": "^1.0.1", + "regexpp": "^3.0.0", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + }, + "dependencies": { + "@typescript-eslint/experimental-utils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-3.1.0.tgz", + "integrity": "sha512-Zf8JVC2K1svqPIk1CB/ehCiWPaERJBBokbMfNTNRczCbQSlQXaXtO/7OfYz9wZaecNvdSvVADt6/XQuIxhC79w==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.3", + "@typescript-eslint/typescript-estree": "3.1.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^2.0.0" + } + }, + "@typescript-eslint/typescript-estree": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-3.1.0.tgz", + "integrity": "sha512-+4nfYauqeQvK55PgFrmBWFVYb6IskLyOosYEmhH3mSVhfBp9AIJnjExdgDmKWoOBHRcPM8Ihfm2BFpZf0euUZQ==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "eslint-visitor-keys": "^1.1.0", + "glob": "^7.1.6", + "is-glob": "^4.0.1", + "lodash": "^4.17.15", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "lodash": { + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", + "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "regexpp": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", + "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", + "dev": true + }, + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", + "dev": true + } + } + }, "@typescript-eslint/experimental-utils": { "version": "2.34.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.34.0.tgz", @@ -7829,6 +7951,97 @@ } } }, + "@typescript-eslint/parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-3.1.0.tgz", + "integrity": "sha512-NcDSJK8qTA2tPfyGiPes9HtVKLbksmuYjlgGAUs7Ld2K0swdWibnCq9IJx9kJN8JJdgUJSorFiGaPHBgH81F/Q==", + "dev": true, + "requires": { + "@types/eslint-visitor-keys": "^1.0.0", + "@typescript-eslint/experimental-utils": "3.1.0", + "@typescript-eslint/typescript-estree": "3.1.0", + "eslint-visitor-keys": "^1.1.0" + }, + "dependencies": { + "@typescript-eslint/experimental-utils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-3.1.0.tgz", + "integrity": "sha512-Zf8JVC2K1svqPIk1CB/ehCiWPaERJBBokbMfNTNRczCbQSlQXaXtO/7OfYz9wZaecNvdSvVADt6/XQuIxhC79w==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.3", + "@typescript-eslint/typescript-estree": "3.1.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^2.0.0" + } + }, + "@typescript-eslint/typescript-estree": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-3.1.0.tgz", + "integrity": "sha512-+4nfYauqeQvK55PgFrmBWFVYb6IskLyOosYEmhH3mSVhfBp9AIJnjExdgDmKWoOBHRcPM8Ihfm2BFpZf0euUZQ==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "eslint-visitor-keys": "^1.1.0", + "glob": "^7.1.6", + "is-glob": "^4.0.1", + "lodash": "^4.17.15", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + } + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "lodash": { + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", + "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", + "dev": true + } + } + }, "@typescript-eslint/typescript-estree": { "version": "2.34.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.34.0.tgz", @@ -8741,6 +8954,4486 @@ } } }, + "@woocommerce/model-factories": { + "version": "file:tests/e2e/factories", + "dev": true, + "requires": { + "axios": "0.19.2", + "create-hmac": "1.1.7", + "faker": "4.1.0", + "fishery": "1.0.0", + "oauth-1.0a": "2.2.6" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "requires": { + "@babel/highlight": "^7.10.4" + } + }, + "@babel/core": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.10.4.tgz", + "integrity": "sha512-3A0tS0HWpy4XujGc7QtOIHTeNwUgWaZc/WuS5YQrfhU67jnVmsD6OGPc1AKHH0LJHQICGncy3+YUjIhVlfDdcA==", + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/generator": "^7.10.4", + "@babel/helper-module-transforms": "^7.10.4", + "@babel/helpers": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/traverse": "^7.10.4", + "@babel/types": "^7.10.4", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.1", + "json5": "^2.1.2", + "lodash": "^4.17.13", + "resolve": "^1.3.2", + "semver": "^5.4.1", + "source-map": "^0.5.0" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + } + } + }, + "@babel/generator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.10.4.tgz", + "integrity": "sha512-toLIHUIAgcQygFZRAQcsLQV3CBuX6yOIru1kJk/qqqvcRmZrYe6WavZTSG+bB8MxhnL9YPf+pKQfuiP161q7ng==", + "requires": { + "@babel/types": "^7.10.4", + "jsesc": "^2.5.1", + "lodash": "^4.17.13", + "source-map": "^0.5.0" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + } + } + }, + "@babel/helper-function-name": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", + "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", + "requires": { + "@babel/helper-get-function-arity": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", + "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", + "requires": { + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.10.4.tgz", + "integrity": "sha512-m5j85pK/KZhuSdM/8cHUABQTAslV47OjfIB9Cc7P+PvlAoBzdb79BGNfw8RhT5Mq3p+xGd0ZfAKixbrUZx0C7A==", + "requires": { + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-module-imports": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz", + "integrity": "sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw==", + "requires": { + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-module-transforms": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.10.4.tgz", + "integrity": "sha512-Er2FQX0oa3nV7eM1o0tNCTx7izmQtwAQsIiaLRWtavAAEcskb0XJ5OjJbVrYXWOTr8om921Scabn4/tzlx7j1Q==", + "requires": { + "@babel/helper-module-imports": "^7.10.4", + "@babel/helper-replace-supers": "^7.10.4", + "@babel/helper-simple-access": "^7.10.4", + "@babel/helper-split-export-declaration": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4", + "lodash": "^4.17.13" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz", + "integrity": "sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg==", + "requires": { + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==" + }, + "@babel/helper-replace-supers": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.10.4.tgz", + "integrity": "sha512-sPxZfFXocEymYTdVK1UNmFPBN+Hv5mJkLPsYWwGBxZAxaWfFu+xqp7b6qWD0yjNuNL2VKc6L5M18tOXUP7NU0A==", + "requires": { + "@babel/helper-member-expression-to-functions": "^7.10.4", + "@babel/helper-optimise-call-expression": "^7.10.4", + "@babel/traverse": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-simple-access": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.10.4.tgz", + "integrity": "sha512-0fMy72ej/VEvF8ULmX6yb5MtHG4uH4Dbd6I/aHDb/JVg0bbivwt9Wg+h3uMvX+QSFtwr5MeItvazbrc4jtRAXw==", + "requires": { + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.4.tgz", + "integrity": "sha512-pySBTeoUff56fL5CBU2hWm9TesA4r/rOkI9DyJLvvgz09MB9YtfIYe3iBriVaYNaPe+Alua0vBIOVOLs2buWhg==", + "requires": { + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==" + }, + "@babel/helpers": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.10.4.tgz", + "integrity": "sha512-L2gX/XeUONeEbI78dXSrJzGdz4GQ+ZTA/aazfUsFaWjSe95kiCuOZ5HsXvkiw3iwF+mFHSRUfJU8t6YavocdXA==", + "requires": { + "@babel/template": "^7.10.4", + "@babel/traverse": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/highlight": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@babel/parser": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.4.tgz", + "integrity": "sha512-8jHII4hf+YVDsskTF6WuMB3X4Eh+PsUkC2ljq22so5rHvH+T8BzyL94VOdyFLNR8tBSVXOTbNHOKpR4TfRxVtA==" + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-class-properties": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.10.4.tgz", + "integrity": "sha512-GCSBF7iUle6rNugfURwNmCGG3Z/2+opxAMLs1nND4bhEG5PuxTIggDBoeYYSujAlLtsupzOHYJQgPS3pivwXIA==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/runtime-corejs3": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.10.4.tgz", + "integrity": "sha512-BFlgP2SoLO9HJX9WBwN67gHWMBhDX/eDz64Jajd6mR/UAUzqrNMm99d4qHnVaKscAElZoFiPv+JpR/Siud5lXw==", + "requires": { + "core-js-pure": "^3.0.0", + "regenerator-runtime": "^0.13.4" + } + }, + "@babel/template": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/traverse": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.10.4.tgz", + "integrity": "sha512-aSy7p5THgSYm4YyxNGz6jZpXf+Ok40QF3aA2LyIONkDHpAcJzDUqlCKXv6peqYUs2gmic849C/t2HKw2a2K20Q==", + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/generator": "^7.10.4", + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-split-export-declaration": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.13" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "@babel/types": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.4.tgz", + "integrity": "sha512-UTCFOxC3FsFHb7lkRMVvgLzaRVamXuAs2Tz4wajva4WxtVY82eZeaUBtC2Zt95FU9TiznuC0Zk35tsim8jeVpg==", + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + }, + "@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==" + }, + "@cnakazawa/watch": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@cnakazawa/watch/-/watch-1.0.4.tgz", + "integrity": "sha512-v9kIhKwjeZThiWrLmj0y17CWoyddASLj9O2yvbZkbvw/N3rWOYy9zkV66ursAoVr0mV15bL8g0c4QZUE6cdDoQ==", + "requires": { + "exec-sh": "^0.3.2", + "minimist": "^1.2.0" + } + }, + "@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "requires": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + } + }, + "@istanbuljs/schema": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz", + "integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==" + }, + "@jest/console": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-25.5.0.tgz", + "integrity": "sha512-T48kZa6MK1Y6k4b89sexwmSF4YLeZS/Udqg3Jj3jG/cHH+N/sLFCEoXEDMOKugJQ9FxPN1osxIknvKkxt6MKyw==", + "requires": { + "@jest/types": "^25.5.0", + "chalk": "^3.0.0", + "jest-message-util": "^25.5.0", + "jest-util": "^25.5.0", + "slash": "^3.0.0" + } + }, + "@jest/core": { + "version": "25.5.4", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-25.5.4.tgz", + "integrity": "sha512-3uSo7laYxF00Dg/DMgbn4xMJKmDdWvZnf89n8Xj/5/AeQ2dOQmn6b6Hkj/MleyzZWXpwv+WSdYWl4cLsy2JsoA==", + "requires": { + "@jest/console": "^25.5.0", + "@jest/reporters": "^25.5.1", + "@jest/test-result": "^25.5.0", + "@jest/transform": "^25.5.1", + "@jest/types": "^25.5.0", + "ansi-escapes": "^4.2.1", + "chalk": "^3.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.4", + "jest-changed-files": "^25.5.0", + "jest-config": "^25.5.4", + "jest-haste-map": "^25.5.1", + "jest-message-util": "^25.5.0", + "jest-regex-util": "^25.2.6", + "jest-resolve": "^25.5.1", + "jest-resolve-dependencies": "^25.5.4", + "jest-runner": "^25.5.4", + "jest-runtime": "^25.5.4", + "jest-snapshot": "^25.5.1", + "jest-util": "^25.5.0", + "jest-validate": "^25.5.0", + "jest-watcher": "^25.5.0", + "micromatch": "^4.0.2", + "p-each-series": "^2.1.0", + "realpath-native": "^2.0.0", + "rimraf": "^3.0.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "@jest/environment": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-25.5.0.tgz", + "integrity": "sha512-U2VXPEqL07E/V7pSZMSQCvV5Ea4lqOlT+0ZFijl/i316cRMHvZ4qC+jBdryd+lmRetjQo0YIQr6cVPNxxK87mA==", + "requires": { + "@jest/fake-timers": "^25.5.0", + "@jest/types": "^25.5.0", + "jest-mock": "^25.5.0" + } + }, + "@jest/fake-timers": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-25.5.0.tgz", + "integrity": "sha512-9y2+uGnESw/oyOI3eww9yaxdZyHq7XvprfP/eeoCsjqKYts2yRlsHS/SgjPDV8FyMfn2nbMy8YzUk6nyvdLOpQ==", + "requires": { + "@jest/types": "^25.5.0", + "jest-message-util": "^25.5.0", + "jest-mock": "^25.5.0", + "jest-util": "^25.5.0", + "lolex": "^5.0.0" + } + }, + "@jest/globals": { + "version": "25.5.2", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-25.5.2.tgz", + "integrity": "sha512-AgAS/Ny7Q2RCIj5kZ+0MuKM1wbF0WMLxbCVl/GOMoCNbODRdJ541IxJ98xnZdVSZXivKpJlNPIWa3QmY0l4CXA==", + "requires": { + "@jest/environment": "^25.5.0", + "@jest/types": "^25.5.0", + "expect": "^25.5.0" + } + }, + "@jest/reporters": { + "version": "25.5.1", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-25.5.1.tgz", + "integrity": "sha512-3jbd8pPDTuhYJ7vqiHXbSwTJQNavczPs+f1kRprRDxETeE3u6srJ+f0NPuwvOmk+lmunZzPkYWIFZDLHQPkviw==", + "requires": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^25.5.0", + "@jest/test-result": "^25.5.0", + "@jest/transform": "^25.5.1", + "@jest/types": "^25.5.0", + "chalk": "^3.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.2", + "graceful-fs": "^4.2.4", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^4.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.0.2", + "jest-haste-map": "^25.5.1", + "jest-resolve": "^25.5.1", + "jest-util": "^25.5.0", + "jest-worker": "^25.5.0", + "node-notifier": "^6.0.0", + "slash": "^3.0.0", + "source-map": "^0.6.0", + "string-length": "^3.1.0", + "terminal-link": "^2.0.0", + "v8-to-istanbul": "^4.1.3" + } + }, + "@jest/source-map": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-25.5.0.tgz", + "integrity": "sha512-eIGx0xN12yVpMcPaVpjXPnn3N30QGJCJQSkEDUt9x1fI1Gdvb07Ml6K5iN2hG7NmMP6FDmtPEssE3z6doOYUwQ==", + "requires": { + "callsites": "^3.0.0", + "graceful-fs": "^4.2.4", + "source-map": "^0.6.0" + } + }, + "@jest/test-result": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-25.5.0.tgz", + "integrity": "sha512-oV+hPJgXN7IQf/fHWkcS99y0smKLU2czLBJ9WA0jHITLst58HpQMtzSYxzaBvYc6U5U6jfoMthqsUlUlbRXs0A==", + "requires": { + "@jest/console": "^25.5.0", + "@jest/types": "^25.5.0", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + } + }, + "@jest/test-sequencer": { + "version": "25.5.4", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-25.5.4.tgz", + "integrity": "sha512-pTJGEkSeg1EkCO2YWq6hbFvKNXk8ejqlxiOg1jBNLnWrgXOkdY6UmqZpwGFXNnRt9B8nO1uWMzLLZ4eCmhkPNA==", + "requires": { + "@jest/test-result": "^25.5.0", + "graceful-fs": "^4.2.4", + "jest-haste-map": "^25.5.1", + "jest-runner": "^25.5.4", + "jest-runtime": "^25.5.4" + } + }, + "@jest/transform": { + "version": "25.5.1", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-25.5.1.tgz", + "integrity": "sha512-Y8CEoVwXb4QwA6Y/9uDkn0Xfz0finGkieuV0xkdF9UtZGJeLukD5nLkaVrVsODB1ojRWlaoD0AJZpVHCSnJEvg==", + "requires": { + "@babel/core": "^7.1.0", + "@jest/types": "^25.5.0", + "babel-plugin-istanbul": "^6.0.0", + "chalk": "^3.0.0", + "convert-source-map": "^1.4.0", + "fast-json-stable-stringify": "^2.0.0", + "graceful-fs": "^4.2.4", + "jest-haste-map": "^25.5.1", + "jest-regex-util": "^25.2.6", + "jest-util": "^25.5.0", + "micromatch": "^4.0.2", + "pirates": "^4.0.1", + "realpath-native": "^2.0.0", + "slash": "^3.0.0", + "source-map": "^0.6.1", + "write-file-atomic": "^3.0.0" + } + }, + "@jest/types": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz", + "integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==", + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^15.0.0", + "chalk": "^3.0.0" + } + }, + "@sinonjs/commons": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.0.tgz", + "integrity": "sha512-wEj54PfsZ5jGSwMX68G8ZXFawcSglQSXqCftWX3ec8MDUzQdHgcKvw97awHbY0efQEL5iKUOAmmVtoYgmrSG4Q==", + "requires": { + "type-detect": "4.0.8" + } + }, + "@types/babel__core": { + "version": "7.1.9", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.9.tgz", + "integrity": "sha512-sY2RsIJ5rpER1u3/aQ8OFSI7qGIy8o1NEEbgb2UaJcvOtXOMpd39ko723NBpjQFg9SIX7TXtjejZVGeIMLhoOw==", + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "@types/babel__generator": { + "version": "7.6.1", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.1.tgz", + "integrity": "sha512-bBKm+2VPJcMRVwNhxKu8W+5/zT7pwNEqeokFOmbvVSqGzFneNxYcEBro9Ac7/N9tlsaPYnZLK8J1LWKkMsLAew==", + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@types/babel__template": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.0.2.tgz", + "integrity": "sha512-/K6zCpeW7Imzgab2bLkLEbz0+1JlFSrUMdw7KoIIu+IUdu51GWaBZpd3y1VXGVXzynvGa4DaIaxNZHiON3GXUg==", + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@types/babel__traverse": { + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.0.12.tgz", + "integrity": "sha512-t4CoEokHTfcyfb4hUaF9oOHu9RmmNWnm1CP0YmMqOOfClKascOmvlEM736vlqeScuGvBDsHkf8R2INd4DWreQA==", + "requires": { + "@babel/types": "^7.3.0" + } + }, + "@types/color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" + }, + "@types/create-hmac": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@types/create-hmac/-/create-hmac-1.1.0.tgz", + "integrity": "sha512-BNYNdzdhOZZQWCOpwvIll3FSvgo3e55Y2M6s/jOY6TuOCwqt3cLmQsK4tSmJ5fayDot8EG4k3+hcZagfww9JlQ==", + "requires": { + "@types/node": "*" + } + }, + "@types/faker": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/faker/-/faker-4.1.12.tgz", + "integrity": "sha512-0MEyzJrLLs1WaOCx9ULK6FzdCSj2EuxdSP9kvuxxdBEGujZYUOZ4vkPXdgu3dhyg/pOdn7VCatelYX7k0YShlA==" + }, + "@types/graceful-fs": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.3.tgz", + "integrity": "sha512-AiHRaEB50LQg0pZmm659vNBb9f4SJ0qrAnteuzhSeAUcJKxoYgEnprg/83kppCnc2zvtCKbdZry1a5pVY3lOTQ==", + "requires": { + "@types/node": "*" + } + }, + "@types/istanbul-lib-coverage": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", + "integrity": "sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw==" + }, + "@types/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "requires": { + "@types/istanbul-lib-coverage": "*" + } + }, + "@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "requires": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "@types/jest": { + "version": "25.2.1", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-25.2.1.tgz", + "integrity": "sha512-msra1bCaAeEdkSyA0CZ6gW1ukMIvZ5YoJkdXw/qhQdsuuDlFTcEUrUw8CLCPt2rVRUfXlClVvK2gvPs9IokZaA==", + "requires": { + "jest-diff": "^25.2.1", + "pretty-format": "^25.2.1" + } + }, + "@types/moxios": { + "version": "0.4.9", + "resolved": "https://registry.npmjs.org/@types/moxios/-/moxios-0.4.9.tgz", + "integrity": "sha512-Sd1b24QRW2N194j2LEDPQAZK1h0TBtpN+2EIH+rERCgm38qm14JZwC7NlpE7n3jULhlCIPZBG8uNcbjF8KcCaQ==", + "requires": { + "axios": "^0.19.0" + } + }, + "@types/node": { + "version": "13.13.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.5.tgz", + "integrity": "sha512-3ySmiBYJPqgjiHA7oEaIo2Rzz0HrOZ7yrNO5HWyaE5q0lQ3BppDZ3N53Miz8bw2I7gh1/zir2MGVZBvpb1zq9g==" + }, + "@types/normalize-package-data": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==" + }, + "@types/prettier": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-1.19.1.tgz", + "integrity": "sha512-5qOlnZscTn4xxM5MeGXAMOsIOIKIbh9e85zJWfBRVPlRMEVawzoPhINYbRGkBZCI8LxvBe7tJCdWiarA99OZfQ==" + }, + "@types/stack-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", + "integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==" + }, + "@types/yargs": { + "version": "15.0.5", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.5.tgz", + "integrity": "sha512-Dk/IDOPtOgubt/IaevIUbTgV7doaKkoorvOyYM2CMwuDyP89bekI7H4xLIwunNYiK9jhCkmc6pUrJk3cj2AB9w==", + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-15.0.0.tgz", + "integrity": "sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw==" + }, + "abab": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.3.tgz", + "integrity": "sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg==" + }, + "acorn": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.3.1.tgz", + "integrity": "sha512-tLc0wSnatxAQHVHUapaHdz72pi9KUyHjq5KyHjGg9Y8Ifdc79pTh2XvI6I1/chZbnM7QtNKzh66ooDogPZSleA==" + }, + "acorn-globals": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.4.tgz", + "integrity": "sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A==", + "requires": { + "acorn": "^6.0.1", + "acorn-walk": "^6.0.1" + }, + "dependencies": { + "acorn": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", + "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==" + } + } + }, + "acorn-walk": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz", + "integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==" + }, + "ajv": { + "version": "6.12.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz", + "integrity": "sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==", + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-escapes": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", + "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", + "requires": { + "type-fest": "^0.11.0" + }, + "dependencies": { + "type-fest": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", + "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==" + } + } + }, + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" + }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==" + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=" + }, + "array-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", + "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=" + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=" + }, + "astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "aws4": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.0.tgz", + "integrity": "sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA==" + }, + "axios": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", + "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==", + "requires": { + "follow-redirects": "1.5.10" + } + }, + "babel-jest": { + "version": "25.5.1", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-25.5.1.tgz", + "integrity": "sha512-9dA9+GmMjIzgPnYtkhBg73gOo/RHqPmLruP3BaGL4KEX3Dwz6pI8auSN8G8+iuEG90+GSswyKvslN+JYSaacaQ==", + "requires": { + "@jest/transform": "^25.5.1", + "@jest/types": "^25.5.0", + "@types/babel__core": "^7.1.7", + "babel-plugin-istanbul": "^6.0.0", + "babel-preset-jest": "^25.5.0", + "chalk": "^3.0.0", + "graceful-fs": "^4.2.4", + "slash": "^3.0.0" + } + }, + "babel-plugin-istanbul": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz", + "integrity": "sha512-AF55rZXpe7trmEylbaE1Gv54wn6rwU03aptvRoVIGP8YykoSxqdVLV1TfwflBCE/QtHmqtP8SWlTENqbK8GCSQ==", + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^4.0.0", + "test-exclude": "^6.0.0" + } + }, + "babel-plugin-jest-hoist": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-25.5.0.tgz", + "integrity": "sha512-u+/W+WAjMlvoocYGTwthAiQSxDcJAyHpQ6oWlHdFZaaN+Rlk8Q7iiwDPg2lN/FyJtAYnKjFxbn7xus4HCFkg5g==", + "requires": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__traverse": "^7.0.6" + } + }, + "babel-preset-current-node-syntax": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-0.1.3.tgz", + "integrity": "sha512-uyexu1sVwcdFnyq9o8UQYsXwXflIh8LvrF5+cKrYam93ned1CStffB3+BEcsxGSgagoA3GEyjDqO4a/58hyPYQ==", + "requires": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + } + }, + "babel-preset-jest": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-25.5.0.tgz", + "integrity": "sha512-8ZczygctQkBU+63DtSOKGh7tFL0CeCuz+1ieud9lJ1WPQ9O6A1a/r+LGn6Y705PA6whHQ3T1XuB/PmpfNYf8Fw==", + "requires": { + "babel-plugin-jest-hoist": "^25.5.0", + "babel-preset-current-node-syntax": "^0.1.2" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "requires": { + "fill-range": "^7.0.1" + } + }, + "browser-process-hrtime": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", + "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==" + }, + "browser-resolve": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.3.tgz", + "integrity": "sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==", + "requires": { + "resolve": "1.1.7" + }, + "dependencies": { + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=" + } + } + }, + "bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "requires": { + "fast-json-stable-stringify": "2.x" + } + }, + "bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "requires": { + "node-int64": "^0.4.0" + } + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + }, + "capture-exit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz", + "integrity": "sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g==", + "requires": { + "rsvp": "^4.8.4" + } + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==" + }, + "cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + }, + "collect-v8-coverage": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", + "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==" + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "convert-source-map": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", + "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "requires": { + "safe-buffer": "~5.1.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } + } + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" + }, + "core-js-pure": { + "version": "3.6.5", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.6.5.tgz", + "integrity": "sha512-lacdXOimsiD0QyNf9BC/mxivNJ/ybBGJXQFKzRekp1WTHoVUWsUHEn+2T8GJAzzIhyOuXA+gOxCVN3l+5PLPUA==" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "dev": true, + "requires": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "dev": true, + "requires": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "cssom": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", + "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==" + }, + "cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "requires": { + "cssom": "~0.3.6" + }, + "dependencies": { + "cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==" + } + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "data-urls": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.1.0.tgz", + "integrity": "sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==", + "requires": { + "abab": "^2.0.0", + "whatwg-mimetype": "^2.2.0", + "whatwg-url": "^7.0.0" + } + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "decamelize": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-3.2.0.tgz", + "integrity": "sha512-4TgkVUsmmu7oCSyGBm5FvfMoACuoh9EOidm7V5/J2X2djAwwt57qb3F2KMP2ITqODTCSwb+YRV+0Zqrv18k/hw==", + "requires": { + "xregexp": "^4.2.4" + } + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" + }, + "deepmerge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==" + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==" + }, + "diff-sequences": { + "version": "25.2.6", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-25.2.6.tgz", + "integrity": "sha512-Hq8o7+6GaZeoFjtpgvRBUknSXNeJiCx7V9Fr94ZMljNiCr9n9L8H8aJqgWOQiDDGdyn29fRNcDdRVJ5fdyihfg==" + }, + "domexception": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", + "integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==", + "requires": { + "webidl-conversions": "^4.0.2" + } + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "requires": { + "once": "^1.4.0" + } + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "requires": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" + }, + "exec-sh": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.4.tgz", + "integrity": "sha512-sEFIkc61v75sWeOe72qyrqg2Qg0OuLESziUDk/O/z2qgS15y2gWVFrI6f2Qn/qw/0/NCfCEsmNA4zOjkwEZT1A==" + }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=" + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "expect": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-25.5.0.tgz", + "integrity": "sha512-w7KAXo0+6qqZZhovCaBVPSIqQp7/UTcx4M9uKt2m6pd2VB1voyC8JizLRqeEqud3AAVP02g+hbErDu5gu64tlA==", + "requires": { + "@jest/types": "^25.5.0", + "ansi-styles": "^4.0.0", + "jest-get-type": "^25.2.6", + "jest-matcher-utils": "^25.5.0", + "jest-message-util": "^25.5.0", + "jest-regex-util": "^25.2.6" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "faker": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/faker/-/faker-4.1.0.tgz", + "integrity": "sha1-HkW7vsxndLPBlfrSg1EJxtdIzD8=", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" + }, + "fb-watchman": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.1.tgz", + "integrity": "sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg==", + "requires": { + "bser": "2.1.1" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "fishery": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fishery/-/fishery-1.0.0.tgz", + "integrity": "sha512-DLQtxcSPlLQYY6J0tL/dl7DfPhrULHCAO6fFDGnrXqA830J6AW124fHarYOLnfvcSXNBEooBS/g65N/HecQYjQ==", + "dev": true, + "requires": { + "lodash.merge": "^4.6.2" + } + }, + "follow-redirects": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", + "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", + "requires": { + "debug": "=3.1.0" + } + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "requires": { + "map-cache": "^0.2.2" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "fsevents": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", + "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", + "optional": true + }, + "gensync": { + "version": "1.0.0-beta.1", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.1.tgz", + "integrity": "sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==" + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, + "get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==" + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "requires": { + "pump": "^3.0.0" + } + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=" + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" + }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" + }, + "growly": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", + "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", + "optional": true + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "requires": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "hash-base": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", + "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", + "dev": true, + "requires": { + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + } + }, + "hosted-git-info": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", + "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==" + }, + "html-encoding-sniffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz", + "integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==", + "requires": { + "whatwg-encoding": "^1.0.1" + } + }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==" + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "human-signals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==" + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "import-local": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.2.tgz", + "integrity": "sha512-vjL3+w0oulAVZ0hBHnxa/Nm5TAurf9YLQJDhqRZyqb+VKGOB6LU8t9H1Nr5CIo16vh9XfJTOoHwU0B71S557gA==", + "requires": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ip-regex": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", + "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=" + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, + "is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "requires": { + "ci-info": "^2.0.0" + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + } + } + }, + "is-docker": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.0.0.tgz", + "integrity": "sha512-pJEdRugimx4fBMra5z2/5iRdZ63OhYV0vr0Dwm5+xtW4D1FvRkB8hamMIhnWfyJeDdyr/aa7BDyNbtG38VxgoQ==", + "optional": true + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==" + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "requires": { + "isobject": "^3.0.1" + } + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==" + }, + "is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "optional": true, + "requires": { + "is-docker": "^2.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "istanbul-lib-coverage": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", + "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==" + }, + "istanbul-lib-instrument": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", + "requires": { + "@babel/core": "^7.7.5", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" + } + }, + "istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + } + }, + "istanbul-lib-source-maps": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz", + "integrity": "sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg==", + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "istanbul-reports": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.2.tgz", + "integrity": "sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==", + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "jest": { + "version": "25.5.4", + "resolved": "https://registry.npmjs.org/jest/-/jest-25.5.4.tgz", + "integrity": "sha512-hHFJROBTqZahnO+X+PMtT6G2/ztqAZJveGqz//FnWWHurizkD05PQGzRZOhF3XP6z7SJmL+5tCfW8qV06JypwQ==", + "requires": { + "@jest/core": "^25.5.4", + "import-local": "^3.0.2", + "jest-cli": "^25.5.4" + }, + "dependencies": { + "jest-cli": { + "version": "25.5.4", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-25.5.4.tgz", + "integrity": "sha512-rG8uJkIiOUpnREh1768/N3n27Cm+xPFkSNFO91tgg+8o2rXeVLStz+vkXkGr4UtzH6t1SNbjwoiswd7p4AhHTw==", + "requires": { + "@jest/core": "^25.5.4", + "@jest/test-result": "^25.5.0", + "@jest/types": "^25.5.0", + "chalk": "^3.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.4", + "import-local": "^3.0.2", + "is-ci": "^2.0.0", + "jest-config": "^25.5.4", + "jest-util": "^25.5.0", + "jest-validate": "^25.5.0", + "prompts": "^2.0.1", + "realpath-native": "^2.0.0", + "yargs": "^15.3.1" + } + } + } + }, + "jest-changed-files": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-25.5.0.tgz", + "integrity": "sha512-EOw9QEqapsDT7mKF162m8HFzRPbmP8qJQny6ldVOdOVBz3ACgPm/1nAn5fPQ/NDaYhX/AHkrGwwkCncpAVSXcw==", + "requires": { + "@jest/types": "^25.5.0", + "execa": "^3.2.0", + "throat": "^5.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "execa": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-3.4.0.tgz", + "integrity": "sha512-r9vdGQk4bmCuK1yKQu1KTwcT2zwfWdbdaXfCtAh+5nU/4fSX+JAb7vZGvI5naJrQlvONrEB20jeruESI69530g==", + "requires": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "p-finally": "^2.0.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + } + }, + "get-stream": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", + "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==", + "requires": { + "pump": "^3.0.0" + } + }, + "is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==" + }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "requires": { + "path-key": "^3.0.0" + } + }, + "p-finally": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-2.0.1.tgz", + "integrity": "sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw==" + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" + } + } + }, + "jest-config": { + "version": "25.5.4", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-25.5.4.tgz", + "integrity": "sha512-SZwR91SwcdK6bz7Gco8qL7YY2sx8tFJYzvg216DLihTWf+LKY/DoJXpM9nTzYakSyfblbqeU48p/p7Jzy05Atg==", + "requires": { + "@babel/core": "^7.1.0", + "@jest/test-sequencer": "^25.5.4", + "@jest/types": "^25.5.0", + "babel-jest": "^25.5.1", + "chalk": "^3.0.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.1", + "graceful-fs": "^4.2.4", + "jest-environment-jsdom": "^25.5.0", + "jest-environment-node": "^25.5.0", + "jest-get-type": "^25.2.6", + "jest-jasmine2": "^25.5.4", + "jest-regex-util": "^25.2.6", + "jest-resolve": "^25.5.1", + "jest-util": "^25.5.0", + "jest-validate": "^25.5.0", + "micromatch": "^4.0.2", + "pretty-format": "^25.5.0", + "realpath-native": "^2.0.0" + } + }, + "jest-diff": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-25.5.0.tgz", + "integrity": "sha512-z1kygetuPiREYdNIumRpAHY6RXiGmp70YHptjdaxTWGmA085W3iCnXNx0DhflK3vwrKmrRWyY1wUpkPMVxMK7A==", + "requires": { + "chalk": "^3.0.0", + "diff-sequences": "^25.2.6", + "jest-get-type": "^25.2.6", + "pretty-format": "^25.5.0" + } + }, + "jest-docblock": { + "version": "25.3.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-25.3.0.tgz", + "integrity": "sha512-aktF0kCar8+zxRHxQZwxMy70stc9R1mOmrLsT5VO3pIT0uzGRSDAXxSlz4NqQWpuLjPpuMhPRl7H+5FRsvIQAg==", + "requires": { + "detect-newline": "^3.0.0" + } + }, + "jest-each": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-25.5.0.tgz", + "integrity": "sha512-QBogUxna3D8vtiItvn54xXde7+vuzqRrEeaw8r1s+1TG9eZLVJE5ZkKoSUlqFwRjnlaA4hyKGiu9OlkFIuKnjA==", + "requires": { + "@jest/types": "^25.5.0", + "chalk": "^3.0.0", + "jest-get-type": "^25.2.6", + "jest-util": "^25.5.0", + "pretty-format": "^25.5.0" + } + }, + "jest-environment-jsdom": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-25.5.0.tgz", + "integrity": "sha512-7Jr02ydaq4jaWMZLY+Skn8wL5nVIYpWvmeatOHL3tOcV3Zw8sjnPpx+ZdeBfc457p8jCR9J6YCc+Lga0oIy62A==", + "requires": { + "@jest/environment": "^25.5.0", + "@jest/fake-timers": "^25.5.0", + "@jest/types": "^25.5.0", + "jest-mock": "^25.5.0", + "jest-util": "^25.5.0", + "jsdom": "^15.2.1" + } + }, + "jest-environment-node": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-25.5.0.tgz", + "integrity": "sha512-iuxK6rQR2En9EID+2k+IBs5fCFd919gVVK5BeND82fYeLWPqvRcFNPKu9+gxTwfB5XwBGBvZ0HFQa+cHtIoslA==", + "requires": { + "@jest/environment": "^25.5.0", + "@jest/fake-timers": "^25.5.0", + "@jest/types": "^25.5.0", + "jest-mock": "^25.5.0", + "jest-util": "^25.5.0", + "semver": "^6.3.0" + } + }, + "jest-get-type": { + "version": "25.2.6", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-25.2.6.tgz", + "integrity": "sha512-DxjtyzOHjObRM+sM1knti6or+eOgcGU4xVSb2HNP1TqO4ahsT+rqZg+nyqHWJSvWgKC5cG3QjGFBqxLghiF/Ig==" + }, + "jest-haste-map": { + "version": "25.5.1", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-25.5.1.tgz", + "integrity": "sha512-dddgh9UZjV7SCDQUrQ+5t9yy8iEgKc1AKqZR9YDww8xsVOtzPQSMVLDChc21+g29oTRexb9/B0bIlZL+sWmvAQ==", + "requires": { + "@jest/types": "^25.5.0", + "@types/graceful-fs": "^4.1.2", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "fsevents": "^2.1.2", + "graceful-fs": "^4.2.4", + "jest-serializer": "^25.5.0", + "jest-util": "^25.5.0", + "jest-worker": "^25.5.0", + "micromatch": "^4.0.2", + "sane": "^4.0.3", + "walker": "^1.0.7", + "which": "^2.0.2" + } + }, + "jest-jasmine2": { + "version": "25.5.4", + "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-25.5.4.tgz", + "integrity": "sha512-9acbWEfbmS8UpdcfqnDO+uBUgKa/9hcRh983IHdM+pKmJPL77G0sWAAK0V0kr5LK3a8cSBfkFSoncXwQlRZfkQ==", + "requires": { + "@babel/traverse": "^7.1.0", + "@jest/environment": "^25.5.0", + "@jest/source-map": "^25.5.0", + "@jest/test-result": "^25.5.0", + "@jest/types": "^25.5.0", + "chalk": "^3.0.0", + "co": "^4.6.0", + "expect": "^25.5.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^25.5.0", + "jest-matcher-utils": "^25.5.0", + "jest-message-util": "^25.5.0", + "jest-runtime": "^25.5.4", + "jest-snapshot": "^25.5.1", + "jest-util": "^25.5.0", + "pretty-format": "^25.5.0", + "throat": "^5.0.0" + } + }, + "jest-leak-detector": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-25.5.0.tgz", + "integrity": "sha512-rV7JdLsanS8OkdDpZtgBf61L5xZ4NnYLBq72r6ldxahJWWczZjXawRsoHyXzibM5ed7C2QRjpp6ypgwGdKyoVA==", + "requires": { + "jest-get-type": "^25.2.6", + "pretty-format": "^25.5.0" + } + }, + "jest-matcher-utils": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-25.5.0.tgz", + "integrity": "sha512-VWI269+9JS5cpndnpCwm7dy7JtGQT30UHfrnM3mXl22gHGt/b7NkjBqXfbhZ8V4B7ANUsjK18PlSBmG0YH7gjw==", + "requires": { + "chalk": "^3.0.0", + "jest-diff": "^25.5.0", + "jest-get-type": "^25.2.6", + "pretty-format": "^25.5.0" + } + }, + "jest-message-util": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-25.5.0.tgz", + "integrity": "sha512-ezddz3YCT/LT0SKAmylVyWWIGYoKHOFOFXx3/nA4m794lfVUskMcwhip6vTgdVrOtYdjeQeis2ypzes9mZb4EA==", + "requires": { + "@babel/code-frame": "^7.0.0", + "@jest/types": "^25.5.0", + "@types/stack-utils": "^1.0.1", + "chalk": "^3.0.0", + "graceful-fs": "^4.2.4", + "micromatch": "^4.0.2", + "slash": "^3.0.0", + "stack-utils": "^1.0.1" + } + }, + "jest-mock": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-25.5.0.tgz", + "integrity": "sha512-eXWuTV8mKzp/ovHc5+3USJMYsTBhyQ+5A1Mak35dey/RG8GlM4YWVylZuGgVXinaW6tpvk/RSecmF37FKUlpXA==", + "requires": { + "@jest/types": "^25.5.0" + } + }, + "jest-pnp-resolver": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz", + "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==" + }, + "jest-regex-util": { + "version": "25.2.6", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-25.2.6.tgz", + "integrity": "sha512-KQqf7a0NrtCkYmZZzodPftn7fL1cq3GQAFVMn5Hg8uKx/fIenLEobNanUxb7abQ1sjADHBseG/2FGpsv/wr+Qw==" + }, + "jest-resolve": { + "version": "25.5.1", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-25.5.1.tgz", + "integrity": "sha512-Hc09hYch5aWdtejsUZhA+vSzcotf7fajSlPA6EZPE1RmPBAD39XtJhvHWFStid58iit4IPDLI/Da4cwdDmAHiQ==", + "requires": { + "@jest/types": "^25.5.0", + "browser-resolve": "^1.11.3", + "chalk": "^3.0.0", + "graceful-fs": "^4.2.4", + "jest-pnp-resolver": "^1.2.1", + "read-pkg-up": "^7.0.1", + "realpath-native": "^2.0.0", + "resolve": "^1.17.0", + "slash": "^3.0.0" + } + }, + "jest-resolve-dependencies": { + "version": "25.5.4", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-25.5.4.tgz", + "integrity": "sha512-yFmbPd+DAQjJQg88HveObcGBA32nqNZ02fjYmtL16t1xw9bAttSn5UGRRhzMHIQbsep7znWvAvnD4kDqOFM0Uw==", + "requires": { + "@jest/types": "^25.5.0", + "jest-regex-util": "^25.2.6", + "jest-snapshot": "^25.5.1" + } + }, + "jest-runner": { + "version": "25.5.4", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-25.5.4.tgz", + "integrity": "sha512-V/2R7fKZo6blP8E9BL9vJ8aTU4TH2beuqGNxHbxi6t14XzTb+x90B3FRgdvuHm41GY8ch4xxvf0ATH4hdpjTqg==", + "requires": { + "@jest/console": "^25.5.0", + "@jest/environment": "^25.5.0", + "@jest/test-result": "^25.5.0", + "@jest/types": "^25.5.0", + "chalk": "^3.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.4", + "jest-config": "^25.5.4", + "jest-docblock": "^25.3.0", + "jest-haste-map": "^25.5.1", + "jest-jasmine2": "^25.5.4", + "jest-leak-detector": "^25.5.0", + "jest-message-util": "^25.5.0", + "jest-resolve": "^25.5.1", + "jest-runtime": "^25.5.4", + "jest-util": "^25.5.0", + "jest-worker": "^25.5.0", + "source-map-support": "^0.5.6", + "throat": "^5.0.0" + } + }, + "jest-runtime": { + "version": "25.5.4", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-25.5.4.tgz", + "integrity": "sha512-RWTt8LeWh3GvjYtASH2eezkc8AehVoWKK20udV6n3/gC87wlTbE1kIA+opCvNWyyPeBs6ptYsc6nyHUb1GlUVQ==", + "requires": { + "@jest/console": "^25.5.0", + "@jest/environment": "^25.5.0", + "@jest/globals": "^25.5.2", + "@jest/source-map": "^25.5.0", + "@jest/test-result": "^25.5.0", + "@jest/transform": "^25.5.1", + "@jest/types": "^25.5.0", + "@types/yargs": "^15.0.0", + "chalk": "^3.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.4", + "jest-config": "^25.5.4", + "jest-haste-map": "^25.5.1", + "jest-message-util": "^25.5.0", + "jest-mock": "^25.5.0", + "jest-regex-util": "^25.2.6", + "jest-resolve": "^25.5.1", + "jest-snapshot": "^25.5.1", + "jest-util": "^25.5.0", + "jest-validate": "^25.5.0", + "realpath-native": "^2.0.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0", + "yargs": "^15.3.1" + } + }, + "jest-serializer": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-25.5.0.tgz", + "integrity": "sha512-LxD8fY1lByomEPflwur9o4e2a5twSQ7TaVNLlFUuToIdoJuBt8tzHfCsZ42Ok6LkKXWzFWf3AGmheuLAA7LcCA==", + "requires": { + "graceful-fs": "^4.2.4" + } + }, + "jest-snapshot": { + "version": "25.5.1", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-25.5.1.tgz", + "integrity": "sha512-C02JE1TUe64p2v1auUJ2ze5vcuv32tkv9PyhEb318e8XOKF7MOyXdJ7kdjbvrp3ChPLU2usI7Rjxs97Dj5P0uQ==", + "requires": { + "@babel/types": "^7.0.0", + "@jest/types": "^25.5.0", + "@types/prettier": "^1.19.0", + "chalk": "^3.0.0", + "expect": "^25.5.0", + "graceful-fs": "^4.2.4", + "jest-diff": "^25.5.0", + "jest-get-type": "^25.2.6", + "jest-matcher-utils": "^25.5.0", + "jest-message-util": "^25.5.0", + "jest-resolve": "^25.5.1", + "make-dir": "^3.0.0", + "natural-compare": "^1.4.0", + "pretty-format": "^25.5.0", + "semver": "^6.3.0" + } + }, + "jest-util": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-25.5.0.tgz", + "integrity": "sha512-KVlX+WWg1zUTB9ktvhsg2PXZVdkI1NBevOJSkTKYAyXyH4QSvh+Lay/e/v+bmaFfrkfx43xD8QTfgobzlEXdIA==", + "requires": { + "@jest/types": "^25.5.0", + "chalk": "^3.0.0", + "graceful-fs": "^4.2.4", + "is-ci": "^2.0.0", + "make-dir": "^3.0.0" + } + }, + "jest-validate": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-25.5.0.tgz", + "integrity": "sha512-okUFKqhZIpo3jDdtUXUZ2LxGUZJIlfdYBvZb1aczzxrlyMlqdnnws9MOxezoLGhSaFc2XYaHNReNQfj5zPIWyQ==", + "requires": { + "@jest/types": "^25.5.0", + "camelcase": "^5.3.1", + "chalk": "^3.0.0", + "jest-get-type": "^25.2.6", + "leven": "^3.1.0", + "pretty-format": "^25.5.0" + } + }, + "jest-watcher": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-25.5.0.tgz", + "integrity": "sha512-XrSfJnVASEl+5+bb51V0Q7WQx65dTSk7NL4yDdVjPnRNpM0hG+ncFmDYJo9O8jaSRcAitVbuVawyXCRoxGrT5Q==", + "requires": { + "@jest/test-result": "^25.5.0", + "@jest/types": "^25.5.0", + "ansi-escapes": "^4.2.1", + "chalk": "^3.0.0", + "jest-util": "^25.5.0", + "string-length": "^3.1.0" + } + }, + "jest-worker": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-25.5.0.tgz", + "integrity": "sha512-/dsSmUkIy5EBGfv/IjjqmFxrNAUpBERfGs1oHROyD7yxjG/w+t0GOJDX8O1k32ySmd7+a5IhnJU2qQFcJ4n1vw==", + "requires": { + "merge-stream": "^2.0.0", + "supports-color": "^7.0.0" + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "js-yaml": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", + "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + }, + "jsdom": { + "version": "15.2.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-15.2.1.tgz", + "integrity": "sha512-fAl1W0/7T2G5vURSyxBzrJ1LSdQn6Tr5UX/xD4PXDx/PDgwygedfW6El/KIj3xJ7FU61TTYnc/l/B7P49Eqt6g==", + "requires": { + "abab": "^2.0.0", + "acorn": "^7.1.0", + "acorn-globals": "^4.3.2", + "array-equal": "^1.0.0", + "cssom": "^0.4.1", + "cssstyle": "^2.0.0", + "data-urls": "^1.1.0", + "domexception": "^1.0.1", + "escodegen": "^1.11.1", + "html-encoding-sniffer": "^1.0.2", + "nwsapi": "^2.2.0", + "parse5": "5.1.0", + "pn": "^1.1.0", + "request": "^2.88.0", + "request-promise-native": "^1.0.7", + "saxes": "^3.1.9", + "symbol-tree": "^3.2.2", + "tough-cookie": "^3.0.1", + "w3c-hr-time": "^1.0.1", + "w3c-xmlserializer": "^1.1.2", + "webidl-conversions": "^4.0.2", + "whatwg-encoding": "^1.0.5", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^7.0.0", + "ws": "^7.0.0", + "xml-name-validator": "^3.0.0" + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "json5": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", + "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", + "requires": { + "minimist": "^1.2.5" + } + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" + }, + "kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==" + }, + "leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==" + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "lines-and-columns": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", + "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=" + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "requires": { + "p-locate": "^4.1.0" + } + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + }, + "lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=" + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=" + }, + "lolex": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-5.1.2.tgz", + "integrity": "sha512-h4hmjAvHTmd+25JSwrtTIuwbKdwg5NzZVRMLn9saij4SZaepCrTCxPr35H/3bjwfMJtN+t3CX8672UIkglz28A==", + "requires": { + "@sinonjs/commons": "^1.7.0" + } + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "requires": { + "semver": "^6.0.0" + } + }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" + }, + "makeerror": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz", + "integrity": "sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw=", + "requires": { + "tmpl": "1.0.x" + } + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=" + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "requires": { + "object-visit": "^1.0.0" + } + }, + "md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "dev": true, + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "mime-db": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==" + }, + "mime-types": { + "version": "2.1.27", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", + "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", + "requires": { + "mime-db": "1.44.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + }, + "mixin-deep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "requires": { + "minimist": "^1.2.5" + } + }, + "moxios": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/moxios/-/moxios-0.4.0.tgz", + "integrity": "sha1-/A2ixlR31yXKa5Z51YNw7QxS9Ts=" + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + } + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=" + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" + }, + "node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=" + }, + "node-modules-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz", + "integrity": "sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=" + }, + "node-notifier": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-6.0.0.tgz", + "integrity": "sha512-SVfQ/wMw+DesunOm5cKqr6yDcvUTDl/yc97ybGHMrteNEY6oekXpNpS3lZwgLlwz0FLgHoiW28ZpmBHUDg37cw==", + "optional": true, + "requires": { + "growly": "^1.3.0", + "is-wsl": "^2.1.1", + "semver": "^6.3.0", + "shellwords": "^0.1.1", + "which": "^1.3.1" + }, + "dependencies": { + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "optional": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + } + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "requires": { + "path-key": "^2.0.0" + } + }, + "nwsapi": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", + "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==" + }, + "oauth-1.0a": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/oauth-1.0a/-/oauth-1.0a-2.2.6.tgz", + "integrity": "sha512-6bkxv3N4Gu5lty4viIcIAnq5GbxECviMBeKR3WX/q87SPQ8E8aursPZUtsXDnxCs787af09WPRBLqYrf/lwoYQ==", + "dev": true + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" + }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "requires": { + "isobject": "^3.0.0" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "requires": { + "isobject": "^3.0.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", + "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "p-each-series": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-2.1.0.tgz", + "integrity": "sha512-ZuRs1miPT4HrjFa+9fRfOFXxGJfORgelKV9f9nNOWw2gl6gVsRaVDOQP0+MI0G0wGKns1Yacsu0GjOFbTK0JFQ==" + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" + }, + "parse-json": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.0.0.tgz", + "integrity": "sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw==", + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1", + "lines-and-columns": "^1.1.6" + } + }, + "parse5": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.0.tgz", + "integrity": "sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==" + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=" + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "picomatch": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==" + }, + "pirates": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.1.tgz", + "integrity": "sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA==", + "requires": { + "node-modules-regexp": "^1.0.0" + } + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "requires": { + "find-up": "^4.0.0" + } + }, + "pn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", + "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==" + }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=" + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" + }, + "pretty-format": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.5.0.tgz", + "integrity": "sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ==", + "requires": { + "@jest/types": "^25.5.0", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^16.12.0" + } + }, + "prompts": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.3.2.tgz", + "integrity": "sha512-Q06uKs2CkNYVID0VqwfAl9mipo99zkBv/n2JtWY89Yxa3ZabWSrs0e2KTudKVa3peLUvYXMefDqIleLPVUBZMA==", + "requires": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.4" + } + }, + "psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "requires": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "dependencies": { + "type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==" + } + } + }, + "read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "requires": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "realpath-native": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/realpath-native/-/realpath-native-2.0.0.tgz", + "integrity": "sha512-v1SEYUOXXdbBZK8ZuNgO4TBjamPsiSgcFr0aP+tEKpQZK8vooEUqV6nm6Cv502mX4NF2EfsnVqtNAHG+/6Ur1Q==" + }, + "regenerator-runtime": { + "version": "0.13.5", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz", + "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==" + }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + } + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" + }, + "repeat-element": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", + "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==" + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" + }, + "request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "dependencies": { + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + } + } + }, + "request-promise-core": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.3.tgz", + "integrity": "sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ==", + "requires": { + "lodash": "^4.17.15" + } + }, + "request-promise-native": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.8.tgz", + "integrity": "sha512-dapwLGqkHtwL5AEbfenuzjTYg35Jd6KPytsC2/TLkVMz8rm+tNt72MGUWT1RP/aYawMpN6HqbNGBQaRcBtjQMQ==", + "requires": { + "request-promise-core": "1.1.3", + "stealthy-require": "^1.1.1", + "tough-cookie": "^2.3.3" + }, + "dependencies": { + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + } + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + }, + "resolve": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", + "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", + "requires": { + "path-parse": "^1.0.6" + } + }, + "resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "requires": { + "resolve-from": "^5.0.0" + } + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==" + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=" + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "requires": { + "glob": "^7.1.3" + } + }, + "ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "dev": true, + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "rsvp": { + "version": "4.8.5", + "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", + "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==" + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "requires": { + "ret": "~0.1.10" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "sane": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/sane/-/sane-4.1.0.tgz", + "integrity": "sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA==", + "requires": { + "@cnakazawa/watch": "^1.0.3", + "anymatch": "^2.0.0", + "capture-exit": "^2.0.0", + "exec-sh": "^0.3.2", + "execa": "^1.0.0", + "fb-watchman": "^2.0.0", + "micromatch": "^3.1.4", + "minimist": "^1.1.1", + "walker": "~1.0.5" + }, + "dependencies": { + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + } + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "requires": { + "remove-trailing-separator": "^1.0.1" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } + } + }, + "saxes": { + "version": "3.1.11", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-3.1.11.tgz", + "integrity": "sha512-Ydydq3zC+WYDJK1+gRxRapLIED9PWeSuuS41wqyoRmzvhhh9nc+QQrVMKJYzJFULazeGhzSV0QleN2wD3boh2g==", + "requires": { + "xmlchars": "^2.1.1" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, + "set-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" + }, + "shellwords": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", + "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", + "optional": true + }, + "signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" + }, + "sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==" + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==" + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "requires": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "requires": { + "kind-of": "^3.2.0" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "source-map-resolve": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", + "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", + "requires": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=" + }, + "spdx-correct": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", + "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==" + }, + "spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", + "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==" + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "requires": { + "extend-shallow": "^3.0.0" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + }, + "sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "stack-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.2.tgz", + "integrity": "sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA==" + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=" + }, + "string-length": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-3.1.0.tgz", + "integrity": "sha512-Ttp5YvkGm5v9Ijagtaz1BnN+k9ObpvS0eIBblPMp2YWL8FBmi9qblQ9fexc2k/CXFgrTIteU3jAw3payCnwSTA==", + "requires": { + "astral-regex": "^1.0.0", + "strip-ansi": "^5.2.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==" + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" + }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==" + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "requires": { + "has-flag": "^4.0.0" + } + }, + "supports-hyperlinks": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.1.0.tgz", + "integrity": "sha512-zoE5/e+dnEijk6ASB6/qrK+oYdm2do1hjoLWrqUC/8WEIW1gbxFcKuBof7sW8ArN6e+AYvsE8HBGiVRWL/F5CA==", + "requires": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + } + }, + "symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" + }, + "terminal-link": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", + "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", + "requires": { + "ansi-escapes": "^4.2.1", + "supports-hyperlinks": "^2.0.0" + } + }, + "test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + } + }, + "throat": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", + "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==" + }, + "tmpl": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", + "integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=" + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "requires": { + "is-number": "^7.0.0" + } + }, + "tough-cookie": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz", + "integrity": "sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==", + "requires": { + "ip-regex": "^2.1.0", + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, + "tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", + "requires": { + "punycode": "^2.1.0" + } + }, + "ts-jest": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-25.5.0.tgz", + "integrity": "sha512-govrjbOk1UEzcJ5cX5k8X8IUtFuP3lp3mrF3ZuKtCdAOQzdeCM7qualhb/U8s8SWFwEDutOqfF5PLkJ+oaYD4w==", + "requires": { + "bs-logger": "0.x", + "buffer-from": "1.x", + "fast-json-stable-stringify": "2.x", + "json5": "2.x", + "lodash.memoize": "4.x", + "make-error": "1.x", + "micromatch": "4.x", + "mkdirp": "0.x", + "semver": "6.x", + "yargs-parser": "18.x" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==" + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==" + }, + "typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "requires": { + "is-typedarray": "^1.0.0" + } + }, + "typescript": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz", + "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==" + }, + "union-value": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + } + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=" + } + } + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "requires": { + "punycode": "^2.1.0" + } + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=" + }, + "use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + }, + "v8-to-istanbul": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-4.1.4.tgz", + "integrity": "sha512-Rw6vJHj1mbdK8edjR7+zuJrpDtKIgNdAvTSAcpYfgMIw+u2dPDntD3dgN4XQFLU2/fvFQdzj+EeSGfd/jnY5fQ==", + "requires": { + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^1.6.0", + "source-map": "^0.7.3" + }, + "dependencies": { + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==" + } + } + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "w3c-hr-time": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", + "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", + "requires": { + "browser-process-hrtime": "^1.0.0" + } + }, + "w3c-xmlserializer": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-1.1.2.tgz", + "integrity": "sha512-p10l/ayESzrBMYWRID6xbuCKh2Fp77+sA0doRuGn4tTIMrrZVeqfpKjXHY+oDh3K4nLdPgNwMTVP6Vp4pvqbNg==", + "requires": { + "domexception": "^1.0.1", + "webidl-conversions": "^4.0.2", + "xml-name-validator": "^3.0.0" + } + }, + "walker": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz", + "integrity": "sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=", + "requires": { + "makeerror": "1.0.x" + } + }, + "webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==" + }, + "whatwg-encoding": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", + "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", + "requires": { + "iconv-lite": "0.4.24" + } + }, + "whatwg-mimetype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", + "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==" + }, + "whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==" + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "requires": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "ws": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.3.0.tgz", + "integrity": "sha512-iFtXzngZVXPGgpTlP1rBqsUK82p9tKqsWRPg5L56egiljujJT3vGAYnHANvFxBieXrTFavhzhxW52jnaWV+w2w==" + }, + "xml-name-validator": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", + "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==" + }, + "xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" + }, + "xregexp": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.3.0.tgz", + "integrity": "sha512-7jXDIFXh5yJ/orPn4SXjuVrWWoi4Cr8jfV1eHv9CixKSbU+jY4mxfrBwAuDvupPNKpMUY+FeIqsVw/JLT9+B8g==", + "requires": { + "@babel/runtime-corejs3": "^7.8.3" + } + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==" + }, + "yargs": { + "version": "15.4.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.0.tgz", + "integrity": "sha512-D3fRFnZwLWp8jVAAhPZBsmeIHY8tTsb8ItV9KaAaopmC6wde2u6Yw29JBIZHXw14kgkRnYmDgmQU4FVMDlIsWw==", + "requires": { + "cliui": "^6.0.0", + "decamelize": "^3.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + } + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "dependencies": { + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + } + } + } + } + }, "@wordpress/babel-plugin-import-jsx-pragma": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@wordpress/babel-plugin-import-jsx-pragma/-/babel-plugin-import-jsx-pragma-1.1.3.tgz", @@ -8892,6 +13585,43 @@ } } }, + "@wordpress/eslint-plugin": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@wordpress/eslint-plugin/-/eslint-plugin-7.1.0.tgz", + "integrity": "sha512-FTrKkpEa8vZg7/7M6GBhd1YW24hnh5rFGzKgKX4MGyB0Jw8GGSwld9J23eRbQ5JQWGFP/tmOMeiu6W1/arxy7Q==", + "dev": true, + "requires": { + "@wordpress/prettier-config": "^0.3.0", + "babel-eslint": "^10.1.0", + "eslint-config-prettier": "^6.10.1", + "eslint-plugin-jest": "^23.8.2", + "eslint-plugin-jsdoc": "^26.0.0", + "eslint-plugin-jsx-a11y": "^6.2.3", + "eslint-plugin-prettier": "^3.1.2", + "eslint-plugin-react": "^7.20.0", + "eslint-plugin-react-hooks": "^4.0.4", + "globals": "^12.0.0", + "prettier": "npm:wp-prettier@2.0.5", + "requireindex": "^1.2.0" + }, + "dependencies": { + "eslint-plugin-react-hooks": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.0.8.tgz", + "integrity": "sha512-6SSb5AiMCPd8FDJrzah+Z4F44P2CdOaK026cXFV+o/xSRzfOiV1FNFeLl2z6xm3yqWOQEZ5OfVgiec90qV2xrQ==", + "dev": true + }, + "globals": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", + "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", + "dev": true, + "requires": { + "type-fest": "^0.8.1" + } + } + } + }, "@wordpress/i18n": { "version": "3.13.0", "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-3.13.0.tgz", @@ -8963,6 +13693,12 @@ } } }, + "@wordpress/prettier-config": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@wordpress/prettier-config/-/prettier-config-0.3.0.tgz", + "integrity": "sha512-wL1ztV+so5Ttwz23lDmb8ZmREmND96sf+Dh/kbP2nyAw/DWt3K8uj31qbczVmjwfoetTiRoH9Z1CasgPs4bccg==", + "dev": true + }, "@wordpress/url": { "version": "2.16.0", "resolved": "https://registry.npmjs.org/@wordpress/url/-/url-2.16.0.tgz", @@ -9361,6 +14097,33 @@ } } }, + "aria-query": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz", + "integrity": "sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA==", + "dev": true, + "requires": { + "@babel/runtime": "^7.10.2", + "@babel/runtime-corejs3": "^7.10.2" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.11.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.11.2.tgz", + "integrity": "sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.13.4" + } + }, + "regenerator-runtime": { + "version": "0.13.7", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", + "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==", + "dev": true + } + } + }, "arr-diff": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", @@ -9385,6 +14148,12 @@ "integrity": "sha512-KbUpJgx909ZscOc/7CLATBFam7P1Z1QRQInvgT0UztM9Q72aGKCunKASAl7WNW0tnPmPyEMeMhdsfWhfmW037w==", "dev": true }, + "array-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", + "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8=", + "dev": true + }, "array-equal": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", @@ -9409,6 +14178,23 @@ "integrity": "sha1-nlKHYrSpBmrRY6aWKjZEGOlibs4=", "dev": true }, + "array-includes": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.1.tgz", + "integrity": "sha512-c2VXaCHl7zPsvpkFsw4nxvFie4fh1ur9bpcgsVkIjqn0H/Xwdg+7fv3n2r/isyS8EBj5b06M9kHyZuIr4El6WQ==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0", + "is-string": "^1.0.5" + } + }, + "array-slice": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", + "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==", + "dev": true + }, "array-union": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", @@ -9450,6 +14236,17 @@ "es-abstract": "^1.17.0-next.1" } }, + "array.prototype.flatmap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.2.3.tgz", + "integrity": "sha512-OOEk+lkePcg+ODXIpvuU9PAryCikCJyo7GlDG1upleEpQRx6mzL9puEBkozQ5iAx20KV0l3DbyQwqciJtqe5Pg==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1", + "function-bind": "^1.1.1" + } + }, "arrify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", @@ -9527,6 +14324,12 @@ "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", "dev": true }, + "ast-types-flow": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", + "integrity": "sha1-9wtzXGvKGlycItmCw+Oef+ujva0=", + "dev": true + }, "astral-regex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", @@ -9576,48 +14379,24 @@ "dev": true }, "autoprefixer": { - "version": "9.8.4", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.4.tgz", - "integrity": "sha512-84aYfXlpUe45lvmS+HoAWKCkirI/sw4JK0/bTeeqgHYco3dcsOn0NqdejISjptsYwNji/21dnkDri9PsYKk89A==", + "version": "9.8.6", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.6.tgz", + "integrity": "sha512-XrvP4VVHdRBCdX1S3WXVD8+RyG9qeb1D5Sn1DeLiG2xfSpzellk5k54xbUERJ3M5DggQxes39UGOTP8CFrEGbg==", "dev": true, "requires": { "browserslist": "^4.12.0", - "caniuse-lite": "^1.0.30001087", - "colorette": "^1.2.0", + "caniuse-lite": "^1.0.30001109", + "colorette": "^1.2.1", "normalize-range": "^0.1.2", "num2fraction": "^1.2.2", "postcss": "^7.0.32", "postcss-value-parser": "^4.1.0" }, "dependencies": { - "browserslist": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.12.1.tgz", - "integrity": "sha512-WMjXwFtPskSW1pQUDJRxvRKRkeCr7usN0O/Za76N+F4oadaTdQHotSGcX9jT/Hs7mSKPkyMFNvqawB/1HzYDKQ==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001088", - "electron-to-chromium": "^1.3.481", - "escalade": "^3.0.1", - "node-releases": "^1.1.58" - } - }, "caniuse-lite": { - "version": "1.0.30001088", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001088.tgz", - "integrity": "sha512-6eYUrlShRYveyqKG58HcyOfPgh3zb2xqs7NvT2VVtP3hEUeeWvc3lqhpeMTxYWBBeeaT9A4bKsrtjATm66BTHg==", - "dev": true - }, - "electron-to-chromium": { - "version": "1.3.483", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.483.tgz", - "integrity": "sha512-+05RF8S9rk8S0G8eBCqBRBaRq7+UN3lDs2DAvnG8SBSgQO3hjy0+qt4CmRk5eiuGbTcaicgXfPmBi31a+BD3lg==", - "dev": true - }, - "node-releases": { - "version": "1.1.58", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.58.tgz", - "integrity": "sha512-NxBudgVKiRh/2aPWMgPR7bPTX0VPmGx5QBwCtdHitnqFE5/O8DeBXuIMH1nwNnw/aMo6AjOrpsHzfY3UbUJ7yg==", + "version": "1.0.30001109", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001109.tgz", + "integrity": "sha512-4JIXRodHzdS3HdK8nSgIqXYLExOvG+D2/EenSvcub2Kp3QEADjo2v2oUn5g0n0D+UNwG9BtwKOyGcSq2qvQXvQ==", "dev": true }, "postcss": { @@ -9651,6 +14430,12 @@ "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", "dev": true }, + "axe-core": { + "version": "3.5.5", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-3.5.5.tgz", + "integrity": "sha512-5P0QZ6J5xGikH780pghEdbEKijCTrruK9KxtPZCFWUpef0f6GipO+xEZ5GKCb020mmqgbiNO6TcA55CriL784Q==", + "dev": true + }, "axios": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", @@ -9660,6 +14445,12 @@ "follow-redirects": "1.5.10" } }, + "axobject-query": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", + "integrity": "sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA==", + "dev": true + }, "babel-code-frame": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", @@ -11198,12 +15989,6 @@ "safe-buffer": "^5.0.1" } }, - "cjk-regex": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cjk-regex/-/cjk-regex-1.0.2.tgz", - "integrity": "sha512-NwSMtwULPLk8Ka9DEUcoFXhMRnV/bpyKDnoyDiVw/Qy5przhvHTvXLcsKaOmx13o8J4XEsPVT1baoCUj5zQs3w==", - "dev": true - }, "class-utils": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", @@ -11451,12 +16236,6 @@ "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", "dev": true }, - "coffeescript": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/coffeescript/-/coffeescript-1.10.0.tgz", - "integrity": "sha1-56qDAZF+9iGzXYo580jc3R234z4=", - "dev": true - }, "collapse-white-space": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-1.0.5.tgz", @@ -11495,9 +16274,9 @@ "dev": true }, "colorette": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.0.tgz", - "integrity": "sha512-soRSroY+OF/8OdA3PTQXwaDJeMc7TfknKKrxeSCencL2a4+Tx5zhxmmv7hdpCjhKBjehzp8+bwe/T68K0hpIjw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.1.tgz", + "integrity": "sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==", "dev": true }, "colors": { @@ -11548,6 +16327,12 @@ "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", "dev": true }, + "comment-parser": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-0.7.5.tgz", + "integrity": "sha512-iH9YA35ccw94nx5244GVkpyC9eVTsL71jZz6iz5w6RIf79JLF2AsXHXq9p6Oaohyl3sx5qSMnGsWUDFIAfWL4w==", + "dev": true + }, "commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", @@ -12683,6 +17468,12 @@ } } }, + "core-js-pure": { + "version": "3.6.5", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.6.5.tgz", + "integrity": "sha512-lacdXOimsiD0QyNf9BC/mxivNJ/ybBGJXQFKzRekp1WTHoVUWsUHEn+2T8GJAzzIhyOuXA+gOxCVN3l+5PLPUA==", + "dev": true + }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -12922,6 +17713,12 @@ "integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=", "dev": true }, + "damerau-levenshtein": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.6.tgz", + "integrity": "sha512-JVrozIeElnj3QzfUIt8tB8YMluBJom4Vw9qTPpjGYQ9fYlB3D/rb6OordUxf3xeFB35LKWs0xqcO5U6ySvBtug==", + "dev": true + }, "dargs": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/dargs/-/dargs-4.1.0.tgz", @@ -12940,12 +17737,6 @@ "assert-plus": "^1.0.0" } }, - "dashify": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/dashify/-/dashify-0.2.2.tgz", - "integrity": "sha1-agdBWgHJH69KMuONnfunH2HLIP4=", - "dev": true - }, "data-urls": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.1.0.tgz", @@ -12964,14 +17755,10 @@ "dev": true }, "dateformat": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.12.tgz", - "integrity": "sha1-nxJLZ1lMk3/3BpMuSmQsyo27/uk=", - "dev": true, - "requires": { - "get-stdin": "^4.0.1", - "meow": "^3.3.0" - } + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", + "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==", + "dev": true }, "deasync": { "version": "0.1.20", @@ -13391,42 +18178,6 @@ "safer-buffer": "^2.1.0" } }, - "editorconfig": { - "version": "0.14.2", - "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.14.2.tgz", - "integrity": "sha512-tghjvKwo1gakrhFiZWlbo5ILWAfnuOu1JFztW0li+vzbnInN0CMZuF4F0T/Pnn9UWpT7Mr1aFTWdHVuxiR9K9A==", - "dev": true, - "requires": { - "bluebird": "^3.0.5", - "commander": "^2.9.0", - "lru-cache": "^3.2.0", - "semver": "^5.1.0", - "sigmund": "^1.0.1" - }, - "dependencies": { - "commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true - }, - "lru-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-3.2.0.tgz", - "integrity": "sha1-cXibO39Tmb7IVl3aOKow0qCX7+4=", - "dev": true, - "requires": { - "pseudomap": "^1.0.1" - } - } - } - }, - "editorconfig-to-prettier": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/editorconfig-to-prettier/-/editorconfig-to-prettier-0.0.6.tgz", - "integrity": "sha512-Ysw+hBdwhPFruYmLapKRm7Or5XgMzhasbqu4AN07V2l/AkqpgooWm2xtTQPzTD6S0tq54A+WbSxNt6qmsO3hoA==", - "dev": true - }, "electron-to-chromium": { "version": "1.3.483", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.483.tgz", @@ -13846,6 +18597,23 @@ } } }, + "eslint-config-prettier": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.11.0.tgz", + "integrity": "sha512-oB8cpLWSAjOVFEJhhyMZh6NOEOtBVziaqdDQ86+qhDHFbZXoRTM7pNSvFRfW/W/L/LrQ38C99J5CGuRBBzBsdA==", + "dev": true, + "requires": { + "get-stdin": "^6.0.0" + }, + "dependencies": { + "get-stdin": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", + "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", + "dev": true + } + } + }, "eslint-config-wpcalypso": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/eslint-config-wpcalypso/-/eslint-config-wpcalypso-5.0.0.tgz", @@ -13864,6 +18632,201 @@ "@typescript-eslint/experimental-utils": "^2.5.0" } }, + "eslint-plugin-jsdoc": { + "version": "26.0.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-26.0.2.tgz", + "integrity": "sha512-KtZjqtM3Z8x84vQBFKGUyBbZRGXYHVWSJ2XyYSUTc8KhfFrvzQ/GXPp6f1M1/YCNzP3ImD5RuDNcr+OVvIZcBA==", + "dev": true, + "requires": { + "comment-parser": "^0.7.4", + "debug": "^4.1.1", + "jsdoctypeparser": "^6.1.0", + "lodash": "^4.17.15", + "regextras": "^0.7.1", + "semver": "^6.3.0", + "spdx-expression-parse": "^3.0.1" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "lodash": { + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", + "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + } + } + }, + "eslint-plugin-jsx-a11y": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.3.1.tgz", + "integrity": "sha512-i1S+P+c3HOlBJzMFORRbC58tHa65Kbo8b52/TwCwSKLohwvpfT5rm2GjGWzOHTEuq4xxf2aRlHHTtmExDQOP+g==", + "dev": true, + "requires": { + "@babel/runtime": "^7.10.2", + "aria-query": "^4.2.2", + "array-includes": "^3.1.1", + "ast-types-flow": "^0.0.7", + "axe-core": "^3.5.4", + "axobject-query": "^2.1.2", + "damerau-levenshtein": "^1.0.6", + "emoji-regex": "^9.0.0", + "has": "^1.0.3", + "jsx-ast-utils": "^2.4.1", + "language-tags": "^1.0.5" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.11.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.11.2.tgz", + "integrity": "sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.13.4" + } + }, + "emoji-regex": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.0.0.tgz", + "integrity": "sha512-6p1NII1Vm62wni/VR/cUMauVQoxmLVb9csqQlvLz+hO2gk8U2UYDfXHQSUYIBKmZwAKz867IDqG7B+u0mj+M6w==", + "dev": true + }, + "regenerator-runtime": { + "version": "0.13.7", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", + "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==", + "dev": true + } + } + }, + "eslint-plugin-prettier": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.4.tgz", + "integrity": "sha512-jZDa8z76klRqo+TdGDTFJSavwbnWK2ZpqGKNZ+VvweMW516pDUMmQ2koXvxEE4JhzNvTv+radye/bWGBmA6jmg==", + "dev": true, + "requires": { + "prettier-linter-helpers": "^1.0.0" + } + }, + "eslint-plugin-react": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.20.5.tgz", + "integrity": "sha512-ajbJfHuFnpVNJjhyrfq+pH1C0gLc2y94OiCbAXT5O0J0YCKaFEHDV8+3+mDOr+w8WguRX+vSs1bM2BDG0VLvCw==", + "dev": true, + "requires": { + "array-includes": "^3.1.1", + "array.prototype.flatmap": "^1.2.3", + "doctrine": "^2.1.0", + "has": "^1.0.3", + "jsx-ast-utils": "^2.4.1", + "object.entries": "^1.1.2", + "object.fromentries": "^2.0.2", + "object.values": "^1.1.1", + "prop-types": "^15.7.2", + "resolve": "^1.17.0", + "string.prototype.matchall": "^4.0.2" + }, + "dependencies": { + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "es-abstract": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", + "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true + }, + "is-callable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.0.tgz", + "integrity": "sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==", + "dev": true + }, + "is-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.0.tgz", + "integrity": "sha512-iI97M8KTWID2la5uYXlkbSDQIg4F6o1sYboZKKTDpnDQMLtUL86zxhgDet3Q2SriaYsyGqZ6Mn2SjbRKeLHdqw==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + }, + "object.entries": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.2.tgz", + "integrity": "sha512-BQdB9qKmb/HyNdMNWVr7O3+z5MUIx3aiegEIJqjMBbBf0YT9RRxTJSim4mzFqtyr7PDAHigq0N9dO0m0tRakQA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5", + "has": "^1.0.3" + } + }, + "resolve": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", + "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + } + } + }, "eslint-plugin-react-hooks": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-2.3.0.tgz", @@ -14236,6 +19199,12 @@ "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", "dev": true }, + "fast-diff": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", + "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", + "dev": true + }, "fast-glob": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.1.1.tgz", @@ -14270,15 +19239,6 @@ "reusify": "^1.0.0" } }, - "fault": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz", - "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==", - "dev": true, - "requires": { - "format": "^0.2.0" - } - }, "faye-websocket": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz", @@ -14508,6 +19468,36 @@ } } }, + "fined": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz", + "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==", + "dev": true, + "requires": { + "expand-tilde": "^2.0.2", + "is-plain-object": "^2.0.3", + "object.defaults": "^1.1.0", + "object.pick": "^1.2.0", + "parse-filepath": "^1.0.1" + }, + "dependencies": { + "expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", + "dev": true, + "requires": { + "homedir-polyfill": "^1.0.1" + } + } + } + }, + "flagged-respawn": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz", + "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==", + "dev": true + }, "flat": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.0.tgz", @@ -14542,18 +19532,6 @@ "integrity": "sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==", "dev": true }, - "flatten": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/flatten/-/flatten-1.0.3.tgz", - "integrity": "sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg==", - "dev": true - }, - "flow-parser": { - "version": "0.59.0", - "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.59.0.tgz", - "integrity": "sha1-9uvK5h/6GH5CCZnUDOCoAfObJjU=", - "dev": true - }, "flush-write-stream": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", @@ -14616,12 +19594,6 @@ "mime-types": "^2.1.12" } }, - "format": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", - "integrity": "sha1-1hcBB+nv3E7TDJ3DkBbflCtctYs=", - "dev": true - }, "fragment-cache": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", @@ -16058,27 +21030,6 @@ "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", "dev": true }, - "globby": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", - "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", - "dev": true, - "requires": { - "array-union": "^1.0.1", - "glob": "^7.0.3", - "object-assign": "^4.0.1", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - }, - "dependencies": { - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - } - } - }, "globjoin": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/globjoin/-/globjoin-0.1.4.tgz", @@ -16119,15 +21070,6 @@ "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==", "dev": true }, - "graphql": { - "version": "0.10.5", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-0.10.5.tgz", - "integrity": "sha512-Q7cx22DiLhwHsEfUnUip1Ww/Vfx7FS0w6+iHItNuN61+XpegHSa3k5U0+6M5BcpavQImBwFiy0z3uYwY7cXMLQ==", - "dev": true, - "requires": { - "iterall": "^1.1.0" - } - }, "growl": { "version": "1.10.5", "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", @@ -16141,67 +21083,97 @@ "dev": true }, "grunt": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.1.0.tgz", - "integrity": "sha512-+NGod0grmviZ7Nzdi9am7vuRS/h76PcWDsV635mEXF0PEQMUV6Kb+OjTdsVxbi0PZmfQOjCMKb3w8CVZcqsn1g==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.2.1.tgz", + "integrity": "sha512-zgJjn9N56tScvRt/y0+1QA+zDBnKTrkpyeSBqQPLcZvbqTD/oyGMrdZQXmm6I3828s+FmPvxc3Xv+lgKFtudOw==", "dev": true, "requires": { - "coffeescript": "~1.10.0", - "dateformat": "~1.0.12", + "dateformat": "~3.0.3", "eventemitter2": "~0.4.13", - "exit": "~0.1.1", + "exit": "~0.1.2", "findup-sync": "~0.3.0", - "glob": "~7.0.0", - "grunt-cli": "~1.2.0", + "glob": "~7.1.6", + "grunt-cli": "~1.3.2", "grunt-known-options": "~1.1.0", "grunt-legacy-log": "~2.0.0", "grunt-legacy-util": "~1.1.1", "iconv-lite": "~0.4.13", - "js-yaml": "~3.13.1", - "minimatch": "~3.0.2", - "mkdirp": "~1.0.3", + "js-yaml": "~3.14.0", + "minimatch": "~3.0.4", + "mkdirp": "~1.0.4", "nopt": "~3.0.6", - "path-is-absolute": "~1.0.0", - "rimraf": "~2.6.2" + "rimraf": "~3.0.2" }, "dependencies": { "glob": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.6.tgz", - "integrity": "sha1-IRuvr0nlJbjNkyYNFKsTYVKz9Xo=", + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", "dev": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", - "minimatch": "^3.0.2", + "minimatch": "^3.0.4", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "grunt-cli": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/grunt-cli/-/grunt-cli-1.2.0.tgz", - "integrity": "sha1-VisRnrsGndtGSs4oRVAb6Xs1tqg=", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/grunt-cli/-/grunt-cli-1.3.2.tgz", + "integrity": "sha512-8OHDiZZkcptxVXtMfDxJvmN7MVJNE8L/yIcPb4HB7TlyFD1kDvjHrb62uhySsU14wJx9ORMnTuhRMQ40lH/orQ==", "dev": true, "requires": { - "findup-sync": "~0.3.0", "grunt-known-options": "~1.1.0", - "nopt": "~3.0.6", - "resolve": "~1.1.0" + "interpret": "~1.1.0", + "liftoff": "~2.5.0", + "nopt": "~4.0.1", + "v8flags": "~3.1.1" + }, + "dependencies": { + "nopt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", + "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", + "dev": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + } + } + }, + "interpret": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz", + "integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=", + "dev": true + }, + "js-yaml": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", + "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" } }, "mkdirp": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.3.tgz", - "integrity": "sha512-6uCP4Qc0sWsgMLy1EOqqS/3rjDHOEnsStVr/4vtAIK2Y5i2kA7lFFejYrpIyiN9w0pYf4ckeCYT9f1r1P9KX5g==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", "dev": true }, - "resolve": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", - "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", - "dev": true + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } } } }, @@ -17398,6 +22370,17 @@ } } }, + "internal-slot": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.2.tgz", + "integrity": "sha512-2cQNfwhAfJIkU4KZPkDI+Gj5yNNnbqi40W9Gge6dfnk4TocEVm00B3bdiL+JINrbGJil2TeHvM4rETGzk/f/0g==", + "dev": true, + "requires": { + "es-abstract": "^1.17.0-next.1", + "has": "^1.0.3", + "side-channel": "^1.0.2" + } + }, "interpret": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.2.0.tgz", @@ -17431,6 +22414,16 @@ "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=", "dev": true }, + "is-absolute": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", + "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", + "dev": true, + "requires": { + "is-relative": "^1.0.0", + "is-windows": "^1.0.1" + } + }, "is-accessor-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", @@ -17681,6 +22674,15 @@ "integrity": "sha1-/S2INUXEa6xaYz57mgnof6LLUGk=", "dev": true }, + "is-relative": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", + "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", + "dev": true, + "requires": { + "is-unc-path": "^1.0.0" + } + }, "is-ssh": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/is-ssh/-/is-ssh-1.3.1.tgz", @@ -17732,6 +22734,15 @@ "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", "dev": true }, + "is-unc-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", + "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", + "dev": true, + "requires": { + "unc-path-regex": "^0.1.2" + } + }, "is-utf8": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", @@ -17939,12 +22950,6 @@ "handlebars": "^4.0.3" } }, - "iterall": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/iterall/-/iterall-1.3.0.tgz", - "integrity": "sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg==", - "dev": true - }, "jest": { "version": "25.1.0", "resolved": "https://registry.npmjs.org/jest/-/jest-25.1.0.tgz", @@ -18574,15 +23579,6 @@ } } }, - "jest-docblock": { - "version": "21.3.0-beta.11", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-21.3.0-beta.11.tgz", - "integrity": "sha512-sxSwZUm7JyCO8dverup5g/OKJhjYRrBdgEdezIO1qAmMGWuza7ewovpfDmxp+JLvlm0i2WRFKUQNNIMGmPGTVg==", - "dev": true, - "requires": { - "detect-newline": "^2.1.0" - } - }, "jest-each": { "version": "25.1.0", "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-25.1.0.tgz", @@ -18763,12 +23759,6 @@ } } }, - "jest-get-type": { - "version": "21.2.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-21.2.0.tgz", - "integrity": "sha512-y2fFw3C+D0yjNSDp7ab1kcd6NUYfy3waPTlD8yWkAtiocJdBRQqNoRqVfMNxgj+IjT0V5cBIHJO0z9vuSSZ43Q==", - "dev": true - }, "jest-haste-map": { "version": "25.1.0", "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-25.1.0.tgz", @@ -19753,18 +24743,6 @@ } } }, - "jest-validate": { - "version": "21.1.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-21.1.0.tgz", - "integrity": "sha512-xS0cyErNWpsLFlGkn/b87pk/Mv7J+mCTs8hQ4KmtOIIoM1sHYobXII8AtkoN8FC7E3+Ptxjo+/3xWk6LK1dKcw==", - "dev": true, - "requires": { - "chalk": "^2.0.1", - "jest-get-type": "^21.0.2", - "leven": "^2.1.0", - "pretty-format": "^21.1.0" - } - }, "jest-watcher": { "version": "25.1.0", "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-25.1.0.tgz", @@ -19895,6 +24873,12 @@ "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", "dev": true }, + "jsdoctypeparser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsdoctypeparser/-/jsdoctypeparser-6.1.0.tgz", + "integrity": "sha512-UCQBZ3xCUBv/PLfwKAJhp6jmGOSLFNKzrotXGNgbKhWvz27wPsCsVeP7gIcHPElQw2agBmynAitXqhxR58XAmA==", + "dev": true + }, "jsdom": { "version": "15.2.1", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-15.2.1.tgz", @@ -20034,6 +25018,16 @@ "verror": "1.10.0" } }, + "jsx-ast-utils": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.4.1.tgz", + "integrity": "sha512-z1xSldJ6imESSzOjd3NNkieVJKRlKYSOtMG8SFyCj2FIrvSaSuli/WjpBkEzCBoR9bYYYFgqJw61Xhu7Lcgk+w==", + "dev": true, + "requires": { + "array-includes": "^3.1.1", + "object.assign": "^4.1.0" + } + }, "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", @@ -20055,6 +25049,21 @@ "integrity": "sha512-Vi3nxDGMm/z+lAaCjvAR1u+7fiv+sG6gU/iYDj5QOF8h76ytK9EW/EKfF0NeTyiGBi8Jy6Hklty/vxISrLox3w==", "dev": true }, + "language-subtag-registry": { + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.20.tgz", + "integrity": "sha512-KPMwROklF4tEx283Xw0pNKtfTj1gZ4UByp4EsIFWLgBavJltF4TiYPc39k06zSTsLzxTVXXDSpbwaQXaFB4Qeg==", + "dev": true + }, + "language-tags": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.5.tgz", + "integrity": "sha1-0yHbxNowuovzAk4ED6XBRmH5GTo=", + "dev": true, + "requires": { + "language-subtag-registry": "~0.3.2" + } + }, "lazy-cache": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", @@ -20147,12 +25156,6 @@ } } }, - "leven": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", - "integrity": "sha1-wuep93IJTe6dNCAq6KzORoeHVYA=", - "dev": true - }, "levenary": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/levenary/-/levenary-1.1.1.tgz", @@ -20180,6 +25183,187 @@ "type-check": "~0.3.2" } }, + "liftoff": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-2.5.0.tgz", + "integrity": "sha1-IAkpG7Mc6oYbvxCnwVooyvdcMew=", + "dev": true, + "requires": { + "extend": "^3.0.0", + "findup-sync": "^2.0.0", + "fined": "^1.0.1", + "flagged-respawn": "^1.0.0", + "is-plain-object": "^2.0.4", + "object.map": "^1.0.0", + "rechoir": "^0.6.2", + "resolve": "^1.1.7" + }, + "dependencies": { + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", + "dev": true, + "requires": { + "homedir-polyfill": "^1.0.1" + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "findup-sync": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz", + "integrity": "sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw=", + "dev": true, + "requires": { + "detect-file": "^1.0.0", + "is-glob": "^3.1.0", + "micromatch": "^3.0.4", + "resolve-dir": "^1.0.1" + } + }, + "global-modules": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "dev": true, + "requires": { + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" + } + }, + "global-prefix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=", + "dev": true, + "requires": { + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" + } + }, + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "^2.1.0" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "resolve-dir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", + "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=", + "dev": true, + "requires": { + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" + } + } + } + }, "lines-and-columns": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", @@ -20666,12 +25850,6 @@ "lodash._reinterpolate": "^3.0.0" } }, - "lodash.unescape": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.unescape/-/lodash.unescape-4.0.1.tgz", - "integrity": "sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw=", - "dev": true - }, "lodash.uniq": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", @@ -20849,6 +26027,23 @@ } } }, + "make-iterator": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz", + "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==", + "dev": true, + "requires": { + "kind-of": "^6.0.2" + }, + "dependencies": { + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + } + } + }, "makeerror": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz", @@ -20907,9 +26102,9 @@ "dev": true }, "marked": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/marked/-/marked-0.6.2.tgz", - "integrity": "sha512-LqxwVH3P/rqKX4EKGz7+c2G9r98WeM/SW34ybhgNGhUQNKtf1GmmSkJ6cDGJ/t6tiyae49qRkpyTw2B9HOrgUA==", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/marked/-/marked-0.6.3.tgz", + "integrity": "sha512-Fqa7eq+UaxfMriqzYLayfqAE40WN03jf+zHjT18/uXNuzjq3TY0XTbrAoPeqSJrAmPz11VuUA+kBPYOhHt9oOQ==", "dev": true }, "mathml-tag-names": { @@ -21002,15 +26197,6 @@ "unist-util-visit": "^1.1.0" } }, - "mem": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz", - "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=", - "dev": true, - "requires": { - "mimic-fn": "^1.0.0" - } - }, "memize": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/memize/-/memize-1.1.0.tgz", @@ -22233,6 +27419,29 @@ "object-keys": "^1.0.11" } }, + "object.defaults": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", + "integrity": "sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8=", + "dev": true, + "requires": { + "array-each": "^1.0.1", + "array-slice": "^1.0.0", + "for-own": "^1.0.0", + "isobject": "^3.0.0" + }, + "dependencies": { + "for-own": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", + "dev": true, + "requires": { + "for-in": "^1.0.1" + } + } + } + }, "object.entries": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.1.tgz", @@ -22267,6 +27476,27 @@ "es-abstract": "^1.17.0-next.1" } }, + "object.map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz", + "integrity": "sha1-z4Plncj8wK1fQlDh94s7gb2AHTc=", + "dev": true, + "requires": { + "for-own": "^1.0.0", + "make-iterator": "^1.0.0" + }, + "dependencies": { + "for-own": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", + "dev": true, + "requires": { + "for-in": "^1.0.1" + } + } + } + }, "object.pick": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", @@ -22591,6 +27821,17 @@ "is-hexadecimal": "^1.0.0" } }, + "parse-filepath": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", + "integrity": "sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE=", + "dev": true, + "requires": { + "is-absolute": "^1.0.0", + "map-cache": "^0.2.0", + "path-root": "^0.1.1" + } + }, "parse-github-repo-url": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/parse-github-repo-url/-/parse-github-repo-url-1.4.1.tgz", @@ -22929,92 +28170,6 @@ "@babel/core": ">=7.2.2" } }, - "postcss-less": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/postcss-less/-/postcss-less-1.1.3.tgz", - "integrity": "sha512-WS0wsQxRm+kmN8wEYAGZ3t4lnoNfoyx9EJZrhiPR1K0lMHR0UNWnz52Ya5QRXChHtY75Ef+kDc05FpnBujebgw==", - "dev": true, - "requires": { - "postcss": "^5.2.16" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - }, - "dependencies": { - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - } - } - }, - "has-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", - "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", - "dev": true - }, - "postcss": { - "version": "5.2.18", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.18.tgz", - "integrity": "sha512-zrUjRRe1bpXKsX1qAJNJjqZViErVuyEkMTRrwu4ud4sbTtIBRmtaYDrHmcGgmrbsW3MHfmtIf+vJumgQn+PrXg==", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "js-base64": "^2.1.9", - "source-map": "^0.5.6", - "supports-color": "^3.2.3" - } - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "supports-color": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", - "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", - "dev": true, - "requires": { - "has-flag": "^1.0.0" - } - } - } - }, "postcss-markdown": { "version": "0.36.0", "resolved": "https://registry.npmjs.org/postcss-markdown/-/postcss-markdown-0.36.0.tgz", @@ -23079,48 +28234,6 @@ "postcss": "^7.0.21" } }, - "postcss-scss": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-1.0.2.tgz", - "integrity": "sha1-/0XPM1S4ee6JpOtoaA9GrJuxT5Q=", - "dev": true, - "requires": { - "postcss": "^6.0.3" - }, - "dependencies": { - "postcss": { - "version": "6.0.23", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", - "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", - "dev": true, - "requires": { - "chalk": "^2.4.1", - "source-map": "^0.6.1", - "supports-color": "^5.4.0" - } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "postcss-selector-parser": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-2.2.3.tgz", - "integrity": "sha1-+UN3iGBsPJrO4W/+jYsWKX8nu5A=", - "dev": true, - "requires": { - "flatten": "^1.0.2", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - } - }, "postcss-syntax": { "version": "0.36.2", "resolved": "https://registry.npmjs.org/postcss-syntax/-/postcss-syntax-0.36.2.tgz", @@ -23133,17 +28246,6 @@ "integrity": "sha512-LmeoohTpp/K4UiyQCwuGWlONxXamGzCMtFxLq4W1nZVGIQLYvMCJx3yAF9qyyuFpflABI9yVdtJAqbihOsCsJQ==", "dev": true }, - "postcss-values-parser": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/postcss-values-parser/-/postcss-values-parser-1.3.1.tgz", - "integrity": "sha512-chFn9CnFAAUpQ3cwrxvVjKB8c0y6BfONv6eapndJoTXJ3h8fr1uAiue8lGP3rUIpBI2KgJGdgCVk9KNvXh0n6A==", - "dev": true, - "requires": { - "flatten": "^1.0.2", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - } - }, "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", @@ -23151,163 +28253,18 @@ "dev": true }, "prettier": { - "version": "github:automattic/calypso-prettier#c56b42511ec98ba6d8f72b6c391e0a626e90f531", - "from": "github:automattic/calypso-prettier#c56b4251", + "version": "npm:wp-prettier@2.0.5", + "resolved": "https://registry.npmjs.org/wp-prettier/-/wp-prettier-2.0.5.tgz", + "integrity": "sha512-5GCgdeevIXwR3cW4Qj5XWC5MO1iSCz8+IPn0mMw6awAt/PBiey8yyO7MhePRsaMqghJAhg6Q3QLYWSnUHWkG6A==", + "dev": true + }, + "prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", "dev": true, "requires": { - "babel-code-frame": "7.0.0-beta.3", - "babylon": "7.0.0-beta.34", - "camelcase": "4.1.0", - "chalk": "2.1.0", - "cjk-regex": "1.0.2", - "cosmiconfig": "3.1.0", - "dashify": "0.2.2", - "diff": "3.2.0", - "editorconfig": "0.14.2", - "editorconfig-to-prettier": "0.0.6", - "emoji-regex": "6.5.1", - "escape-string-regexp": "1.0.5", - "esutils": "2.0.2", - "flow-parser": "0.59.0", - "get-stream": "3.0.0", - "globby": "6.1.0", - "graphql": "0.10.5", - "ignore": "3.3.7", - "jest-docblock": "21.3.0-beta.11", - "jest-validate": "21.1.0", - "leven": "2.1.0", - "mem": "1.1.0", - "minimatch": "3.0.4", - "minimist": "1.2.0", - "parse5": "3.0.3", - "path-root": "0.1.1", - "postcss-less": "1.1.3", - "postcss-media-query-parser": "0.2.3", - "postcss-scss": "1.0.2", - "postcss-selector-parser": "2.2.3", - "postcss-values-parser": "1.3.1", - "remark-frontmatter": "1.1.0", - "remark-parse": "4.0.0", - "semver": "5.4.1", - "string-width": "2.1.1", - "typescript": "2.6.2", - "typescript-eslint-parser": "9.0.1", - "unicode-regex": "1.0.1", - "unified": "6.1.6" - }, - "dependencies": { - "babel-code-frame": { - "version": "7.0.0-beta.3", - "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-7.0.0-beta.3.tgz", - "integrity": "sha512-flMsJ9eSpShupt2Gwpka84DoMePvE4HlDObzdEc+1iNkacv3+NHlsJ7dMKmbnVA/AT22UhcGEBHwbJLoXWBO6Q==", - "dev": true, - "requires": { - "chalk": "^2.0.0", - "esutils": "^2.0.2", - "js-tokens": "^3.0.0" - } - }, - "babylon": { - "version": "7.0.0-beta.34", - "resolved": "https://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.34.tgz", - "integrity": "sha512-ribEzWEhWKKjY+1FdKCryo+HiN/1idPjUB8vyR5Yf221MtGzCd5+7OwPvWvYHerHHC2eJLr6MhvumbTocXGY7Q==", - "dev": true - }, - "camelcase": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", - "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", - "dev": true - }, - "chalk": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.1.0.tgz", - "integrity": "sha512-LUHGS/dge4ujbXMJrnihYMcL4AoOweGnw9Tp3kQuqy1Kx5c1qKjqvMJZ6nVJPMWJtKCTN72ZogH3oeSO9g9rXQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.1.0", - "escape-string-regexp": "^1.0.5", - "supports-color": "^4.0.0" - } - }, - "cosmiconfig": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-3.1.0.tgz", - "integrity": "sha512-zedsBhLSbPBms+kE7AH4vHg6JsKDz6epSv2/+5XHs8ILHlgDciSJfSWf8sX9aQ52Jb7KI7VswUTsLpR/G0cr2Q==", - "dev": true, - "requires": { - "is-directory": "^0.3.1", - "js-yaml": "^3.9.0", - "parse-json": "^3.0.0", - "require-from-string": "^2.0.1" - } - }, - "diff": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.2.0.tgz", - "integrity": "sha1-yc45Okt8vQsFinJck98pkCeGj/k=", - "dev": true - }, - "emoji-regex": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-6.5.1.tgz", - "integrity": "sha512-PAHp6TxrCy7MGMFidro8uikr+zlJJKJ/Q6mm2ExZ7HwkyR9lSVFfE3kt36qcwa24BQL7y0G9axycGjK1A/0uNQ==", - "dev": true - }, - "get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", - "dev": true - }, - "has-flag": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", - "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", - "dev": true - }, - "ignore": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.7.tgz", - "integrity": "sha512-YGG3ejvBNHRqu0559EOxxNFihD0AjpvHlC/pdGKd3X3ofe+CoJkYazwNJYTNebqpPKN+VVQbh4ZFn1DivMNuHA==", - "dev": true - }, - "js-tokens": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", - "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", - "dev": true - }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - }, - "parse-json": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-3.0.0.tgz", - "integrity": "sha1-+m9HsY4jgm6tMvJj50TQ4ehH+xM=", - "dev": true, - "requires": { - "error-ex": "^1.3.1" - } - }, - "semver": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", - "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==", - "dev": true - }, - "supports-color": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", - "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", - "dev": true, - "requires": { - "has-flag": "^2.0.0" - } - } + "fast-diff": "^1.1.2" } }, "pretty-bytes": { @@ -23319,16 +28276,6 @@ "number-is-nan": "^1.0.0" } }, - "pretty-format": { - "version": "21.2.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-21.2.1.tgz", - "integrity": "sha512-ZdWPGYAnYfcVP8yKA3zFjCn8s4/17TeYH28MXuC8vTp0o21eXjbFGcOAXZEaDaOFJjc3h2qa7HQNHNshhvoh2A==", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0", - "ansi-styles": "^3.2.0" - } - }, "process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -25141,6 +30088,15 @@ "util.promisify": "^1.0.0" } }, + "rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "dev": true, + "requires": { + "resolve": "^1.1.6" + } + }, "redent": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", @@ -25214,6 +30170,16 @@ "safe-regex": "^1.1.0" } }, + "regexp.prototype.flags": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz", + "integrity": "sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1" + } + }, "regexpp": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", @@ -25234,6 +30200,12 @@ "unicode-match-property-value-ecmascript": "^1.2.0" } }, + "regextras": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/regextras/-/regextras-0.7.1.tgz", + "integrity": "sha512-9YXf6xtW+qzQ+hcMQXx95MOvfqXFgsKDZodX3qZB0x2n5Z94ioetIITsBtvJbiOyxa/6s9AtyweBLCdPmPko/w==", + "dev": true + }, "regjsgen": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.2.tgz", @@ -25327,39 +30299,6 @@ } } }, - "remark-frontmatter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remark-frontmatter/-/remark-frontmatter-1.1.0.tgz", - "integrity": "sha512-mLbYtwP9w1L9TA8dX+I/HyDF5lCpa0dmYvvW9Io+zUPpqEZ49QMKWb0hSpunpLVA+Squy0SowzSzjHVPbxWq1g==", - "dev": true, - "requires": { - "fault": "^1.0.1", - "xtend": "^4.0.1" - } - }, - "remark-parse": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-4.0.0.tgz", - "integrity": "sha512-XZgICP2gJ1MHU7+vQaRM+VA9HEL3X253uwUM/BGgx3iv6TH2B3bF3B8q00DKcyP9YrJV+/7WOWEWBFF/u8cIsw==", - "dev": true, - "requires": { - "collapse-white-space": "^1.0.2", - "is-alphabetical": "^1.0.0", - "is-decimal": "^1.0.0", - "is-whitespace-character": "^1.0.0", - "is-word-character": "^1.0.0", - "markdown-escapes": "^1.0.0", - "parse-entities": "^1.0.2", - "repeat-string": "^1.5.4", - "state-toggle": "^1.0.0", - "trim": "0.0.1", - "trim-trailing-lines": "^1.0.0", - "unherit": "^1.0.4", - "unist-util-remove-position": "^1.0.0", - "vfile-location": "^2.0.0", - "xtend": "^4.0.1" - } - }, "remark-stringify": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-6.0.4.tgz", @@ -25485,18 +30424,18 @@ "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", "dev": true }, - "require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true - }, "require-main-filename": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", "dev": true }, + "requireindex": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz", + "integrity": "sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==", + "dev": true + }, "resolve": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.1.tgz", @@ -26193,11 +31132,15 @@ "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", "dev": true }, - "sigmund": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", - "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=", - "dev": true + "side-channel": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.2.tgz", + "integrity": "sha512-7rL9YlPHg7Ancea1S96Pa8/QWb4BtXL/TZvS6B8XFetGBeuhAsfmUspK6DokBeZ64+Kj9TCNRD/30pVz1BvQNA==", + "dev": true, + "requires": { + "es-abstract": "^1.17.0-next.1", + "object-inspect": "^1.7.0" + } }, "signal-exit": { "version": "3.0.2", @@ -26697,6 +31640,28 @@ } } }, + "string.prototype.matchall": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.2.tgz", + "integrity": "sha512-N/jp6O5fMf9os0JU3E72Qhf590RSRZU/ungsL/qJUYVTNv7hTG0P/dbPjxINVN9jpscu3nzYwKESU3P3RY5tOg==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0", + "has-symbols": "^1.0.1", + "internal-slot": "^1.0.2", + "regexp.prototype.flags": "^1.3.0", + "side-channel": "^1.0.2" + }, + "dependencies": { + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true + } + } + }, "string.prototype.trim": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.1.tgz", @@ -26708,6 +31673,58 @@ "function-bind": "^1.1.1" } }, + "string.prototype.trimend": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz", + "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", + "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true + }, + "is-callable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.0.tgz", + "integrity": "sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==", + "dev": true + }, + "is-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.0.tgz", + "integrity": "sha512-iI97M8KTWID2la5uYXlkbSDQIg4F6o1sYboZKKTDpnDQMLtUL86zxhgDet3Q2SriaYsyGqZ6Mn2SjbRKeLHdqw==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + } + } + }, "string.prototype.trimleft": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz", @@ -26728,6 +31745,58 @@ "function-bind": "^1.1.1" } }, + "string.prototype.trimstart": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz", + "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + }, + "dependencies": { + "es-abstract": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", + "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", + "dev": true, + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.0", + "is-regex": "^1.1.0", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", + "dev": true + }, + "is-callable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.0.tgz", + "integrity": "sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==", + "dev": true + }, + "is-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.0.tgz", + "integrity": "sha512-iI97M8KTWID2la5uYXlkbSDQIg4F6o1sYboZKKTDpnDQMLtUL86zxhgDet3Q2SriaYsyGqZ6Mn2SjbRKeLHdqw==", + "dev": true, + "requires": { + "has-symbols": "^1.0.1" + } + } + } + }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -28133,29 +33202,11 @@ } }, "typescript": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.6.2.tgz", - "integrity": "sha1-PFtv1/beCRQmkCfwPAlGdY92c6Q=", + "version": "3.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.5.tgz", + "integrity": "sha512-hSAifV3k+i6lEoCJ2k6R2Z/rp/H3+8sdmcn5NrS3/3kE7+RyZXm9aqvxWqjEXHAd8b0pShatpcdMTvEdvAJltQ==", "dev": true }, - "typescript-eslint-parser": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/typescript-eslint-parser/-/typescript-eslint-parser-9.0.1.tgz", - "integrity": "sha512-w1jqotvnhLtLukD9H3gQPAlbD0kLf7ZkoQGwiwSIshKIlzRL7i0OY9Y7VIdE1xtytZXThg678eomxMZ1rZXGVQ==", - "dev": true, - "requires": { - "lodash.unescape": "4.0.1", - "semver": "5.4.1" - }, - "dependencies": { - "semver": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", - "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==", - "dev": true - } - } - }, "uglify-js": { "version": "3.5.11", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.5.11.tgz", @@ -28186,6 +33237,12 @@ "integrity": "sha1-8pzr8B31F5ErtY/5xOUP3o4zMg0=", "dev": true }, + "unc-path-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", + "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=", + "dev": true + }, "underscore.string": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.3.5.tgz", @@ -28234,27 +33291,6 @@ "integrity": "sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg==", "dev": true }, - "unicode-regex": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/unicode-regex/-/unicode-regex-1.0.1.tgz", - "integrity": "sha1-+BngUBkdW5VhozmljdO5CV7ZSzU=", - "dev": true - }, - "unified": { - "version": "6.1.6", - "resolved": "https://registry.npmjs.org/unified/-/unified-6.1.6.tgz", - "integrity": "sha512-pW2f82bCIo2ifuIGYcV12fL96kMMYgw7JKVEgh7ODlrM9rj6vXSY3BV+H6lCcv1ksxynFf582hwWLnA1qRFy4w==", - "dev": true, - "requires": { - "bail": "^1.0.0", - "extend": "^3.0.0", - "is-plain-obj": "^1.1.0", - "trough": "^1.0.0", - "vfile": "^2.0.0", - "x-is-function": "^1.0.4", - "x-is-string": "^0.1.0" - } - }, "union-value": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", @@ -28595,6 +33631,15 @@ } } }, + "v8flags": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.1.3.tgz", + "integrity": "sha512-amh9CCg3ZxkzQ48Mhcb8iX7xpAfYJgePHxWMQCBWECpOSqJUXgY26ncA61UTV0BkPqfhcy6mzwCIoP4ygxpW8w==", + "dev": true, + "requires": { + "homedir-polyfill": "^1.0.1" + } + }, "validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -28625,18 +33670,6 @@ "extsprintf": "^1.2.0" } }, - "vfile": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-2.3.0.tgz", - "integrity": "sha512-ASt4mBUHcTpMKD/l5Q+WJXNtshlWxOogYyGYYrg4lt/vuRjC1EFQtlAofL5VmtVNIZJzWYFJjzGWZ0Gw8pzW1w==", - "dev": true, - "requires": { - "is-buffer": "^1.1.4", - "replace-ext": "1.0.0", - "unist-util-stringify-position": "^1.0.0", - "vfile-message": "^1.0.0" - } - }, "vfile-location": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-2.0.4.tgz", @@ -29561,12 +34594,6 @@ "async-limiter": "~1.0.0" } }, - "x-is-function": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/x-is-function/-/x-is-function-1.0.4.tgz", - "integrity": "sha1-XSlNw9Joy90GJYDgxd93o5HR+h4=", - "dev": true - }, "x-is-string": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/x-is-string/-/x-is-string-0.1.0.tgz", diff --git a/package.json b/package.json index ee6f28d1548..8b0a6c8d309 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "scripts": { "build": "grunt && npm run makepot && npm run build:packages", "build-watch": "grunt watch", - "build:packages": "node ./tests/e2e/bin/build.js", + "build:packages": "lerna run build", "build:zip": "./bin/build-zip.sh", "lint:js": "eslint assets/js --ext=js", "docker:up": "npm explore @woocommerce/e2e-environment -- npm run docker:up", @@ -21,7 +21,7 @@ "test:e2e-dev": "npm explore @woocommerce/e2e-environment -- npm run test:e2e-dev", "makepot": "composer run-script makepot", "packages:fix:textdomain": "node ./bin/package-update-textdomain.js", - "publish-packages": "npm run build:packages && lerna publish from-package", + "publish-packages": "lerna publish from-package", "git:update-hooks": "rm -r .git/hooks && mkdir -p .git/hooks && node ./node_modules/husky/husky.js install" }, "devDependencies": { @@ -30,12 +30,16 @@ "@babel/polyfill": "7.10.4", "@babel/preset-env": "7.10.4", "@babel/register": "7.10.4", - "@jest/test-sequencer": "^25.0.0", + "@jest/test-sequencer": "25.1.0", + "@typescript-eslint/eslint-plugin": "3.1.0", + "@typescript-eslint/parser": "3.1.0", "@woocommerce/e2e-environment": "file:tests/e2e/env", + "@woocommerce/model-factories": "file:tests/e2e/factories", "@wordpress/babel-plugin-import-jsx-pragma": "1.1.3", "@wordpress/babel-preset-default": "3.0.2", "@wordpress/e2e-test-utils": "4.6.0", - "autoprefixer": "9.8.4", + "@wordpress/eslint-plugin": "7.1.0", + "autoprefixer": "9.8.6", "babel-eslint": "10.1.0", "chai": "4.2.0", "chai-as-promised": "7.1.1", @@ -47,7 +51,7 @@ "eslint-config-wpcalypso": "5.0.0", "eslint-plugin-jest": "23.19.0", "github-contributors-list": "https://github.com/woocommerce/github-contributors-list/tarball/master", - "grunt": "1.1.0", + "grunt": "1.2.1", "grunt-contrib-clean": "2.0.0", "grunt-contrib-concat": "1.0.1", "grunt-contrib-copy": "1.0.0", @@ -68,11 +72,12 @@ "lint-staged": "9.5.0", "mocha": "7.2.0", "node-sass": "4.13.0", - "prettier": "github:automattic/calypso-prettier#c56b4251", + "prettier": "npm:wp-prettier@^2.0.5", "puppeteer": "2.0.0", "puppeteer-utils": "github:Automattic/puppeteer-utils#0f3ec50", "stylelint": "12.0.1", "stylelint-config-wordpress": "16.0.0", + "typescript": "3.9.5", "webpack": "4.41.6", "webpack-cli": "3.3.11", "wp-textdomain": "^1.0.1" @@ -100,6 +105,10 @@ "*.js": [ "eslint --fix", "git add" + ], + "*.ts": [ + "eslint --fix", + "git add" ] }, "browserslist": [ diff --git a/phpcs.xml b/phpcs.xml index 52f90dad994..e20a6dd049b 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -81,6 +81,15 @@ tests/Tools/ + + src/ + tests/php/ + + + src/ + tests/php/ + + tests/php/ diff --git a/readme.txt b/readme.txt index d51e741f691..45119e0d2fc 100644 --- a/readme.txt +++ b/readme.txt @@ -4,7 +4,7 @@ Tags: e-commerce, store, sales, sell, woo, shop, cart, checkout, downloadable, d Requires at least: 5.2 Tested up to: 5.4 Requires PHP: 7.0 -Stable tag: 4.3.0 +Stable tag: 4.3.1 License: GPLv3 License URI: https://www.gnu.org/licenses/gpl-3.0.html @@ -181,6 +181,134 @@ INTERESTED IN DEVELOPMENT? = 4.4.0 - 2020-08-18 = +**WooCommerce** +* Accessibility: Adds alt attribute to photoswipe gallery images. #26945 +* Enhancement - Remove the privacy page dropdown from the Accounts & Privacy page. #26809 +* Enhancement - Added automatic language pack updates for WooCommerce.com extensions. #26750 +* Enhancement - Improvements for the Hungarian address format. #26697 +* Enhancement - Dropdown arrow width was made smaller. #26202 +* Enhancement - Add a "No change" option to the "Stock status" selector in quick edit, preselect it when the product being edited is a variable product. #26174 +* Enhancement - Don't request language packs for empty locales list. #27148 +* Localization - Added 14 Namibia regions. #26894 +* Localization - Change default Greek states names to English. #26719 +* Localization - Improved Puerto Rico addresses and improve address formatting. #26698 +* Localization - Wrapped price and currency inside a BDI tag, in order to prevent the bidirectional algorithm to produce confusing results. #26462 +* Localization - Added Algerian provinces. #25687 +* Tweak - Added "order_total" to the wcadmin_orders_edit_status_change tracker event. #26935 +* Tweak - Fixed WooCommerce menu for users that can only manage orders on WooCommerce. #26877 +* Tweak - Limit nocache headers to googleweblight by default. #26858 +* Tweak - Preserve quantity input value when changing variations. #26805 +* Tweak - Confirm before running any tool from the WooCommerce Status settings. #26660 +* Tweak - Limit stock changes for order items to status methods for consistency. #26642 +* Tweak - Custom vendor taxonomy update messages. #26634 +* Tweak - Remove HTML tags from plain text email template for Customer new account. #26613 +* Tweak - Conditionally change the text in My account to reflect if shipping is disabled. #26325 +* Tweak - Show CSV file name in result message when product import is complete. #25240 +* Tweak - Improve order details UI to highlight "Paid" and "Net Payment" sections. #27142 +* Fix - Remove the dot after the generated password in new account emails. #27073 +* Fix - Delayed the execution of all webhooks until after the request has completed. #27067 +* Fix - [Importer/Exporter] Fixed the value display of "Published" for children of draft variable products. #27046 +* Fix - Removed the extra id parameter added to CLI commands that shouldn't have one. #27017 +* Fix - Added the missing instance_id to the REST CLI command so that shipping zone method commands will work. #27017 +* Fix - Add rating_count to order by rating clause. #26964 +* Fix - Don't show premium support forum link if the store is not connected to WooCommerce.com. #26932 +* Fix - Incorrect capability used on add order note while creating an user note. #26920 +* Fix - Preserve HTML entities from product names in the cart page. #26885 +* Fix - Display warning hen leaving settings page without saving first. #26880 +* Fix - Remove wc_round_tax_total from shipping tax because shipping prices never include tax so rounding down is not needed. #26850 +* Fix - Make the "Please log in" message displayed to users with an existing account a hyperlink. #26837 +* Fix - Typo in composer.json for makepot. #26829 +* Fix - Layout issue on the checkout page when switching countries. #26697 +* Fix - Missing closing select tag to the product exporter category select. #26680 +* Fix - Possible PHP undefined index notice before WooCommerce has been configured. #26658 +* Fix - A deferred product sync is now scheduled when a product having a parent (e.g. a variation product) is deleted, not only when it's saved. #26629 +* Fix - Stock status of variable products that handle stock at the main product level is now appropriately updated when the product is saved. #26611 +* Fix - Discounted prices are no longer underlined in Twenty Twenty. #26609 +* Fix - Email link color clash. #26591 +* Fix - Remove HTML from error message. #26589 +* Fix - Fixed Tooltip flashing. #26558 +* Fix - Correctly displays the instructional option as default in the select box for picking a Country / Region on the checkout page. #26554 +* Fix - Default option "Select a country..." will now display accurately on Country select box in Cart shipping calculator. #26541 +* Fix - Fixed user capability required to view the order count indicator. #26338 +* Fix - The filtering widget now works as expected with variable products, displaying those products for which visible variations are available. #26260 +* Fix - Added a z-index to the remove button (x) to set the z-order of the element. #26202 +* Fix - Don't change the stock status of variations when bulk editing a variable product and leaving the "Stock status" selector as "No change". #26174 +* Fix - Remove new WP 5.5 meta box arrows from "Order data" and "Order items" meta boxes. #27173 +* 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 +* 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 +* Dev - Allow extend BACS accounts filter with order ID. #26961 +* Dev - Add npm run build:packages to npm run build. #26906 +* Dev - Add woocommerce_order_note_added action. #26846 +* Dev - Add tests for template cache. #26840 +* Dev - Add filter to allow disabling nocache headers. #26802 +* Dev - Introduce a dependency injection framework for the code in the src directory. #26731 +* Dev - Normalized parameters of woocommerce_product_importer_parsed_data filter. #26669 +* Dev - Introduced new WC_Product_CSV_Importer::get_formatting_callback() fixing a typo in the method name. #26668 +* Dev - Allow set "date_created" while creating orders via CRUD. #26567 +* Dev - Allow set a custom as order key using wc_generate_order_key(). #26566 +* Dev - Allow set order_key while creating an order via CRUD. #26565 +* Dev - Introduced woocommerce_product_cross_sells_products_heading filter. #26545 +* Dev - Added the removed_coupon_in_checkout event that is triggered on the Checkout page after a coupon is removed using .woocommerce-remove-coupon button. #26536 +* Dev - Remove no longer used styles from TwentyTwenty. #26516 +* Dev - Fix error message in wc_get_template() function. #26515 +* Dev - Add npm publish script for @woocommerce/e2e-environment. #26432 +* Dev - Make WC_Cart::display_prices_including_tax aware of tax display changes. #26400 +* Dev - Deprecated WC_Legacy_Cart::tax_display_cart in favor of WC_Cart:: get_tax_price_display_mode(). #26400 +* Dev - Add an optional $render_variations argument to in WC_Product_Variable::get_available_variation() in order to allow plugins to avoid performance bottlenecks. #26303 +* 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 + +**REST API 1.0.11** +* 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 +* 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 + +**WooCommerce Admin 1.4.0** +* Enhancement - Move the WooCommerce > Coupons dashboard menu item to Marketing > Coupons. #4786 +* Fix - Installation of child theme zip files from the store setup wizard. #4852 +* Fix - Center the skip link on the theme selection step. #4847 +* Fix - Removed item "profiler" from the menu. #4851 +* Fix - PHP notices when hosts block certain WP scripts. #4856 +* Fix - Remove new WP 5.5 meta box arrows in the shipping banner. #4914 +* Fix - Allow revisiting of the payments task. #4918 +* Fix - Use of Jetpack autoloader. #4920 +* Fix - Only show WCPay task in US based stores. #4899 +* 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 - Change account required text on biz step in onboarding wizard. #4909 +* Dev - Add the experimental resolver to WCA data package. #4862 +* Dev - Fix linter errors. #4904 + +**WooCommerce Blocks 3.0.0** +* Build - Updated the automattic/jetpack-autoloader package to the 2.0 branch. #2847 +* Enhancement - Add support for the Bank Transfer (BACS) payment method in the Checkout block. #2821 +* Enhancement - Several improvements to make Credit Card input fields display more consistent across different themes and viewport sizes. #2869 +* Enhancement - Cart and Checkout blocks show a notification for products on backorder. #2833 +* Enhancement - Chip styles of the Filter Products by Attribute and Active Filters have been updated to give a more consistent experience. #2765 +* Enhancement - Add protection for rogue filters on order queries when executing cleanup draft orders logic. #2874 +* Enhancement - Extend payment gateway extension API so gateways (payment methods) can dynamically disable (hide), based on checkout or order data (such as cart items or shipping method). For example, Cash on Delivery can limit availability to specific shipping methods only. #2840 [DN] +* Enhancement - Support Cash on Delivery core payment gateway in the Checkout block. #2831 +* Performance - Don't load shortcode Cart and Checkout scripts when using the blocks. #2842 +* Performance - Scripts only relevant to the frontend side of blocks are no longer loaded in the editor. #2788 +* Performance - Lazy Loading Atomic Components. #2777 +* Pefactor - Remove dashicon classes. #2848 + +**WooCommerce Blocks 3.1.0** +* Fix - Missing permissions_callback arg in StoreApi route definitions. #2926 +* 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 + [See changelog for all versions](https://raw.githubusercontent.com/woocommerce/woocommerce/master/CHANGELOG.txt). == Upgrade Notice == diff --git a/src/Autoloader.php b/src/Autoloader.php index b67eafa4f76..53f802e31fb 100644 --- a/src/Autoloader.php +++ b/src/Autoloader.php @@ -1,8 +1,6 @@ get_stock_managed_by_id(); - $rows[ $managed_by_id ] = isset( $rows[ $managed_by_id ] ) ? $rows[ $managed_by_id ] + $item->get_quantity() : $item->get_quantity(); + $managed_by_id = $product->get_stock_managed_by_id(); + + /** + * Filter order item quantity. + * + * @param int|float $quantity Quantity. + * @param WC_Order $order Order data. + * @param WC_Order_Item_Product $item Order item data. + */ + $item_quantity = apply_filters( 'woocommerce_order_item_quantity', $item->get_quantity(), $order, $item ); + + $rows[ $managed_by_id ] = isset( $rows[ $managed_by_id ] ) ? $rows[ $managed_by_id ] + $item_quantity : $item_quantity; } if ( ! empty( $rows ) ) { diff --git a/src/Checkout/Helpers/ReserveStockException.php b/src/Checkout/Helpers/ReserveStockException.php index e84409b5f1d..0122ff96ec0 100644 --- a/src/Checkout/Helpers/ReserveStockException.php +++ b/src/Checkout/Helpers/ReserveStockException.php @@ -1,8 +1,6 @@ '\\Automattic\\WooCommerce\\Blocks\\Package', - 'woocommerce-rest-api' => '\\Automattic\\WooCommerce\\RestApi\\Package', 'woocommerce-admin' => '\\Automattic\\WooCommerce\\Admin\\Composer\\Package', ); diff --git a/src/Proxies/ActionsProxy.php b/src/Proxies/ActionsProxy.php index fc058951c7e..6fe4173f643 100644 --- a/src/Proxies/ActionsProxy.php +++ b/src/Proxies/ActionsProxy.php @@ -1,8 +1,6 @@ { return /.\.js$/.test( filepath ); }; @@ -55,9 +44,8 @@ const isJsFile = ( filepath ) => { * @return {string} Build path */ function getBuildPath( file, buildFolder ) { - const pkgName = getPackageName( file ); - const pkgSrcPath = path.resolve( PACKAGES_DIR, pkgName, SRC_DIR ); - const pkgBuildPath = path.resolve( PACKAGES_DIR, pkgName, buildFolder ); + const pkgSrcPath = path.resolve( PACKAGE_DIR, SRC_DIR ); + const pkgBuildPath = path.resolve( PACKAGE_DIR, buildFolder ); const relativeToSrcPath = path.relative( pkgSrcPath, file ); return path.resolve( pkgBuildPath, relativeToSrcPath ); } @@ -121,9 +109,9 @@ function buildJsFileFor( file, silent, environment ) { if ( ! silent ) { process.stdout.write( chalk.green( ' \u2022 ' ) + - path.relative( PACKAGES_DIR, file ) + + path.relative( PACKAGE_DIR, file ) + chalk.green( ' \u21D2 ' ) + - path.relative( PACKAGES_DIR, destPath ) + + path.relative( PACKAGE_DIR, destPath ) + '\n' ); } @@ -136,6 +124,15 @@ function buildJsFileFor( file, silent, environment ) { */ function buildPackage( packagePath ) { const srcDir = path.resolve( packagePath, SRC_DIR ); + + let packageName; + try { + packageName = require( path.resolve( PACKAGE_DIR, 'package.json' ) ).name; + } catch ( e ) { + packageName = PACKAGE_DIR.split( path.sep ).pop(); + } + process.stdout.write( chalk.inverse( `>> Building package: ${ packageName }\n` ) ); + const jsFiles = glob.sync( `${ srcDir }/**/*.js`, { ignore: [ `${ srcDir }/**/test/**/*.js`, @@ -144,8 +141,6 @@ function buildPackage( packagePath ) { nodir: true, } ); - process.stdout.write( `${ path.basename( packagePath ) }\n` ); - // Build js files individually. jsFiles.forEach( ( file ) => buildJsFile( file, true ) ); @@ -157,7 +152,5 @@ const files = process.argv.slice( 2 ); if ( files.length ) { buildFiles( files ); } else { - process.stdout.write( chalk.inverse( '>> Building packages \n' ) ); - getPackages().forEach( buildPackage ); - process.stdout.write( '\n' ); + buildPackage( PACKAGE_DIR ); } diff --git a/tests/e2e/bin/get-packages.js b/tests/e2e/bin/get-packages.js deleted file mode 100644 index dc6eaeb0ea8..00000000000 --- a/tests/e2e/bin/get-packages.js +++ /dev/null @@ -1,83 +0,0 @@ -/** - * External dependencies - */ -const fs = require( 'fs' ); -const path = require( 'path' ); -const { overEvery, compact, includes, negate } = require( 'lodash' ); - -/** - * Absolute path to packages directory. - * - * @type {string} - */ -const PACKAGES_DIR = path.resolve( __dirname, '../' ); - -const { - /** - * Comma-separated string of packages to include in build. - * - * @type {string} - */ - INCLUDE_PACKAGES, - - /** - * Comma-separated string of packages to exclude from build. - * - * @type {string} - */ - EXCLUDE_PACKAGES, -} = process.env; - -/** - * Given a comma-separated string, returns a filter function which returns true - * if the item is contained within as a comma-separated entry. - * - * @param {Function} filterFn Filter function to call with item to test. - * @param {string} list Comma-separated list of items. - * - * @return {Function} Filter function. - */ -const createCommaSeparatedFilter = ( filterFn, list ) => { - const listItems = list.split( ',' ); - return ( item ) => filterFn( listItems, item ); -}; - -/** - * Returns true if the given base file name for a file within the packages - * directory is itself a directory. - * - * @param {string} file Packages directory file. - * - * @return {boolean} Whether file is a directory. - */ -function isDirectory( file ) { - return fs.lstatSync( path.resolve( PACKAGES_DIR, file ) ).isDirectory(); -} - -/** - * Filter predicate, returning true if the given base file name is to be - * included in the build. - * - * @param {string} pkg File base name to test. - * - * @return {boolean} Whether to include file in build. - */ -const filterPackages = overEvery( compact( [ - isDirectory, - INCLUDE_PACKAGES && createCommaSeparatedFilter( includes, INCLUDE_PACKAGES ), - EXCLUDE_PACKAGES && createCommaSeparatedFilter( negate( includes ), EXCLUDE_PACKAGES ), -] ) ); - -/** - * Returns the absolute path of all WordPress packages - * - * @return {Array} Package paths - */ -function getPackages() { - return fs - .readdirSync( PACKAGES_DIR ) - .filter( filterPackages ) - .map( ( file ) => path.resolve( PACKAGES_DIR, file ) ); -} - -module.exports = getPackages; diff --git a/tests/e2e/docker/initialize.sh b/tests/e2e/docker/initialize.sh index f04484a8947..86426ca19df 100755 --- a/tests/e2e/docker/initialize.sh +++ b/tests/e2e/docker/initialize.sh @@ -5,3 +5,6 @@ echo "Initializing WooCommerce E2E" wp plugin install woocommerce --activate wp theme install twentynineteen --activate wp user create customer customer@woocommercecoree2etestsuite.com --user_pass=password --role=customer --path=/var/www/html + +# we cannot create API keys for the API, so we using basic auth, this plugin allows that. +wp plugin install https://github.com/WP-API/Basic-Auth/archive/master.zip --activate diff --git a/tests/e2e/env/package.json b/tests/e2e/env/package.json index 88f262614d4..65f35c27fe0 100644 --- a/tests/e2e/env/package.json +++ b/tests/e2e/env/package.json @@ -41,6 +41,10 @@ "access": "public" }, "scripts": { + "clean": "rm -rf ./build ./build-module", + "compile": "node ./../bin/build.js", + "build": "npm run clean && npm run compile", + "prepare": "npm run build", "docker:up": "./bin/docker-compose.js up", "docker:down": "./bin/docker-compose.js down", "docker:clear-all": "docker rmi --force $(docker images -q)", diff --git a/tests/e2e/factories/.eslintignore b/tests/e2e/factories/.eslintignore new file mode 100644 index 00000000000..25bd998eae8 --- /dev/null +++ b/tests/e2e/factories/.eslintignore @@ -0,0 +1,8 @@ +/dist/ +/node_modules +.eslintrc.js +.gitignore +jest.config.js +package.json +package-lock.json +tsconfig.json diff --git a/tests/e2e/factories/.eslintrc.js b/tests/e2e/factories/.eslintrc.js new file mode 100644 index 00000000000..fc56fef7cf8 --- /dev/null +++ b/tests/e2e/factories/.eslintrc.js @@ -0,0 +1,31 @@ +module.exports = { + parser: '@typescript-eslint/parser', + env: { + 'jest/globals': true + }, + ignorePatterns: [ + 'dist/', + 'node_modules/' + ], + rules: { + 'no-unused-vars': 'off', + 'no-dupe-class-members': 'off', + }, + extends: [ + 'plugin:@wordpress/eslint-plugin/recommended-with-formatting' + ], + overrides: [ + { + 'files': [ '**/*.ts' ] + }, + { + 'files': [ + '**/*.spec.ts', + '**/*.test.ts' + ], + 'rules': { + 'no-console': 'off', + } + } + ] +} diff --git a/tests/e2e/factories/.gitignore b/tests/e2e/factories/.gitignore new file mode 100644 index 00000000000..1f578929131 --- /dev/null +++ b/tests/e2e/factories/.gitignore @@ -0,0 +1,18 @@ + +# Editors +project.xml +project.properties +/nbproject/private/ +.buildpath +.project +.settings* +.idea +.vscode +*.sublime-project +*.sublime-workspace +.sublimelinterrc + +# Build Artifacts +/node_modules/ +/dist/ +tsconfig.tsbuildinfo diff --git a/tests/e2e/factories/README.md b/tests/e2e/factories/README.md new file mode 100644 index 00000000000..fd1498a8cd4 --- /dev/null +++ b/tests/e2e/factories/README.md @@ -0,0 +1,58 @@ +# Model Factories + +A simple interface for generating models of different types. + +## Installation + +``bash +npm install @woocommerce/model-factories --save-dev +`` + +## Usage + +Consumers of this package should rely on an instance of `ModelRegistry` to access the factories. +Here is an example of how to initialize and use the package to generate a simple product: + +```javascript +import { + AdapterTypes, + initializeUsingBasicAuth, + ModelRegistry, + registerSimpleProduct, + SimpleProduct +} from '@woocommerce/model-factories'; + +// The ModelRegistry instance is where all of the factories and adapters are stored in an easy-to-access way. +const modelRegistry = new ModelRegistry() + +// Call the register functions to add a kind of factory to the model registry. +// This will also add any adapters we've created for the factory, allowing it +// to be created on the server. +registerSimpleProduct( modelRegistry ); + +// Before you can use the included API adapter you need to initialize it using one of the utility methods. +// If you do not initialize the API adapters they will not be able to make requests to the API. +// Note that these utility functions only set up adapters that have been registered already +// and so further calls to `registeryXXX` functions will have adapters that aren't ready. +initializeUsingBasicAuth( modelRegistry, 'https://test.test/wp-json', 'admin', 'password' ); +initializeUsingOAuth( modelRegistry, 'https://test.test/wp-json', 'consumer_key', 'consumer_secret' ); + +// In order to actually create the models on the server, each registered factory must have an adapter set. +// You can do this on a per-factory basis using +modelRegistry.changeFactoryAdapter( SimpleProduct, AdapterTypes.API ); +// You can do this to all factories registered using +modelRegistry.changeAllFactoryAdapters( AdapterTypes.API ); + +// Once all of the initialization has been taken care of you can create models! +// Any fields that are not defined will be filled out by random data. +const product = await modelRegistry.getFactory( SimpleProduct ).create( { name: 'Test Product' } ); +// You can now access the ID of the created model using `product.id`! + +// You can also create models in bulk! +const poducts = await modelRegistry.getFactory( SimpleProduct ).createList( 5 ); +// You now have an array of products to work with! +``` + +## Custom Models + +## Custom Adapters diff --git a/tests/e2e/factories/jest.config.js b/tests/e2e/factories/jest.config.js new file mode 100644 index 00000000000..0595d52172f --- /dev/null +++ b/tests/e2e/factories/jest.config.js @@ -0,0 +1,5 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testPathIgnorePatterns: [ '/node_modules/', '/dist/' ], +}; diff --git a/tests/e2e/factories/package-lock.json b/tests/e2e/factories/package-lock.json new file mode 100644 index 00000000000..2039d4c6bec --- /dev/null +++ b/tests/e2e/factories/package-lock.json @@ -0,0 +1,4985 @@ +{ + "name": "@woocommerce/model-factories", + "version": "0.1.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@babel/code-frame": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "dev": true, + "requires": { + "@babel/highlight": "^7.10.4" + } + }, + "@babel/core": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.10.4.tgz", + "integrity": "sha512-3A0tS0HWpy4XujGc7QtOIHTeNwUgWaZc/WuS5YQrfhU67jnVmsD6OGPc1AKHH0LJHQICGncy3+YUjIhVlfDdcA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/generator": "^7.10.4", + "@babel/helper-module-transforms": "^7.10.4", + "@babel/helpers": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/traverse": "^7.10.4", + "@babel/types": "^7.10.4", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.1", + "json5": "^2.1.2", + "lodash": "^4.17.13", + "resolve": "^1.3.2", + "semver": "^5.4.1", + "source-map": "^0.5.0" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "@babel/generator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.10.4.tgz", + "integrity": "sha512-toLIHUIAgcQygFZRAQcsLQV3CBuX6yOIru1kJk/qqqvcRmZrYe6WavZTSG+bB8MxhnL9YPf+pKQfuiP161q7ng==", + "dev": true, + "requires": { + "@babel/types": "^7.10.4", + "jsesc": "^2.5.1", + "lodash": "^4.17.13", + "source-map": "^0.5.0" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "@babel/helper-function-name": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", + "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", + "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", + "dev": true, + "requires": { + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.10.4.tgz", + "integrity": "sha512-m5j85pK/KZhuSdM/8cHUABQTAslV47OjfIB9Cc7P+PvlAoBzdb79BGNfw8RhT5Mq3p+xGd0ZfAKixbrUZx0C7A==", + "dev": true, + "requires": { + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-module-imports": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.10.4.tgz", + "integrity": "sha512-nEQJHqYavI217oD9+s5MUBzk6x1IlvoS9WTPfgG43CbMEeStE0v+r+TucWdx8KFGowPGvyOkDT9+7DHedIDnVw==", + "dev": true, + "requires": { + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-module-transforms": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.10.4.tgz", + "integrity": "sha512-Er2FQX0oa3nV7eM1o0tNCTx7izmQtwAQsIiaLRWtavAAEcskb0XJ5OjJbVrYXWOTr8om921Scabn4/tzlx7j1Q==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.10.4", + "@babel/helper-replace-supers": "^7.10.4", + "@babel/helper-simple-access": "^7.10.4", + "@babel/helper-split-export-declaration": "^7.10.4", + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4", + "lodash": "^4.17.13" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.4.tgz", + "integrity": "sha512-n3UGKY4VXwXThEiKrgRAoVPBMqeoPgHVqiHZOanAJCG9nQUL2pLRQirUzl0ioKclHGpGqRgIOkgcIJaIWLpygg==", + "dev": true, + "requires": { + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", + "dev": true + }, + "@babel/helper-replace-supers": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.10.4.tgz", + "integrity": "sha512-sPxZfFXocEymYTdVK1UNmFPBN+Hv5mJkLPsYWwGBxZAxaWfFu+xqp7b6qWD0yjNuNL2VKc6L5M18tOXUP7NU0A==", + "dev": true, + "requires": { + "@babel/helper-member-expression-to-functions": "^7.10.4", + "@babel/helper-optimise-call-expression": "^7.10.4", + "@babel/traverse": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-simple-access": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.10.4.tgz", + "integrity": "sha512-0fMy72ej/VEvF8ULmX6yb5MtHG4uH4Dbd6I/aHDb/JVg0bbivwt9Wg+h3uMvX+QSFtwr5MeItvazbrc4jtRAXw==", + "dev": true, + "requires": { + "@babel/template": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.10.4.tgz", + "integrity": "sha512-pySBTeoUff56fL5CBU2hWm9TesA4r/rOkI9DyJLvvgz09MB9YtfIYe3iBriVaYNaPe+Alua0vBIOVOLs2buWhg==", + "dev": true, + "requires": { + "@babel/types": "^7.10.4" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", + "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", + "dev": true + }, + "@babel/helpers": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.10.4.tgz", + "integrity": "sha512-L2gX/XeUONeEbI78dXSrJzGdz4GQ+ZTA/aazfUsFaWjSe95kiCuOZ5HsXvkiw3iwF+mFHSRUfJU8t6YavocdXA==", + "dev": true, + "requires": { + "@babel/template": "^7.10.4", + "@babel/traverse": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/highlight": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", + "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@babel/parser": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.10.4.tgz", + "integrity": "sha512-8jHII4hf+YVDsskTF6WuMB3X4Eh+PsUkC2ljq22so5rHvH+T8BzyL94VOdyFLNR8tBSVXOTbNHOKpR4TfRxVtA==", + "dev": true + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-class-properties": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.10.4.tgz", + "integrity": "sha512-GCSBF7iUle6rNugfURwNmCGG3Z/2+opxAMLs1nND4bhEG5PuxTIggDBoeYYSujAlLtsupzOHYJQgPS3pivwXIA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/runtime-corejs3": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.10.4.tgz", + "integrity": "sha512-BFlgP2SoLO9HJX9WBwN67gHWMBhDX/eDz64Jajd6mR/UAUzqrNMm99d4qHnVaKscAElZoFiPv+JpR/Siud5lXw==", + "dev": true, + "requires": { + "core-js-pure": "^3.0.0", + "regenerator-runtime": "^0.13.4" + } + }, + "@babel/template": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", + "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4" + } + }, + "@babel/traverse": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.10.4.tgz", + "integrity": "sha512-aSy7p5THgSYm4YyxNGz6jZpXf+Ok40QF3aA2LyIONkDHpAcJzDUqlCKXv6peqYUs2gmic849C/t2HKw2a2K20Q==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/generator": "^7.10.4", + "@babel/helper-function-name": "^7.10.4", + "@babel/helper-split-export-declaration": "^7.10.4", + "@babel/parser": "^7.10.4", + "@babel/types": "^7.10.4", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.13" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@babel/types": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.10.4.tgz", + "integrity": "sha512-UTCFOxC3FsFHb7lkRMVvgLzaRVamXuAs2Tz4wajva4WxtVY82eZeaUBtC2Zt95FU9TiznuC0Zk35tsim8jeVpg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.10.4", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + }, + "@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "@cnakazawa/watch": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@cnakazawa/watch/-/watch-1.0.4.tgz", + "integrity": "sha512-v9kIhKwjeZThiWrLmj0y17CWoyddASLj9O2yvbZkbvw/N3rWOYy9zkV66ursAoVr0mV15bL8g0c4QZUE6cdDoQ==", + "dev": true, + "requires": { + "exec-sh": "^0.3.2", + "minimist": "^1.2.0" + } + }, + "@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + } + }, + "@istanbuljs/schema": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz", + "integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==", + "dev": true + }, + "@jest/console": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-25.5.0.tgz", + "integrity": "sha512-T48kZa6MK1Y6k4b89sexwmSF4YLeZS/Udqg3Jj3jG/cHH+N/sLFCEoXEDMOKugJQ9FxPN1osxIknvKkxt6MKyw==", + "dev": true, + "requires": { + "@jest/types": "^25.5.0", + "chalk": "^3.0.0", + "jest-message-util": "^25.5.0", + "jest-util": "^25.5.0", + "slash": "^3.0.0" + } + }, + "@jest/core": { + "version": "25.5.4", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-25.5.4.tgz", + "integrity": "sha512-3uSo7laYxF00Dg/DMgbn4xMJKmDdWvZnf89n8Xj/5/AeQ2dOQmn6b6Hkj/MleyzZWXpwv+WSdYWl4cLsy2JsoA==", + "dev": true, + "requires": { + "@jest/console": "^25.5.0", + "@jest/reporters": "^25.5.1", + "@jest/test-result": "^25.5.0", + "@jest/transform": "^25.5.1", + "@jest/types": "^25.5.0", + "ansi-escapes": "^4.2.1", + "chalk": "^3.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.4", + "jest-changed-files": "^25.5.0", + "jest-config": "^25.5.4", + "jest-haste-map": "^25.5.1", + "jest-message-util": "^25.5.0", + "jest-regex-util": "^25.2.6", + "jest-resolve": "^25.5.1", + "jest-resolve-dependencies": "^25.5.4", + "jest-runner": "^25.5.4", + "jest-runtime": "^25.5.4", + "jest-snapshot": "^25.5.1", + "jest-util": "^25.5.0", + "jest-validate": "^25.5.0", + "jest-watcher": "^25.5.0", + "micromatch": "^4.0.2", + "p-each-series": "^2.1.0", + "realpath-native": "^2.0.0", + "rimraf": "^3.0.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "@jest/environment": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-25.5.0.tgz", + "integrity": "sha512-U2VXPEqL07E/V7pSZMSQCvV5Ea4lqOlT+0ZFijl/i316cRMHvZ4qC+jBdryd+lmRetjQo0YIQr6cVPNxxK87mA==", + "dev": true, + "requires": { + "@jest/fake-timers": "^25.5.0", + "@jest/types": "^25.5.0", + "jest-mock": "^25.5.0" + } + }, + "@jest/fake-timers": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-25.5.0.tgz", + "integrity": "sha512-9y2+uGnESw/oyOI3eww9yaxdZyHq7XvprfP/eeoCsjqKYts2yRlsHS/SgjPDV8FyMfn2nbMy8YzUk6nyvdLOpQ==", + "dev": true, + "requires": { + "@jest/types": "^25.5.0", + "jest-message-util": "^25.5.0", + "jest-mock": "^25.5.0", + "jest-util": "^25.5.0", + "lolex": "^5.0.0" + } + }, + "@jest/globals": { + "version": "25.5.2", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-25.5.2.tgz", + "integrity": "sha512-AgAS/Ny7Q2RCIj5kZ+0MuKM1wbF0WMLxbCVl/GOMoCNbODRdJ541IxJ98xnZdVSZXivKpJlNPIWa3QmY0l4CXA==", + "dev": true, + "requires": { + "@jest/environment": "^25.5.0", + "@jest/types": "^25.5.0", + "expect": "^25.5.0" + } + }, + "@jest/reporters": { + "version": "25.5.1", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-25.5.1.tgz", + "integrity": "sha512-3jbd8pPDTuhYJ7vqiHXbSwTJQNavczPs+f1kRprRDxETeE3u6srJ+f0NPuwvOmk+lmunZzPkYWIFZDLHQPkviw==", + "dev": true, + "requires": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^25.5.0", + "@jest/test-result": "^25.5.0", + "@jest/transform": "^25.5.1", + "@jest/types": "^25.5.0", + "chalk": "^3.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.2", + "graceful-fs": "^4.2.4", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^4.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.0.2", + "jest-haste-map": "^25.5.1", + "jest-resolve": "^25.5.1", + "jest-util": "^25.5.0", + "jest-worker": "^25.5.0", + "node-notifier": "^6.0.0", + "slash": "^3.0.0", + "source-map": "^0.6.0", + "string-length": "^3.1.0", + "terminal-link": "^2.0.0", + "v8-to-istanbul": "^4.1.3" + } + }, + "@jest/source-map": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-25.5.0.tgz", + "integrity": "sha512-eIGx0xN12yVpMcPaVpjXPnn3N30QGJCJQSkEDUt9x1fI1Gdvb07Ml6K5iN2hG7NmMP6FDmtPEssE3z6doOYUwQ==", + "dev": true, + "requires": { + "callsites": "^3.0.0", + "graceful-fs": "^4.2.4", + "source-map": "^0.6.0" + } + }, + "@jest/test-result": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-25.5.0.tgz", + "integrity": "sha512-oV+hPJgXN7IQf/fHWkcS99y0smKLU2czLBJ9WA0jHITLst58HpQMtzSYxzaBvYc6U5U6jfoMthqsUlUlbRXs0A==", + "dev": true, + "requires": { + "@jest/console": "^25.5.0", + "@jest/types": "^25.5.0", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + } + }, + "@jest/test-sequencer": { + "version": "25.5.4", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-25.5.4.tgz", + "integrity": "sha512-pTJGEkSeg1EkCO2YWq6hbFvKNXk8ejqlxiOg1jBNLnWrgXOkdY6UmqZpwGFXNnRt9B8nO1uWMzLLZ4eCmhkPNA==", + "dev": true, + "requires": { + "@jest/test-result": "^25.5.0", + "graceful-fs": "^4.2.4", + "jest-haste-map": "^25.5.1", + "jest-runner": "^25.5.4", + "jest-runtime": "^25.5.4" + } + }, + "@jest/transform": { + "version": "25.5.1", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-25.5.1.tgz", + "integrity": "sha512-Y8CEoVwXb4QwA6Y/9uDkn0Xfz0finGkieuV0xkdF9UtZGJeLukD5nLkaVrVsODB1ojRWlaoD0AJZpVHCSnJEvg==", + "dev": true, + "requires": { + "@babel/core": "^7.1.0", + "@jest/types": "^25.5.0", + "babel-plugin-istanbul": "^6.0.0", + "chalk": "^3.0.0", + "convert-source-map": "^1.4.0", + "fast-json-stable-stringify": "^2.0.0", + "graceful-fs": "^4.2.4", + "jest-haste-map": "^25.5.1", + "jest-regex-util": "^25.2.6", + "jest-util": "^25.5.0", + "micromatch": "^4.0.2", + "pirates": "^4.0.1", + "realpath-native": "^2.0.0", + "slash": "^3.0.0", + "source-map": "^0.6.1", + "write-file-atomic": "^3.0.0" + } + }, + "@jest/types": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz", + "integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^15.0.0", + "chalk": "^3.0.0" + } + }, + "@sinonjs/commons": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.0.tgz", + "integrity": "sha512-wEj54PfsZ5jGSwMX68G8ZXFawcSglQSXqCftWX3ec8MDUzQdHgcKvw97awHbY0efQEL5iKUOAmmVtoYgmrSG4Q==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@types/babel__core": { + "version": "7.1.9", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.9.tgz", + "integrity": "sha512-sY2RsIJ5rpER1u3/aQ8OFSI7qGIy8o1NEEbgb2UaJcvOtXOMpd39ko723NBpjQFg9SIX7TXtjejZVGeIMLhoOw==", + "dev": true, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "@types/babel__generator": { + "version": "7.6.1", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.1.tgz", + "integrity": "sha512-bBKm+2VPJcMRVwNhxKu8W+5/zT7pwNEqeokFOmbvVSqGzFneNxYcEBro9Ac7/N9tlsaPYnZLK8J1LWKkMsLAew==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@types/babel__template": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.0.2.tgz", + "integrity": "sha512-/K6zCpeW7Imzgab2bLkLEbz0+1JlFSrUMdw7KoIIu+IUdu51GWaBZpd3y1VXGVXzynvGa4DaIaxNZHiON3GXUg==", + "dev": true, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@types/babel__traverse": { + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.0.12.tgz", + "integrity": "sha512-t4CoEokHTfcyfb4hUaF9oOHu9RmmNWnm1CP0YmMqOOfClKascOmvlEM736vlqeScuGvBDsHkf8R2INd4DWreQA==", + "dev": true, + "requires": { + "@babel/types": "^7.3.0" + } + }, + "@types/color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", + "dev": true + }, + "@types/create-hmac": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@types/create-hmac/-/create-hmac-1.1.0.tgz", + "integrity": "sha512-BNYNdzdhOZZQWCOpwvIll3FSvgo3e55Y2M6s/jOY6TuOCwqt3cLmQsK4tSmJ5fayDot8EG4k3+hcZagfww9JlQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/faker": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/faker/-/faker-4.1.12.tgz", + "integrity": "sha512-0MEyzJrLLs1WaOCx9ULK6FzdCSj2EuxdSP9kvuxxdBEGujZYUOZ4vkPXdgu3dhyg/pOdn7VCatelYX7k0YShlA==", + "dev": true + }, + "@types/graceful-fs": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.3.tgz", + "integrity": "sha512-AiHRaEB50LQg0pZmm659vNBb9f4SJ0qrAnteuzhSeAUcJKxoYgEnprg/83kppCnc2zvtCKbdZry1a5pVY3lOTQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/istanbul-lib-coverage": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", + "integrity": "sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw==", + "dev": true + }, + "@types/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*" + } + }, + "@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "@types/jest": { + "version": "25.2.1", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-25.2.1.tgz", + "integrity": "sha512-msra1bCaAeEdkSyA0CZ6gW1ukMIvZ5YoJkdXw/qhQdsuuDlFTcEUrUw8CLCPt2rVRUfXlClVvK2gvPs9IokZaA==", + "dev": true, + "requires": { + "jest-diff": "^25.2.1", + "pretty-format": "^25.2.1" + } + }, + "@types/moxios": { + "version": "0.4.9", + "resolved": "https://registry.npmjs.org/@types/moxios/-/moxios-0.4.9.tgz", + "integrity": "sha512-Sd1b24QRW2N194j2LEDPQAZK1h0TBtpN+2EIH+rERCgm38qm14JZwC7NlpE7n3jULhlCIPZBG8uNcbjF8KcCaQ==", + "dev": true, + "requires": { + "axios": "^0.19.0" + } + }, + "@types/node": { + "version": "13.13.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.5.tgz", + "integrity": "sha512-3ySmiBYJPqgjiHA7oEaIo2Rzz0HrOZ7yrNO5HWyaE5q0lQ3BppDZ3N53Miz8bw2I7gh1/zir2MGVZBvpb1zq9g==", + "dev": true + }, + "@types/normalize-package-data": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==", + "dev": true + }, + "@types/prettier": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-1.19.1.tgz", + "integrity": "sha512-5qOlnZscTn4xxM5MeGXAMOsIOIKIbh9e85zJWfBRVPlRMEVawzoPhINYbRGkBZCI8LxvBe7tJCdWiarA99OZfQ==", + "dev": true + }, + "@types/stack-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", + "integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==", + "dev": true + }, + "@types/yargs": { + "version": "15.0.5", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.5.tgz", + "integrity": "sha512-Dk/IDOPtOgubt/IaevIUbTgV7doaKkoorvOyYM2CMwuDyP89bekI7H4xLIwunNYiK9jhCkmc6pUrJk3cj2AB9w==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-15.0.0.tgz", + "integrity": "sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw==", + "dev": true + }, + "abab": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.3.tgz", + "integrity": "sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg==", + "dev": true + }, + "acorn": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.3.1.tgz", + "integrity": "sha512-tLc0wSnatxAQHVHUapaHdz72pi9KUyHjq5KyHjGg9Y8Ifdc79pTh2XvI6I1/chZbnM7QtNKzh66ooDogPZSleA==", + "dev": true + }, + "acorn-globals": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.4.tgz", + "integrity": "sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A==", + "dev": true, + "requires": { + "acorn": "^6.0.1", + "acorn-walk": "^6.0.1" + }, + "dependencies": { + "acorn": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", + "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==", + "dev": true + } + } + }, + "acorn-walk": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz", + "integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==", + "dev": true + }, + "ajv": { + "version": "6.12.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz", + "integrity": "sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-escapes": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", + "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", + "dev": true, + "requires": { + "type-fest": "^0.11.0" + }, + "dependencies": { + "type-fest": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", + "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", + "dev": true + } + } + }, + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", + "dev": true + }, + "array-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", + "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "dev": true, + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "dev": true + }, + "astral-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", + "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "dev": true + }, + "aws4": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.0.tgz", + "integrity": "sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA==", + "dev": true + }, + "axios": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", + "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==", + "requires": { + "follow-redirects": "1.5.10" + } + }, + "babel-jest": { + "version": "25.5.1", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-25.5.1.tgz", + "integrity": "sha512-9dA9+GmMjIzgPnYtkhBg73gOo/RHqPmLruP3BaGL4KEX3Dwz6pI8auSN8G8+iuEG90+GSswyKvslN+JYSaacaQ==", + "dev": true, + "requires": { + "@jest/transform": "^25.5.1", + "@jest/types": "^25.5.0", + "@types/babel__core": "^7.1.7", + "babel-plugin-istanbul": "^6.0.0", + "babel-preset-jest": "^25.5.0", + "chalk": "^3.0.0", + "graceful-fs": "^4.2.4", + "slash": "^3.0.0" + } + }, + "babel-plugin-istanbul": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz", + "integrity": "sha512-AF55rZXpe7trmEylbaE1Gv54wn6rwU03aptvRoVIGP8YykoSxqdVLV1TfwflBCE/QtHmqtP8SWlTENqbK8GCSQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^4.0.0", + "test-exclude": "^6.0.0" + } + }, + "babel-plugin-jest-hoist": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-25.5.0.tgz", + "integrity": "sha512-u+/W+WAjMlvoocYGTwthAiQSxDcJAyHpQ6oWlHdFZaaN+Rlk8Q7iiwDPg2lN/FyJtAYnKjFxbn7xus4HCFkg5g==", + "dev": true, + "requires": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__traverse": "^7.0.6" + } + }, + "babel-preset-current-node-syntax": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-0.1.3.tgz", + "integrity": "sha512-uyexu1sVwcdFnyq9o8UQYsXwXflIh8LvrF5+cKrYam93ned1CStffB3+BEcsxGSgagoA3GEyjDqO4a/58hyPYQ==", + "dev": true, + "requires": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + } + }, + "babel-preset-jest": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-25.5.0.tgz", + "integrity": "sha512-8ZczygctQkBU+63DtSOKGh7tFL0CeCuz+1ieud9lJ1WPQ9O6A1a/r+LGn6Y705PA6whHQ3T1XuB/PmpfNYf8Fw==", + "dev": true, + "requires": { + "babel-plugin-jest-hoist": "^25.5.0", + "babel-preset-current-node-syntax": "^0.1.2" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "dev": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "browser-process-hrtime": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", + "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==", + "dev": true + }, + "browser-resolve": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.3.tgz", + "integrity": "sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==", + "dev": true, + "requires": { + "resolve": "1.1.7" + }, + "dependencies": { + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "dev": true + } + } + }, + "bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "requires": { + "fast-json-stable-stringify": "2.x" + } + }, + "bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "requires": { + "node-int64": "^0.4.0" + } + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "capture-exit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz", + "integrity": "sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g==", + "dev": true, + "requires": { + "rsvp": "^4.8.4" + } + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "dev": true + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "dev": true + }, + "cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true + }, + "collect-v8-coverage": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", + "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==", + "dev": true + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "dev": true, + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "convert-source-map": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", + "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + } + } + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", + "dev": true + }, + "core-js-pure": { + "version": "3.6.5", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.6.5.tgz", + "integrity": "sha512-lacdXOimsiD0QyNf9BC/mxivNJ/ybBGJXQFKzRekp1WTHoVUWsUHEn+2T8GJAzzIhyOuXA+gOxCVN3l+5PLPUA==", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "requires": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "requires": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "cssom": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", + "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==", + "dev": true + }, + "cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "dev": true, + "requires": { + "cssom": "~0.3.6" + }, + "dependencies": { + "cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true + } + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "data-urls": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.1.0.tgz", + "integrity": "sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==", + "dev": true, + "requires": { + "abab": "^2.0.0", + "whatwg-mimetype": "^2.2.0", + "whatwg-url": "^7.0.0" + } + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "decamelize": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-3.2.0.tgz", + "integrity": "sha512-4TgkVUsmmu7oCSyGBm5FvfMoACuoh9EOidm7V5/J2X2djAwwt57qb3F2KMP2ITqODTCSwb+YRV+0Zqrv18k/hw==", + "dev": true, + "requires": { + "xregexp": "^4.2.4" + } + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "deepmerge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", + "dev": true + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true + }, + "diff-sequences": { + "version": "25.2.6", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-25.2.6.tgz", + "integrity": "sha512-Hq8o7+6GaZeoFjtpgvRBUknSXNeJiCx7V9Fr94ZMljNiCr9n9L8H8aJqgWOQiDDGdyn29fRNcDdRVJ5fdyihfg==", + "dev": true + }, + "domexception": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", + "integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==", + "dev": true, + "requires": { + "webidl-conversions": "^4.0.2" + } + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "dev": true, + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "dev": true, + "requires": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "exec-sh": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.4.tgz", + "integrity": "sha512-sEFIkc61v75sWeOe72qyrqg2Qg0OuLESziUDk/O/z2qgS15y2gWVFrI6f2Qn/qw/0/NCfCEsmNA4zOjkwEZT1A==", + "dev": true + }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", + "dev": true + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "expect": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-25.5.0.tgz", + "integrity": "sha512-w7KAXo0+6qqZZhovCaBVPSIqQp7/UTcx4M9uKt2m6pd2VB1voyC8JizLRqeEqud3AAVP02g+hbErDu5gu64tlA==", + "dev": true, + "requires": { + "@jest/types": "^25.5.0", + "ansi-styles": "^4.0.0", + "jest-get-type": "^25.2.6", + "jest-matcher-utils": "^25.5.0", + "jest-message-util": "^25.5.0", + "jest-regex-util": "^25.2.6" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "dev": true + }, + "faker": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/faker/-/faker-4.1.0.tgz", + "integrity": "sha1-HkW7vsxndLPBlfrSg1EJxtdIzD8=" + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "fb-watchman": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.1.tgz", + "integrity": "sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg==", + "dev": true, + "requires": { + "bser": "2.1.1" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "fishery": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fishery/-/fishery-1.0.0.tgz", + "integrity": "sha512-DLQtxcSPlLQYY6J0tL/dl7DfPhrULHCAO6fFDGnrXqA830J6AW124fHarYOLnfvcSXNBEooBS/g65N/HecQYjQ==", + "requires": { + "lodash.merge": "^4.6.2" + } + }, + "follow-redirects": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", + "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", + "requires": { + "debug": "=3.1.0" + } + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "dev": true, + "requires": { + "map-cache": "^0.2.2" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fsevents": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", + "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", + "dev": true, + "optional": true + }, + "gensync": { + "version": "1.0.0-beta.1", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.1.tgz", + "integrity": "sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==", + "dev": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", + "dev": true + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true + }, + "growly": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", + "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", + "dev": true, + "optional": true + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "dev": true + }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "dev": true, + "requires": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "dev": true, + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "hash-base": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", + "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", + "requires": { + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + } + }, + "hosted-git-info": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", + "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", + "dev": true + }, + "html-encoding-sniffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz", + "integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==", + "dev": true, + "requires": { + "whatwg-encoding": "^1.0.1" + } + }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "human-signals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", + "dev": true + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "import-local": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.2.tgz", + "integrity": "sha512-vjL3+w0oulAVZ0hBHnxa/Nm5TAurf9YLQJDhqRZyqb+VKGOB6LU8t9H1Nr5CIo16vh9XfJTOoHwU0B71S557gA==", + "dev": true, + "requires": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ip-regex": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", + "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=", + "dev": true + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "dev": true, + "requires": { + "ci-info": "^2.0.0" + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "is-docker": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.0.0.tgz", + "integrity": "sha512-pJEdRugimx4fBMra5z2/5iRdZ63OhYV0vr0Dwm5+xtW4D1FvRkB8hamMIhnWfyJeDdyr/aa7BDyNbtG38VxgoQ==", + "dev": true, + "optional": true + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true + }, + "is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "optional": true, + "requires": { + "is-docker": "^2.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true + }, + "istanbul-lib-coverage": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", + "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", + "dev": true, + "requires": { + "@babel/core": "^7.7.5", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" + } + }, + "istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + } + }, + "istanbul-lib-source-maps": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz", + "integrity": "sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "istanbul-reports": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.2.tgz", + "integrity": "sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "jest": { + "version": "25.5.4", + "resolved": "https://registry.npmjs.org/jest/-/jest-25.5.4.tgz", + "integrity": "sha512-hHFJROBTqZahnO+X+PMtT6G2/ztqAZJveGqz//FnWWHurizkD05PQGzRZOhF3XP6z7SJmL+5tCfW8qV06JypwQ==", + "dev": true, + "requires": { + "@jest/core": "^25.5.4", + "import-local": "^3.0.2", + "jest-cli": "^25.5.4" + }, + "dependencies": { + "jest-cli": { + "version": "25.5.4", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-25.5.4.tgz", + "integrity": "sha512-rG8uJkIiOUpnREh1768/N3n27Cm+xPFkSNFO91tgg+8o2rXeVLStz+vkXkGr4UtzH6t1SNbjwoiswd7p4AhHTw==", + "dev": true, + "requires": { + "@jest/core": "^25.5.4", + "@jest/test-result": "^25.5.0", + "@jest/types": "^25.5.0", + "chalk": "^3.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.4", + "import-local": "^3.0.2", + "is-ci": "^2.0.0", + "jest-config": "^25.5.4", + "jest-util": "^25.5.0", + "jest-validate": "^25.5.0", + "prompts": "^2.0.1", + "realpath-native": "^2.0.0", + "yargs": "^15.3.1" + } + } + } + }, + "jest-changed-files": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-25.5.0.tgz", + "integrity": "sha512-EOw9QEqapsDT7mKF162m8HFzRPbmP8qJQny6ldVOdOVBz3ACgPm/1nAn5fPQ/NDaYhX/AHkrGwwkCncpAVSXcw==", + "dev": true, + "requires": { + "@jest/types": "^25.5.0", + "execa": "^3.2.0", + "throat": "^5.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "execa": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-3.4.0.tgz", + "integrity": "sha512-r9vdGQk4bmCuK1yKQu1KTwcT2zwfWdbdaXfCtAh+5nU/4fSX+JAb7vZGvI5naJrQlvONrEB20jeruESI69530g==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "p-finally": "^2.0.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + } + }, + "get-stream": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", + "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", + "dev": true + }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "requires": { + "path-key": "^3.0.0" + } + }, + "p-finally": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-2.0.1.tgz", + "integrity": "sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw==", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + } + } + }, + "jest-config": { + "version": "25.5.4", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-25.5.4.tgz", + "integrity": "sha512-SZwR91SwcdK6bz7Gco8qL7YY2sx8tFJYzvg216DLihTWf+LKY/DoJXpM9nTzYakSyfblbqeU48p/p7Jzy05Atg==", + "dev": true, + "requires": { + "@babel/core": "^7.1.0", + "@jest/test-sequencer": "^25.5.4", + "@jest/types": "^25.5.0", + "babel-jest": "^25.5.1", + "chalk": "^3.0.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.1", + "graceful-fs": "^4.2.4", + "jest-environment-jsdom": "^25.5.0", + "jest-environment-node": "^25.5.0", + "jest-get-type": "^25.2.6", + "jest-jasmine2": "^25.5.4", + "jest-regex-util": "^25.2.6", + "jest-resolve": "^25.5.1", + "jest-util": "^25.5.0", + "jest-validate": "^25.5.0", + "micromatch": "^4.0.2", + "pretty-format": "^25.5.0", + "realpath-native": "^2.0.0" + } + }, + "jest-diff": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-25.5.0.tgz", + "integrity": "sha512-z1kygetuPiREYdNIumRpAHY6RXiGmp70YHptjdaxTWGmA085W3iCnXNx0DhflK3vwrKmrRWyY1wUpkPMVxMK7A==", + "dev": true, + "requires": { + "chalk": "^3.0.0", + "diff-sequences": "^25.2.6", + "jest-get-type": "^25.2.6", + "pretty-format": "^25.5.0" + } + }, + "jest-docblock": { + "version": "25.3.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-25.3.0.tgz", + "integrity": "sha512-aktF0kCar8+zxRHxQZwxMy70stc9R1mOmrLsT5VO3pIT0uzGRSDAXxSlz4NqQWpuLjPpuMhPRl7H+5FRsvIQAg==", + "dev": true, + "requires": { + "detect-newline": "^3.0.0" + } + }, + "jest-each": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-25.5.0.tgz", + "integrity": "sha512-QBogUxna3D8vtiItvn54xXde7+vuzqRrEeaw8r1s+1TG9eZLVJE5ZkKoSUlqFwRjnlaA4hyKGiu9OlkFIuKnjA==", + "dev": true, + "requires": { + "@jest/types": "^25.5.0", + "chalk": "^3.0.0", + "jest-get-type": "^25.2.6", + "jest-util": "^25.5.0", + "pretty-format": "^25.5.0" + } + }, + "jest-environment-jsdom": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-25.5.0.tgz", + "integrity": "sha512-7Jr02ydaq4jaWMZLY+Skn8wL5nVIYpWvmeatOHL3tOcV3Zw8sjnPpx+ZdeBfc457p8jCR9J6YCc+Lga0oIy62A==", + "dev": true, + "requires": { + "@jest/environment": "^25.5.0", + "@jest/fake-timers": "^25.5.0", + "@jest/types": "^25.5.0", + "jest-mock": "^25.5.0", + "jest-util": "^25.5.0", + "jsdom": "^15.2.1" + } + }, + "jest-environment-node": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-25.5.0.tgz", + "integrity": "sha512-iuxK6rQR2En9EID+2k+IBs5fCFd919gVVK5BeND82fYeLWPqvRcFNPKu9+gxTwfB5XwBGBvZ0HFQa+cHtIoslA==", + "dev": true, + "requires": { + "@jest/environment": "^25.5.0", + "@jest/fake-timers": "^25.5.0", + "@jest/types": "^25.5.0", + "jest-mock": "^25.5.0", + "jest-util": "^25.5.0", + "semver": "^6.3.0" + } + }, + "jest-get-type": { + "version": "25.2.6", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-25.2.6.tgz", + "integrity": "sha512-DxjtyzOHjObRM+sM1knti6or+eOgcGU4xVSb2HNP1TqO4ahsT+rqZg+nyqHWJSvWgKC5cG3QjGFBqxLghiF/Ig==", + "dev": true + }, + "jest-haste-map": { + "version": "25.5.1", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-25.5.1.tgz", + "integrity": "sha512-dddgh9UZjV7SCDQUrQ+5t9yy8iEgKc1AKqZR9YDww8xsVOtzPQSMVLDChc21+g29oTRexb9/B0bIlZL+sWmvAQ==", + "dev": true, + "requires": { + "@jest/types": "^25.5.0", + "@types/graceful-fs": "^4.1.2", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "fsevents": "^2.1.2", + "graceful-fs": "^4.2.4", + "jest-serializer": "^25.5.0", + "jest-util": "^25.5.0", + "jest-worker": "^25.5.0", + "micromatch": "^4.0.2", + "sane": "^4.0.3", + "walker": "^1.0.7", + "which": "^2.0.2" + } + }, + "jest-jasmine2": { + "version": "25.5.4", + "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-25.5.4.tgz", + "integrity": "sha512-9acbWEfbmS8UpdcfqnDO+uBUgKa/9hcRh983IHdM+pKmJPL77G0sWAAK0V0kr5LK3a8cSBfkFSoncXwQlRZfkQ==", + "dev": true, + "requires": { + "@babel/traverse": "^7.1.0", + "@jest/environment": "^25.5.0", + "@jest/source-map": "^25.5.0", + "@jest/test-result": "^25.5.0", + "@jest/types": "^25.5.0", + "chalk": "^3.0.0", + "co": "^4.6.0", + "expect": "^25.5.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^25.5.0", + "jest-matcher-utils": "^25.5.0", + "jest-message-util": "^25.5.0", + "jest-runtime": "^25.5.4", + "jest-snapshot": "^25.5.1", + "jest-util": "^25.5.0", + "pretty-format": "^25.5.0", + "throat": "^5.0.0" + } + }, + "jest-leak-detector": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-25.5.0.tgz", + "integrity": "sha512-rV7JdLsanS8OkdDpZtgBf61L5xZ4NnYLBq72r6ldxahJWWczZjXawRsoHyXzibM5ed7C2QRjpp6ypgwGdKyoVA==", + "dev": true, + "requires": { + "jest-get-type": "^25.2.6", + "pretty-format": "^25.5.0" + } + }, + "jest-matcher-utils": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-25.5.0.tgz", + "integrity": "sha512-VWI269+9JS5cpndnpCwm7dy7JtGQT30UHfrnM3mXl22gHGt/b7NkjBqXfbhZ8V4B7ANUsjK18PlSBmG0YH7gjw==", + "dev": true, + "requires": { + "chalk": "^3.0.0", + "jest-diff": "^25.5.0", + "jest-get-type": "^25.2.6", + "pretty-format": "^25.5.0" + } + }, + "jest-message-util": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-25.5.0.tgz", + "integrity": "sha512-ezddz3YCT/LT0SKAmylVyWWIGYoKHOFOFXx3/nA4m794lfVUskMcwhip6vTgdVrOtYdjeQeis2ypzes9mZb4EA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@jest/types": "^25.5.0", + "@types/stack-utils": "^1.0.1", + "chalk": "^3.0.0", + "graceful-fs": "^4.2.4", + "micromatch": "^4.0.2", + "slash": "^3.0.0", + "stack-utils": "^1.0.1" + } + }, + "jest-mock": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-25.5.0.tgz", + "integrity": "sha512-eXWuTV8mKzp/ovHc5+3USJMYsTBhyQ+5A1Mak35dey/RG8GlM4YWVylZuGgVXinaW6tpvk/RSecmF37FKUlpXA==", + "dev": true, + "requires": { + "@jest/types": "^25.5.0" + } + }, + "jest-pnp-resolver": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz", + "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==", + "dev": true + }, + "jest-regex-util": { + "version": "25.2.6", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-25.2.6.tgz", + "integrity": "sha512-KQqf7a0NrtCkYmZZzodPftn7fL1cq3GQAFVMn5Hg8uKx/fIenLEobNanUxb7abQ1sjADHBseG/2FGpsv/wr+Qw==", + "dev": true + }, + "jest-resolve": { + "version": "25.5.1", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-25.5.1.tgz", + "integrity": "sha512-Hc09hYch5aWdtejsUZhA+vSzcotf7fajSlPA6EZPE1RmPBAD39XtJhvHWFStid58iit4IPDLI/Da4cwdDmAHiQ==", + "dev": true, + "requires": { + "@jest/types": "^25.5.0", + "browser-resolve": "^1.11.3", + "chalk": "^3.0.0", + "graceful-fs": "^4.2.4", + "jest-pnp-resolver": "^1.2.1", + "read-pkg-up": "^7.0.1", + "realpath-native": "^2.0.0", + "resolve": "^1.17.0", + "slash": "^3.0.0" + } + }, + "jest-resolve-dependencies": { + "version": "25.5.4", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-25.5.4.tgz", + "integrity": "sha512-yFmbPd+DAQjJQg88HveObcGBA32nqNZ02fjYmtL16t1xw9bAttSn5UGRRhzMHIQbsep7znWvAvnD4kDqOFM0Uw==", + "dev": true, + "requires": { + "@jest/types": "^25.5.0", + "jest-regex-util": "^25.2.6", + "jest-snapshot": "^25.5.1" + } + }, + "jest-runner": { + "version": "25.5.4", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-25.5.4.tgz", + "integrity": "sha512-V/2R7fKZo6blP8E9BL9vJ8aTU4TH2beuqGNxHbxi6t14XzTb+x90B3FRgdvuHm41GY8ch4xxvf0ATH4hdpjTqg==", + "dev": true, + "requires": { + "@jest/console": "^25.5.0", + "@jest/environment": "^25.5.0", + "@jest/test-result": "^25.5.0", + "@jest/types": "^25.5.0", + "chalk": "^3.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.4", + "jest-config": "^25.5.4", + "jest-docblock": "^25.3.0", + "jest-haste-map": "^25.5.1", + "jest-jasmine2": "^25.5.4", + "jest-leak-detector": "^25.5.0", + "jest-message-util": "^25.5.0", + "jest-resolve": "^25.5.1", + "jest-runtime": "^25.5.4", + "jest-util": "^25.5.0", + "jest-worker": "^25.5.0", + "source-map-support": "^0.5.6", + "throat": "^5.0.0" + } + }, + "jest-runtime": { + "version": "25.5.4", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-25.5.4.tgz", + "integrity": "sha512-RWTt8LeWh3GvjYtASH2eezkc8AehVoWKK20udV6n3/gC87wlTbE1kIA+opCvNWyyPeBs6ptYsc6nyHUb1GlUVQ==", + "dev": true, + "requires": { + "@jest/console": "^25.5.0", + "@jest/environment": "^25.5.0", + "@jest/globals": "^25.5.2", + "@jest/source-map": "^25.5.0", + "@jest/test-result": "^25.5.0", + "@jest/transform": "^25.5.1", + "@jest/types": "^25.5.0", + "@types/yargs": "^15.0.0", + "chalk": "^3.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.4", + "jest-config": "^25.5.4", + "jest-haste-map": "^25.5.1", + "jest-message-util": "^25.5.0", + "jest-mock": "^25.5.0", + "jest-regex-util": "^25.2.6", + "jest-resolve": "^25.5.1", + "jest-snapshot": "^25.5.1", + "jest-util": "^25.5.0", + "jest-validate": "^25.5.0", + "realpath-native": "^2.0.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0", + "yargs": "^15.3.1" + } + }, + "jest-serializer": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-25.5.0.tgz", + "integrity": "sha512-LxD8fY1lByomEPflwur9o4e2a5twSQ7TaVNLlFUuToIdoJuBt8tzHfCsZ42Ok6LkKXWzFWf3AGmheuLAA7LcCA==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.4" + } + }, + "jest-snapshot": { + "version": "25.5.1", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-25.5.1.tgz", + "integrity": "sha512-C02JE1TUe64p2v1auUJ2ze5vcuv32tkv9PyhEb318e8XOKF7MOyXdJ7kdjbvrp3ChPLU2usI7Rjxs97Dj5P0uQ==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0", + "@jest/types": "^25.5.0", + "@types/prettier": "^1.19.0", + "chalk": "^3.0.0", + "expect": "^25.5.0", + "graceful-fs": "^4.2.4", + "jest-diff": "^25.5.0", + "jest-get-type": "^25.2.6", + "jest-matcher-utils": "^25.5.0", + "jest-message-util": "^25.5.0", + "jest-resolve": "^25.5.1", + "make-dir": "^3.0.0", + "natural-compare": "^1.4.0", + "pretty-format": "^25.5.0", + "semver": "^6.3.0" + } + }, + "jest-util": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-25.5.0.tgz", + "integrity": "sha512-KVlX+WWg1zUTB9ktvhsg2PXZVdkI1NBevOJSkTKYAyXyH4QSvh+Lay/e/v+bmaFfrkfx43xD8QTfgobzlEXdIA==", + "dev": true, + "requires": { + "@jest/types": "^25.5.0", + "chalk": "^3.0.0", + "graceful-fs": "^4.2.4", + "is-ci": "^2.0.0", + "make-dir": "^3.0.0" + } + }, + "jest-validate": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-25.5.0.tgz", + "integrity": "sha512-okUFKqhZIpo3jDdtUXUZ2LxGUZJIlfdYBvZb1aczzxrlyMlqdnnws9MOxezoLGhSaFc2XYaHNReNQfj5zPIWyQ==", + "dev": true, + "requires": { + "@jest/types": "^25.5.0", + "camelcase": "^5.3.1", + "chalk": "^3.0.0", + "jest-get-type": "^25.2.6", + "leven": "^3.1.0", + "pretty-format": "^25.5.0" + } + }, + "jest-watcher": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-25.5.0.tgz", + "integrity": "sha512-XrSfJnVASEl+5+bb51V0Q7WQx65dTSk7NL4yDdVjPnRNpM0hG+ncFmDYJo9O8jaSRcAitVbuVawyXCRoxGrT5Q==", + "dev": true, + "requires": { + "@jest/test-result": "^25.5.0", + "@jest/types": "^25.5.0", + "ansi-escapes": "^4.2.1", + "chalk": "^3.0.0", + "jest-util": "^25.5.0", + "string-length": "^3.1.0" + } + }, + "jest-worker": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-25.5.0.tgz", + "integrity": "sha512-/dsSmUkIy5EBGfv/IjjqmFxrNAUpBERfGs1oHROyD7yxjG/w+t0GOJDX8O1k32ySmd7+a5IhnJU2qQFcJ4n1vw==", + "dev": true, + "requires": { + "merge-stream": "^2.0.0", + "supports-color": "^7.0.0" + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", + "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true + }, + "jsdom": { + "version": "15.2.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-15.2.1.tgz", + "integrity": "sha512-fAl1W0/7T2G5vURSyxBzrJ1LSdQn6Tr5UX/xD4PXDx/PDgwygedfW6El/KIj3xJ7FU61TTYnc/l/B7P49Eqt6g==", + "dev": true, + "requires": { + "abab": "^2.0.0", + "acorn": "^7.1.0", + "acorn-globals": "^4.3.2", + "array-equal": "^1.0.0", + "cssom": "^0.4.1", + "cssstyle": "^2.0.0", + "data-urls": "^1.1.0", + "domexception": "^1.0.1", + "escodegen": "^1.11.1", + "html-encoding-sniffer": "^1.0.2", + "nwsapi": "^2.2.0", + "parse5": "5.1.0", + "pn": "^1.1.0", + "request": "^2.88.0", + "request-promise-native": "^1.0.7", + "saxes": "^3.1.9", + "symbol-tree": "^3.2.2", + "tough-cookie": "^3.0.1", + "w3c-hr-time": "^1.0.1", + "w3c-xmlserializer": "^1.1.2", + "webidl-conversions": "^4.0.2", + "whatwg-encoding": "^1.0.5", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^7.0.0", + "ws": "^7.0.0", + "xml-name-validator": "^3.0.0" + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, + "json5": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", + "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + }, + "kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true + }, + "leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "lines-and-columns": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", + "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=", + "dev": true + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "lodash": { + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", + "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", + "dev": true + }, + "lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", + "dev": true + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + }, + "lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", + "dev": true + }, + "lolex": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-5.1.2.tgz", + "integrity": "sha512-h4hmjAvHTmd+25JSwrtTIuwbKdwg5NzZVRMLn9saij4SZaepCrTCxPr35H/3bjwfMJtN+t3CX8672UIkglz28A==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0" + } + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + } + }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "makeerror": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz", + "integrity": "sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw=", + "dev": true, + "requires": { + "tmpl": "1.0.x" + } + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "dev": true + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "dev": true, + "requires": { + "object-visit": "^1.0.0" + } + }, + "md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "mime-db": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==", + "dev": true + }, + "mime-types": { + "version": "2.1.27", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", + "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", + "dev": true, + "requires": { + "mime-db": "1.44.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, + "mixin-deep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "dev": true, + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "moxios": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/moxios/-/moxios-0.4.0.tgz", + "integrity": "sha1-/A2ixlR31yXKa5Z51YNw7QxS9Ts=", + "dev": true + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + } + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=", + "dev": true + }, + "node-modules-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz", + "integrity": "sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=", + "dev": true + }, + "node-notifier": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-6.0.0.tgz", + "integrity": "sha512-SVfQ/wMw+DesunOm5cKqr6yDcvUTDl/yc97ybGHMrteNEY6oekXpNpS3lZwgLlwz0FLgHoiW28ZpmBHUDg37cw==", + "dev": true, + "optional": true, + "requires": { + "growly": "^1.3.0", + "is-wsl": "^2.1.1", + "semver": "^6.3.0", + "shellwords": "^0.1.1", + "which": "^1.3.1" + }, + "dependencies": { + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "optional": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "dev": true, + "requires": { + "path-key": "^2.0.0" + } + }, + "nwsapi": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", + "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==", + "dev": true + }, + "oauth-1.0a": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/oauth-1.0a/-/oauth-1.0a-2.2.6.tgz", + "integrity": "sha512-6bkxv3N4Gu5lty4viIcIAnq5GbxECviMBeKR3WX/q87SPQ8E8aursPZUtsXDnxCs787af09WPRBLqYrf/lwoYQ==" + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "dev": true + }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "dev": true, + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "dev": true, + "requires": { + "isobject": "^3.0.0" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", + "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "p-each-series": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-2.1.0.tgz", + "integrity": "sha512-ZuRs1miPT4HrjFa+9fRfOFXxGJfORgelKV9f9nNOWw2gl6gVsRaVDOQP0+MI0G0wGKns1Yacsu0GjOFbTK0JFQ==", + "dev": true + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "parse-json": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.0.0.tgz", + "integrity": "sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1", + "lines-and-columns": "^1.1.6" + } + }, + "parse5": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.0.tgz", + "integrity": "sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==", + "dev": true + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", + "dev": true + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "dev": true + }, + "picomatch": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", + "dev": true + }, + "pirates": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.1.tgz", + "integrity": "sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA==", + "dev": true, + "requires": { + "node-modules-regexp": "^1.0.0" + } + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + } + }, + "pn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", + "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==", + "dev": true + }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", + "dev": true + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "pretty-format": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.5.0.tgz", + "integrity": "sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ==", + "dev": true, + "requires": { + "@jest/types": "^25.5.0", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^16.12.0" + } + }, + "prompts": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.3.2.tgz", + "integrity": "sha512-Q06uKs2CkNYVID0VqwfAl9mipo99zkBv/n2JtWY89Yxa3ZabWSrs0e2KTudKVa3peLUvYXMefDqIleLPVUBZMA==", + "dev": true, + "requires": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.4" + } + }, + "psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", + "dev": true + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "dev": true + }, + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + }, + "read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "requires": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "dependencies": { + "type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true + } + } + }, + "read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "requires": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "realpath-native": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/realpath-native/-/realpath-native-2.0.0.tgz", + "integrity": "sha512-v1SEYUOXXdbBZK8ZuNgO4TBjamPsiSgcFr0aP+tEKpQZK8vooEUqV6nm6Cv502mX4NF2EfsnVqtNAHG+/6Ur1Q==", + "dev": true + }, + "regenerator-runtime": { + "version": "0.13.5", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz", + "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==", + "dev": true + }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + } + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", + "dev": true + }, + "repeat-element": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", + "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true + }, + "request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "dev": true, + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "dependencies": { + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + } + } + }, + "request-promise-core": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.3.tgz", + "integrity": "sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ==", + "dev": true, + "requires": { + "lodash": "^4.17.15" + } + }, + "request-promise-native": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.8.tgz", + "integrity": "sha512-dapwLGqkHtwL5AEbfenuzjTYg35Jd6KPytsC2/TLkVMz8rm+tNt72MGUWT1RP/aYawMpN6HqbNGBQaRcBtjQMQ==", + "dev": true, + "requires": { + "request-promise-core": "1.1.3", + "stealthy-require": "^1.1.1", + "tough-cookie": "^2.3.3" + }, + "dependencies": { + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + } + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "resolve": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", + "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + }, + "resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "requires": { + "resolve-from": "^5.0.0" + } + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", + "dev": true + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "rsvp": { + "version": "4.8.5", + "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", + "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==", + "dev": true + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "dev": true, + "requires": { + "ret": "~0.1.10" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "sane": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/sane/-/sane-4.1.0.tgz", + "integrity": "sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA==", + "dev": true, + "requires": { + "@cnakazawa/watch": "^1.0.3", + "anymatch": "^2.0.0", + "capture-exit": "^2.0.0", + "exec-sh": "^0.3.2", + "execa": "^1.0.0", + "fb-watchman": "^2.0.0", + "micromatch": "^3.1.4", + "minimist": "^1.1.1", + "walker": "~1.0.5" + }, + "dependencies": { + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dev": true, + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + } + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + } + } + }, + "saxes": { + "version": "3.1.11", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-3.1.11.tgz", + "integrity": "sha512-Ydydq3zC+WYDJK1+gRxRapLIED9PWeSuuS41wqyoRmzvhhh9nc+QQrVMKJYzJFULazeGhzSV0QleN2wD3boh2g==", + "dev": true, + "requires": { + "xmlchars": "^2.1.1" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "set-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "shellwords": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", + "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", + "dev": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", + "dev": true + }, + "sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, + "requires": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, + "requires": { + "kind-of": "^3.2.0" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-resolve": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", + "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", + "dev": true, + "requires": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "source-map-support": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", + "dev": true + }, + "spdx-correct": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", + "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", + "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", + "dev": true + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.0" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "dev": true, + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "stack-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.2.tgz", + "integrity": "sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA==", + "dev": true + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "dev": true, + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "stealthy-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", + "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=", + "dev": true + }, + "string-length": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-3.1.0.tgz", + "integrity": "sha512-Ttp5YvkGm5v9Ijagtaz1BnN+k9ObpvS0eIBblPMp2YWL8FBmi9qblQ9fexc2k/CXFgrTIteU3jAw3payCnwSTA==", + "dev": true, + "requires": { + "astral-regex": "^1.0.0", + "strip-ansi": "^5.2.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "dev": true + }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "supports-hyperlinks": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.1.0.tgz", + "integrity": "sha512-zoE5/e+dnEijk6ASB6/qrK+oYdm2do1hjoLWrqUC/8WEIW1gbxFcKuBof7sW8ArN6e+AYvsE8HBGiVRWL/F5CA==", + "dev": true, + "requires": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + } + }, + "symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true + }, + "terminal-link": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", + "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", + "dev": true, + "requires": { + "ansi-escapes": "^4.2.1", + "supports-hyperlinks": "^2.0.0" + } + }, + "test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + } + }, + "throat": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", + "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==", + "dev": true + }, + "tmpl": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", + "integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=", + "dev": true + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "tough-cookie": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz", + "integrity": "sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==", + "dev": true, + "requires": { + "ip-regex": "^2.1.0", + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, + "tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "ts-jest": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-25.5.0.tgz", + "integrity": "sha512-govrjbOk1UEzcJ5cX5k8X8IUtFuP3lp3mrF3ZuKtCdAOQzdeCM7qualhb/U8s8SWFwEDutOqfF5PLkJ+oaYD4w==", + "dev": true, + "requires": { + "bs-logger": "0.x", + "buffer-from": "1.x", + "fast-json-stable-stringify": "2.x", + "json5": "2.x", + "lodash.memoize": "4.x", + "make-error": "1.x", + "micromatch": "4.x", + "mkdirp": "0.x", + "semver": "6.x", + "yargs-parser": "18.x" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "dev": true + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + }, + "typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "requires": { + "is-typedarray": "^1.0.0" + } + }, + "typescript": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz", + "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==", + "dev": true + }, + "union-value": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + } + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "dev": true, + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "dev": true, + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", + "dev": true + } + } + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "dev": true + }, + "use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "dev": true + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "dev": true + }, + "v8-to-istanbul": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-4.1.4.tgz", + "integrity": "sha512-Rw6vJHj1mbdK8edjR7+zuJrpDtKIgNdAvTSAcpYfgMIw+u2dPDntD3dgN4XQFLU2/fvFQdzj+EeSGfd/jnY5fQ==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^1.6.0", + "source-map": "^0.7.3" + }, + "dependencies": { + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "dev": true + } + } + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "w3c-hr-time": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", + "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", + "dev": true, + "requires": { + "browser-process-hrtime": "^1.0.0" + } + }, + "w3c-xmlserializer": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-1.1.2.tgz", + "integrity": "sha512-p10l/ayESzrBMYWRID6xbuCKh2Fp77+sA0doRuGn4tTIMrrZVeqfpKjXHY+oDh3K4nLdPgNwMTVP6Vp4pvqbNg==", + "dev": true, + "requires": { + "domexception": "^1.0.1", + "webidl-conversions": "^4.0.2", + "xml-name-validator": "^3.0.0" + } + }, + "walker": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz", + "integrity": "sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=", + "dev": true, + "requires": { + "makeerror": "1.0.x" + } + }, + "webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true + }, + "whatwg-encoding": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", + "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", + "dev": true, + "requires": { + "iconv-lite": "0.4.24" + } + }, + "whatwg-mimetype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", + "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", + "dev": true + }, + "whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "dev": true, + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "ws": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.3.0.tgz", + "integrity": "sha512-iFtXzngZVXPGgpTlP1rBqsUK82p9tKqsWRPg5L56egiljujJT3vGAYnHANvFxBieXrTFavhzhxW52jnaWV+w2w==", + "dev": true + }, + "xml-name-validator": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", + "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", + "dev": true + }, + "xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true + }, + "xregexp": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.3.0.tgz", + "integrity": "sha512-7jXDIFXh5yJ/orPn4SXjuVrWWoi4Cr8jfV1eHv9CixKSbU+jY4mxfrBwAuDvupPNKpMUY+FeIqsVw/JLT9+B8g==", + "dev": true, + "requires": { + "@babel/runtime-corejs3": "^7.8.3" + } + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "dev": true + }, + "yargs": { + "version": "15.4.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.0.tgz", + "integrity": "sha512-D3fRFnZwLWp8jVAAhPZBsmeIHY8tTsb8ItV9KaAaopmC6wde2u6Yw29JBIZHXw14kgkRnYmDgmQU4FVMDlIsWw==", + "dev": true, + "requires": { + "cliui": "^6.0.0", + "decamelize": "^3.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + } + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "dependencies": { + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + } + } + } + } +} diff --git a/tests/e2e/factories/package.json b/tests/e2e/factories/package.json new file mode 100644 index 00000000000..924c48d8442 --- /dev/null +++ b/tests/e2e/factories/package.json @@ -0,0 +1,55 @@ +{ + "name": "@woocommerce/model-factories", + "version": "0.1.0", + "author": "Automattic", + "description": "A simple interface for generating models of different types.", + "homepage": "https://github.com/woocommerce/woocommerce/tree/master/tests/e2e/factories/README.md", + "repository": { + "type": "git", + "url": "https://github.com/woocommerce/woocommerce.git" + }, + "keywords": [ + "woocommerce", + "e2e" + ], + "license": "GPL-3.0+", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "/dist/", + "!*.tsbuildinfo", + "!*.spec.js", + "!*.spec.d.ts", + "!*.test.js", + "!*.test.d.ts" + ], + "sideEffects": false, + "scripts": { + "test": "jest", + "clean": "rm -rf ./dist ./tsconfig.tsbuildinfo", + "compile": "tsc -b", + "build": "npm run clean && npm run compile", + "prepare": "npm run build" + }, + "dependencies": { + "axios": "0.19.2", + "create-hmac": "1.1.7", + "faker": "4.1.0", + "fishery": "1.0.0", + "oauth-1.0a": "2.2.6" + }, + "devDependencies": { + "@types/create-hmac": "1.1.0", + "@types/faker": "4.1.12", + "@types/jest": "25.2.1", + "@types/moxios": "0.4.9", + "@types/node": "13.13.5", + "jest": "25.5.4", + "moxios": "0.4.0", + "ts-jest": "25.5.0", + "typescript": "3.8.3" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/tests/e2e/factories/src/framework/adapter.ts b/tests/e2e/factories/src/framework/adapter.ts new file mode 100644 index 00000000000..30f08a2d12d --- /dev/null +++ b/tests/e2e/factories/src/framework/adapter.ts @@ -0,0 +1,16 @@ +import { Model } from './model'; + +/** + * An interface for implementing adapters to create models. + */ +export interface Adapter { + /** + * Creates a model or array of models using a service.. + * + * @param {Model|Model[]} model The model or array of models to create. + * @return {Promise} Resolves to the created input model or array of models. + */ + create( model: T ): Promise; + create( model: T[] ): Promise; + create( model: T | T[] ): Promise | Promise; +} diff --git a/tests/e2e/factories/src/framework/api/api-adapter.spec.ts b/tests/e2e/factories/src/framework/api/api-adapter.spec.ts new file mode 100644 index 00000000000..117121b6fc0 --- /dev/null +++ b/tests/e2e/factories/src/framework/api/api-adapter.spec.ts @@ -0,0 +1,55 @@ +import { Model } from '../model'; +import { APIAdapter } from './api-adapter'; +import { SimpleProduct } from '../../models/simple-product'; +import { APIResponse, APIService } from './api-service'; + +class MockAPI implements APIService { + public get = jest.fn(); + public post = jest.fn(); + public put = jest.fn(); + public patch = jest.fn(); + public delete = jest.fn(); +} + +describe( 'APIModelCreator', () => { + let adapter: APIAdapter; + let mockService: MockAPI; + + beforeEach( () => { + adapter = new APIAdapter( '/wc/v3/product', () => 'test' ); + mockService = new MockAPI(); + adapter.setAPIService( mockService ); + } ); + + it( 'should create single instance', async () => { + mockService.post.mockReturnValueOnce( Promise.resolve( new APIResponse( 200, {}, { id: 1 } ) ) ); + + const result = await adapter.create( new SimpleProduct() ); + + expect( result ).toBeInstanceOf( SimpleProduct ); + expect( result.id ).toBe( 1 ); + expect( mockService.post.mock.calls[ 0 ][ 0 ] ).toBe( '/wc/v3/product' ); + expect( mockService.post.mock.calls[ 0 ][ 1 ] ).toBe( 'test' ); + } ); + + it( 'should create multiple instances', async () => { + mockService.post + .mockReturnValueOnce( Promise.resolve( new APIResponse( 200, {}, { id: 1 } ) ) ) + .mockReturnValueOnce( Promise.resolve( new APIResponse( 200, {}, { id: 2 } ) ) ) + .mockReturnValueOnce( Promise.resolve( new APIResponse( 200, {}, { id: 3 } ) ) ); + + const result = await adapter.create( [ new SimpleProduct(), new SimpleProduct(), new SimpleProduct() ] ); + + expect( result ).toBeInstanceOf( Array ); + expect( result ).toHaveLength( 3 ); + expect( result[ 0 ].id ).toBe( 1 ); + expect( result[ 1 ].id ).toBe( 2 ); + expect( result[ 2 ].id ).toBe( 3 ); + expect( mockService.post.mock.calls[ 0 ][ 0 ] ).toBe( '/wc/v3/product' ); + expect( mockService.post.mock.calls[ 0 ][ 1 ] ).toBe( 'test' ); + expect( mockService.post.mock.calls[ 1 ][ 0 ] ).toBe( '/wc/v3/product' ); + expect( mockService.post.mock.calls[ 1 ][ 1 ] ).toBe( 'test' ); + expect( mockService.post.mock.calls[ 2 ][ 0 ] ).toBe( '/wc/v3/product' ); + expect( mockService.post.mock.calls[ 2 ][ 1 ] ).toBe( 'test' ); + } ); +} ); diff --git a/tests/e2e/factories/src/framework/api/api-adapter.ts b/tests/e2e/factories/src/framework/api/api-adapter.ts new file mode 100644 index 00000000000..e12938323cc --- /dev/null +++ b/tests/e2e/factories/src/framework/api/api-adapter.ts @@ -0,0 +1,87 @@ +import { APIResponse, APIService } from './api-service'; +import { Model } from '../model'; +import { Adapter } from '../adapter'; + +/** + * A callback for transforming models into an API request body. + * + * @callback APITransformerFn + * @param {Model} model The model that we want to transform. + * @return {*} The structured request data for the API. + */ +export type APITransformerFn = ( model: T ) => any; + +/** + * A class used for creating data models using a supplied API endpoint. + */ +export class APIAdapter implements Adapter { + private readonly endpoint: string; + private readonly transformer: APITransformerFn; + private apiService: APIService | null; + + public constructor( endpoint: string, transformer: APITransformerFn ) { + this.endpoint = endpoint; + this.transformer = transformer; + this.apiService = null; + } + + /** + * Sets the API service that the adapter should use for creation actions. + * + * @param {APIService|null} service The new API service for the adapter to use. + */ + public setAPIService( service: APIService | null ): void { + this.apiService = service; + } + + /** + * Creates a model or array of models using the API service. + * + * @param {Model|Model[]} model The model or array of models to create. + * @return {Promise} Resolves to the created input model or array of models. + */ + public create( model: T ): Promise; + public create( model: T[] ): Promise; + public create( model: T | T[] ): Promise | Promise { + if ( ! this.apiService ) { + throw new Error( 'An API service must be registered for the adapter to work.' ); + } + + if ( Array.isArray( model ) ) { + return this.createList( model ); + } + + return this.createSingle( model ); + } + + /** + * Creates a single model using the API service. + * + * @param {Model} model The model to create. + * @return {Promise} Resolves to the created input model. + */ + private async createSingle( model: T ): Promise { + return this.apiService!.post( + this.endpoint, + this.transformer( model ), + ).then( ( data: APIResponse ) => { + model.setID( data.data.id ); + return model; + } ); + } + + /** + * Creates an array of models using the API service. + * + * @param {Model[]} models The array of models to create. + * @return {Promise} Resolves to the array of created input models. + */ + private async createList( models: T[] ): Promise { + const promises: Promise[] = []; + for ( const model of models ) { + promises.push( this.createSingle( model ) ); + } + + return Promise.all( promises ); + } +} diff --git a/tests/e2e/factories/src/framework/api/api-service.ts b/tests/e2e/factories/src/framework/api/api-service.ts new file mode 100644 index 00000000000..19191f6af46 --- /dev/null +++ b/tests/e2e/factories/src/framework/api/api-service.ts @@ -0,0 +1,100 @@ +/** + * A structured response from the API. + */ +export class APIResponse { + public readonly status: number; + public readonly headers: any; + public readonly data: T; + + public constructor( status: number, headers: any, data: T ) { + this.status = status; + this.headers = headers; + this.data = data; + } +} + +/** + * A structured error from the API. + */ +export class APIError { + public readonly code: string; + public readonly message: string; + public readonly data: any; + + public constructor( code: string, message: string, data: any ) { + this.code = code; + this.message = message; + this.data = data; + } +} + +/** + * Checks whether or not an APIResponse contains an error. + * + * @param {APIResponse} response The response to evaluate. + */ +export function isAPIError( response: APIResponse ): response is APIResponse { + return response.status < 200 || response.status >= 400; +} + +/** + * An interface for implementing services to make calls against the API. + */ +export interface APIService { + /** + * Performs a GET request against the WordPress API. + * + * @param {string} endpoint The API endpoint we should query. + * @param {*} params Any parameters that should be passed in the request. + * @return {Promise} Resolves to an APIResponse and throws an APIResponse containing an APIError. + */ + get( + endpoint: string, + params?: any + ): Promise>; + + /** + * Performs a POST request against the WordPress API. + * + * @param {string} endpoint The API endpoint we should query. + * @param {*} data Any parameters that should be passed in the request. + * @return {Promise} Resolves to an APIResponse and throws an APIResponse containing an APIError. + */ + post( + endpoint: string, + data?: any + ): Promise>; + + /** + * Performs a PUT request against the WordPress API. + * + * @param {string} endpoint The API endpoint we should query. + * @param {*} data Any parameters that should be passed in the request. + * @return {Promise} Resolves to an APIResponse and throws an APIResponse containing an APIError. + */ + put( endpoint: string, data?: any ): Promise>; + + /** + * Performs a PATCH request against the WordPress API. + * + * @param {string} endpoint The API endpoint we should query. + * @param {*} data Any parameters that should be passed in the request. + * @return {Promise} Resolves to an APIResponse and throws an APIResponse containing an APIError. + */ + patch( + endpoint: string, + data?: any + ): Promise>; + + /** + * Performs a DELETE request against the WordPress API. + * + * @param {string} endpoint The API endpoint we should query. + * @param {*} data Any parameters that should be passed in the request. + * @return {Promise} Resolves to an APIResponse and throws an APIResponse containing an APIError. + */ + delete( + endpoint: string, + data?: any + ): Promise>; +} diff --git a/tests/e2e/factories/src/framework/api/axios/axios-api-service.spec.ts b/tests/e2e/factories/src/framework/api/axios/axios-api-service.spec.ts new file mode 100644 index 00000000000..fbf911afa13 --- /dev/null +++ b/tests/e2e/factories/src/framework/api/axios/axios-api-service.spec.ts @@ -0,0 +1,57 @@ +import moxios from 'moxios'; +import { APIResponse } from '../api-service'; +import { AxiosAPIService } from './axios-api-service'; + +describe( 'AxiosAPIService', () => { + let apiClient: AxiosAPIService; + + beforeEach( () => { + moxios.install(); + } ); + + afterEach( () => { + moxios.uninstall(); + } ); + + it( 'should add OAuth interceptors', async () => { + apiClient = AxiosAPIService.createUsingOAuth( + 'http://test.test/wp-json/', + 'consumer_key', + 'consumer_secret', + ); + + moxios.stubOnce( 'GET', '/wc/v2/product', { + status: 200, + headers: { + 'Content-Type': 'application/json', + }, + responseText: JSON.stringify( { test: 'value' } ), + } ); + + const response = await apiClient.get( '/wc/v2/product' ); + expect( response ).toBeInstanceOf( APIResponse ); + + const request = moxios.requests.mostRecent(); + expect( request.headers ).toHaveProperty( 'Authorization' ); + expect( request.headers.Authorization ).toMatch( /^OAuth/ ); + } ); + + it( 'should add basic auth interceptors', async () => { + apiClient = AxiosAPIService.createUsingBasicAuth( 'http://test.test/wp-json/', 'test', 'pass' ); + + moxios.stubOnce( 'GET', '/wc/v2/product', { + status: 200, + headers: { + 'Content-Type': 'application/json', + }, + responseText: JSON.stringify( { test: 'value' } ), + } ); + + const response = await apiClient.get( '/wc/v2/product' ); + expect( response ).toBeInstanceOf( APIResponse ); + + const request = moxios.requests.mostRecent(); + expect( request.headers ).toHaveProperty( 'Authorization' ); + expect( request.headers.Authorization ).toMatch( /^Basic/ ); + } ); +} ); diff --git a/tests/e2e/factories/src/framework/api/axios/axios-api-service.ts b/tests/e2e/factories/src/framework/api/axios/axios-api-service.ts new file mode 100644 index 00000000000..b5782518886 --- /dev/null +++ b/tests/e2e/factories/src/framework/api/axios/axios-api-service.ts @@ -0,0 +1,127 @@ +import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'; +import { APIResponse, APIService } from '../api-service'; +import { AxiosOAuthInterceptor } from './axios-oauth-interceptor'; +import { AxiosInterceptor } from './axios-interceptor'; +import { AxiosResponseInterceptor } from './axios-response-interceptor'; + +/** + * An API service implementation that uses Axios to make requests to the WordPress API. + */ +export class AxiosAPIService implements APIService { + private readonly client: AxiosInstance; + private readonly interceptors: AxiosInterceptor[]; + + public constructor( config: AxiosRequestConfig, interceptors: AxiosInterceptor[] = [] ) { + this.client = axios.create( config ); + this.interceptors = interceptors; + for ( const interceptor of this.interceptors ) { + interceptor.start( this.client ); + } + } + + /** + * Creates a new Axios API Service using OAuth 1.0a one-legged authentication. + * + * @param {string} apiURL The base URL for the API requests to be sent. + * @param {string} consumerKey The OAuth consumer key. + * @param {string} consumerSecret The OAuth consumer secret. + * @return {AxiosAPIService} The created service. + */ + public static createUsingOAuth( apiURL: string, consumerKey: string, consumerSecret: string ): AxiosAPIService { + return new AxiosAPIService( + { baseURL: apiURL }, + [ + new AxiosOAuthInterceptor( consumerKey, consumerSecret ), + new AxiosResponseInterceptor(), + ], + ); + } + + /** + * Creates a new Axios API Service using basic authentication. + * + * @param {string} apiURL The base URL for the API requests to be sent. + * @param {string} username The username for authentication. + * @param {string} password The password for authentication. + * @return {AxiosAPIService} The created service. + */ + public static createUsingBasicAuth( apiURL: string, username: string, password: string ): AxiosAPIService { + return new AxiosAPIService( + { + baseURL: apiURL, + auth: { username, password }, + }, + [ new AxiosResponseInterceptor() ], + ); + } + + /** + * Performs a GET request against the WordPress API. + * + * @param {string} endpoint The API endpoint we should query. + * @param {*} params Any parameters that should be passed in the request. + * @return {Promise} Resolves to an APIResponse and throws an APIResponse containing an APIError. + */ + public get( + endpoint: string, + params?: any, + ): Promise> { + return this.client.get( endpoint, { params } ); + } + + /** + * Performs a POST request against the WordPress API. + * + * @param {string} endpoint The API endpoint we should query. + * @param {*} data Any parameters that should be passed in the request. + * @return {Promise} Resolves to an APIResponse and throws an APIResponse containing an APIError. + */ + public post( + endpoint: string, + data?: any, + ): Promise> { + return this.client.post( endpoint, data ); + } + + /** + * Performs a PUT request against the WordPress API. + * + * @param {string} endpoint The API endpoint we should query. + * @param {*} data Any parameters that should be passed in the request. + * @return {Promise} Resolves to an APIResponse and throws an APIResponse containing an APIError. + */ + public put( + endpoint: string, + data?: any, + ): Promise> { + return this.client.put( endpoint, data ); + } + + /** + * Performs a PATCH request against the WordPress API. + * + * @param {string} endpoint The API endpoint we should query. + * @param {*} data Any parameters that should be passed in the request. + * @return {Promise} Resolves to an APIResponse and throws an APIResponse containing an APIError. + */ + public patch( + endpoint: string, + data?: any, + ): Promise> { + return this.client.patch( endpoint, data ); + } + + /** + * Performs a DELETE request against the WordPress API. + * + * @param {string} endpoint The API endpoint we should query. + * @param {*} data Any parameters that should be passed in the request. + * @return {Promise} Resolves to an APIResponse and throws an APIResponse containing an APIError. + */ + public delete( + endpoint: string, + data?: any, + ): Promise> { + return this.client.delete( endpoint, { data } ); + } +} diff --git a/tests/e2e/factories/src/framework/api/axios/axios-interceptor.ts b/tests/e2e/factories/src/framework/api/axios/axios-interceptor.ts new file mode 100644 index 00000000000..2d2ac3c493a --- /dev/null +++ b/tests/e2e/factories/src/framework/api/axios/axios-interceptor.ts @@ -0,0 +1,73 @@ +import { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'; + +type ActiveInterceptor = { + client: AxiosInstance; + requestInterceptorID: number; + responseInterceptorID: number; +} + +/** + * A base class for encapsulating the start and stop functionality required by all axios interceptors. + */ +export abstract class AxiosInterceptor { + private readonly activeInterceptors: ActiveInterceptor[] = []; + + /** + * Starts intercepting requests and responses. + * + * @param {AxiosInstance} client The client to start intercepting the requests/responses of. + */ + public start( client: AxiosInstance ): void { + const requestInterceptorID = client.interceptors.request.use( + ( response ) => this.handleRequest( response ), + ); + const responseInterceptorID = client.interceptors.response.use( + ( response ) => this.onResponseSuccess( response ), + ( error ) => this.onResponseRejected( error ), + ); + this.activeInterceptors.push( { client, requestInterceptorID, responseInterceptorID } ); + } + + /** + * Stops intercepting requests and responses. + * + * @param {AxiosInstance} client The client to stop intercepting the requests/responses of. + */ + public stop( client: AxiosInstance ): void { + for ( let i = this.activeInterceptors.length - 1; i >= 0; --i ) { + const active = this.activeInterceptors[ i ]; + if ( client === active.client ) { + client.interceptors.request.eject( active.requestInterceptorID ); + client.interceptors.response.eject( active.responseInterceptorID ); + this.activeInterceptors.splice( i, 1 ); + } + } + } + + /** + * An interceptor method for handling requests before they are made to the server. + * + * @param {AxiosRequestConfig} config The axios request options. + */ + protected handleRequest( config: AxiosRequestConfig ): AxiosRequestConfig { + return config; + } + + /** + * An interceptor method for handling successful responses. + * + * @param {AxiosResponse} response The response from the axios client. + */ + protected onResponseSuccess( response: AxiosResponse ): any { + return response; + } + + /** + * An interceptor method for handling response failures. + * + * @param {*} error The error that occurred. + */ + protected onResponseRejected( error: any ): any { + return error; + } +} diff --git a/tests/e2e/factories/src/framework/api/axios/axios-oauth-interceptor.spec.ts b/tests/e2e/factories/src/framework/api/axios/axios-oauth-interceptor.spec.ts new file mode 100644 index 00000000000..928762fee16 --- /dev/null +++ b/tests/e2e/factories/src/framework/api/axios/axios-oauth-interceptor.spec.ts @@ -0,0 +1,83 @@ +import axios, { AxiosInstance } from 'axios'; +import moxios from 'moxios'; +import { AxiosOAuthInterceptor } from './axios-oauth-interceptor'; + +describe( 'AxiosOAuthInterceptor', () => { + let apiAuthInterceptor: AxiosOAuthInterceptor; + let axiosInstance: AxiosInstance; + + beforeEach( () => { + axiosInstance = axios.create(); + moxios.install( axiosInstance ); + apiAuthInterceptor = new AxiosOAuthInterceptor( + 'consumer_key', + 'consumer_secret', + ); + apiAuthInterceptor.start( axiosInstance ); + } ); + + afterEach( () => { + apiAuthInterceptor.stop( axiosInstance ); + moxios.uninstall( axiosInstance ); + } ); + + it( 'should not run unless started', async () => { + moxios.stubOnce( 'GET', 'https://api.test', { status: 200 } ); + + apiAuthInterceptor.stop( axiosInstance ); + await axiosInstance.get( 'https://api.test' ); + + let request = moxios.requests.mostRecent(); + expect( request.headers ).not.toHaveProperty( 'Authorization' ); + + apiAuthInterceptor.start( axiosInstance ); + await axiosInstance.get( 'https://api.test' ); + + request = moxios.requests.mostRecent(); + expect( request.headers ).toHaveProperty( 'Authorization' ); + } ); + + it( 'should use basic auth for HTTPS', async () => { + moxios.stubOnce( 'GET', 'https://api.test', { status: 200 } ); + await axiosInstance.get( 'https://api.test' ); + + const request = moxios.requests.mostRecent(); + + expect( request.headers ).toHaveProperty( 'Authorization' ); + expect( request.headers.Authorization ).toBe( + 'Basic ' + + Buffer.from( 'consumer_key:consumer_secret' ).toString( 'base64' ), + ); + } ); + + it( 'should use OAuth 1.0a for HTTP', async () => { + moxios.stubOnce( 'GET', 'http://api.test', { status: 200 } ); + await axiosInstance.get( 'http://api.test' ); + + const request = moxios.requests.mostRecent(); + + // We're going to assume that the oauth-1.0a package added the signature data correctly so we will + // focus on ensuring that the header looks roughly correct given what we readily know. + expect( request.headers ).toHaveProperty( 'Authorization' ); + expect( request.headers.Authorization ).toMatch( + /^OAuth oauth_consumer_key="consumer_key".*oauth_signature_method="HMAC-SHA256".*oauth_version="1.0"/, + ); + } ); + + it( 'should work with base URL', async () => { + moxios.stubOnce( 'GET', '/test', { status: 200 } ); + await axiosInstance.request( { + method: 'GET', + baseURL: 'https://api.test/', + url: '/test', + } ); + + const request = moxios.requests.mostRecent(); + + expect( request.headers ).toHaveProperty( 'Authorization' ); + expect( request.headers.Authorization ).toBe( + 'Basic ' + + Buffer.from( 'consumer_key:consumer_secret' ).toString( 'base64' ), + ); + } ); +} ); diff --git a/tests/e2e/factories/src/framework/api/axios/axios-oauth-interceptor.ts b/tests/e2e/factories/src/framework/api/axios/axios-oauth-interceptor.ts new file mode 100644 index 00000000000..f9de2cef9b2 --- /dev/null +++ b/tests/e2e/factories/src/framework/api/axios/axios-oauth-interceptor.ts @@ -0,0 +1,51 @@ +import { AxiosRequestConfig } from 'axios'; +import createHmac from 'create-hmac'; +import OAuth from 'oauth-1.0a'; +import { AxiosInterceptor } from './axios-interceptor'; + +/** + * A utility class for managing the lifecycle of an authentication interceptor. + */ +export class AxiosOAuthInterceptor extends AxiosInterceptor { + private oauth: OAuth; + + public constructor( consumerKey: string, consumerSecret: string ) { + super(); + + this.oauth = new OAuth( { + consumer: { + key: consumerKey, + secret: consumerSecret, + }, + signature_method: 'HMAC-SHA256', + hash_function: ( base: any, key: any ) => { + return createHmac( 'sha256', key ).update( base ).digest( 'base64' ); + }, + } ); + } + + /** + * Adds WooCommerce API authentication details to the outgoing request. + * + * @param {AxiosRequestConfig} request The request that was intercepted. + * @return {AxiosRequestConfig} The request with the additional authorization headers. + */ + protected handleRequest( request: AxiosRequestConfig ): AxiosRequestConfig { + const url = ( request.baseURL || '' ) + ( request.url || '' ); + if ( url.startsWith( 'https' ) ) { + request.auth = { + username: this.oauth.consumer.key, + password: this.oauth.consumer.secret, + }; + } else { + request.headers.Authorization = this.oauth.toHeader( + this.oauth.authorize( { + url, + method: request.method!, + } ), + ).Authorization; + } + + return request; + } +} diff --git a/tests/e2e/factories/src/framework/api/axios/axios-response-interceptor.spec.ts b/tests/e2e/factories/src/framework/api/axios/axios-response-interceptor.spec.ts new file mode 100644 index 00000000000..c4785e70b20 --- /dev/null +++ b/tests/e2e/factories/src/framework/api/axios/axios-response-interceptor.spec.ts @@ -0,0 +1,61 @@ +import axios, { AxiosInstance } from 'axios'; +import moxios from 'moxios'; +import { APIResponse, APIError } from '../api-service'; +import { AxiosResponseInterceptor } from './axios-response-interceptor'; + +describe( 'AxiosResponseInterceptor', () => { + let apiResponseInterceptor: AxiosResponseInterceptor; + let axiosInstance: AxiosInstance; + + beforeEach( () => { + axiosInstance = axios.create(); + moxios.install( axiosInstance ); + apiResponseInterceptor = new AxiosResponseInterceptor(); + apiResponseInterceptor.start( axiosInstance ); + } ); + + afterEach( () => { + apiResponseInterceptor.stop( axiosInstance ); + moxios.uninstall(); + } ); + + it( 'should transform responses into APIResponse', async () => { + moxios.stubOnce( 'GET', 'http://test.test', { + status: 200, + headers: { + 'Content-Type': 'application/json', + }, + responseText: JSON.stringify( { test: 'value' } ), + } ); + + const response = await axiosInstance.get( 'http://test.test' ); + + expect( response ).toMatchObject( { + status: 200, + headers: { + 'content-type': 'application/json', + }, + data: { + test: 'value', + }, + } ); + } ); + + it( 'should transform response errors into APIError', async () => { + moxios.stubOnce( 'GET', 'http://test.test', { + status: 404, + headers: { + 'Content-Type': 'application/json', + }, + responseText: JSON.stringify( { code: 'error_code', message: 'value', data: null } ), + } ); + + await expect( axiosInstance.get( 'http://test.test' ) ).rejects.toMatchObject( + new APIResponse( + 404, + { 'content-type': 'application/json' }, + new APIError( 'error_code', 'value', null ), + ), + ); + } ); +} ); diff --git a/tests/e2e/factories/src/framework/api/axios/axios-response-interceptor.ts b/tests/e2e/factories/src/framework/api/axios/axios-response-interceptor.ts new file mode 100644 index 00000000000..164f48f6107 --- /dev/null +++ b/tests/e2e/factories/src/framework/api/axios/axios-response-interceptor.ts @@ -0,0 +1,39 @@ +import { AxiosResponse } from 'axios'; +import { APIResponse, APIError } from '../api-service'; +import { AxiosInterceptor } from './axios-interceptor'; + +export class AxiosResponseInterceptor extends AxiosInterceptor { + /** + * Transforms the Axios response into our API response to be consumed in a consistent manner. + * + * @param {AxiosResponse} response The respons ethat we need to transform. + * @return {Promise} A promise containing the APIResponse. + */ + protected onResponseSuccess( response: AxiosResponse ): Promise { + return Promise.resolve( + new APIResponse( response.status, response.headers, response.data ), + ); + } + + /** + * Transforms HTTP errors into an API error if the error came from the API. + * + * @param {*} error The error that was caught. + */ + protected onResponseRejected( error: any ): Promise { + // Only transform API errors. + if ( ! error.response ) { + throw error; + } + + throw new APIResponse( + error.response.status, + error.response.headers, + new APIError( + error.response.data.code, + error.response.data.message, + error.response.data.data, + ), + ); + } +} diff --git a/tests/e2e/factories/src/framework/index.ts b/tests/e2e/factories/src/framework/index.ts new file mode 100644 index 00000000000..d611eab4c1e --- /dev/null +++ b/tests/e2e/factories/src/framework/index.ts @@ -0,0 +1,14 @@ +/** + * CORE CLASSES + * These exports relate to extending the core functionality of the package. + */ +export { Adapter } from './adapter'; +export { ModelFactory } from './model-factory'; +export { Model } from './model'; + +/** + * API ADAPTER + * These exports relate to replacing the underlying HTTP layer of API adapters. + */ +export { APIAdapter } from './api/api-adapter'; +export { APIService, APIResponse, APIError } from './api/api-service'; diff --git a/tests/e2e/factories/src/framework/model-factory.spec.ts b/tests/e2e/factories/src/framework/model-factory.spec.ts new file mode 100644 index 00000000000..cbc166ccce2 --- /dev/null +++ b/tests/e2e/factories/src/framework/model-factory.spec.ts @@ -0,0 +1,41 @@ +import { ModelFactory } from './model-factory'; +import { Adapter } from './adapter'; +import { Product } from '../models/product'; +import { SimpleProduct } from '../models/simple-product'; + +class MockAdapter implements Adapter { + public create = jest.fn(); +} + +describe( 'ModelFactory', () => { + let mockAdapter: MockAdapter; + let factory: ModelFactory; + + beforeEach( () => { + mockAdapter = new MockAdapter(); + factory = ModelFactory.define>( + ( { params } ) => { + return new SimpleProduct( params ); + }, + ); + } ); + + it( 'should error without adapter', async () => { + expect( () => factory.create() ).toThrowError( /no adapter/ ); + } ); + + it( 'should create using adapter', async () => { + factory.setAdapter( mockAdapter ); + + const expectedModel = new SimpleProduct( { name: 'test2' } ); + expectedModel.setID( 1 ); + mockAdapter.create.mockReturnValueOnce( Promise.resolve( expectedModel ) ); + + const created = await factory.create( { name: 'test' } ); + + expect( mockAdapter.create.mock.calls ).toHaveLength( 1 ); + expect( created ).toBeInstanceOf( Product ); + expect( created.id ).toBe( 1 ); + expect( created.name ).toBe( 'test2' ); + } ); +} ); diff --git a/tests/e2e/factories/src/framework/model-factory.ts b/tests/e2e/factories/src/framework/model-factory.ts new file mode 100644 index 00000000000..c975381a187 --- /dev/null +++ b/tests/e2e/factories/src/framework/model-factory.ts @@ -0,0 +1,52 @@ +import { DeepPartial, Factory, BuildOptions } from 'fishery'; +import { Model } from './model'; +import { Adapter } from './adapter'; + +/** + * A factory that can be used to create models using an adapter. + */ +export class ModelFactory extends Factory { + private adapter: Adapter | null = null; + + /** + * Sets the adapter that the factory will use to create models. + * + * @param {Adapter|null} adapter + */ + public setAdapter( adapter: Adapter | null ): void { + this.adapter = adapter; + } + + /** + * Create an object using your factory + * + * @param {DeepPartial} params The parameters that should populate the object. + * @param {BuildOptions} options The options to be used in the builder. + * @return {Promise} Resolves to the created model. + */ + public create( params?: DeepPartial, options?: BuildOptions ): Promise { + if ( ! this.adapter ) { + throw new Error( 'The factory has no adapter to create using.' ); + } + + const model = this.build( params, options ); + return this.adapter.create( model ); + } + + /** + * Create an array of objects using your factory + * + * @param {number} number The number of models to create. + * @param {DeepPartial} params The parameters that should populate the object. + * @param {BuildOptions} options The options to be used in the builder. + * @return {Promise} Resolves to the created model. + */ + public createList( number: number, params?: DeepPartial, options?: BuildOptions ): Promise { + if ( ! this.adapter ) { + throw new Error( 'The factory has no adapter to create using.' ); + } + + const model = this.buildList( number, params, options ); + return this.adapter.create( model ); + } +} diff --git a/tests/e2e/factories/src/framework/model-registry.spec.ts b/tests/e2e/factories/src/framework/model-registry.spec.ts new file mode 100644 index 00000000000..1673df9345f --- /dev/null +++ b/tests/e2e/factories/src/framework/model-registry.spec.ts @@ -0,0 +1,45 @@ +import { AdapterTypes, ModelRegistry } from './model-registry'; +import { ModelFactory } from './model-factory'; +import { Product } from '../models/product'; +import { APIAdapter } from './api/api-adapter'; +import { SimpleProduct } from '../models/simple-product'; + +describe( 'ModelRegistry', () => { + let factoryRegistry: ModelRegistry; + + beforeEach( () => { + factoryRegistry = new ModelRegistry(); + } ); + + it( 'should register factories once', () => { + const factory = ModelFactory.define>( ( { params } ) => { + return new SimpleProduct( params ); + } ); + + expect( factoryRegistry.getFactory( SimpleProduct ) ).toBeNull(); + + factoryRegistry.registerFactory( SimpleProduct, factory ); + + expect( () => factoryRegistry.registerFactory( SimpleProduct, factory ) ) + .toThrowError( /already been registered/ ); + + const loaded = factoryRegistry.getFactory( SimpleProduct ); + + expect( loaded ).toBe( factory ); + } ); + + it( 'should register adapters once', () => { + const adapter = new APIAdapter( '', ( model ) => model ); + + expect( factoryRegistry.getAdapter( SimpleProduct, AdapterTypes.API ) ).toBeNull(); + + factoryRegistry.registerAdapter( SimpleProduct, AdapterTypes.API, adapter ); + + expect( () => factoryRegistry.registerAdapter( SimpleProduct, AdapterTypes.API, adapter ) ) + .toThrowError( /already been registered/ ); + + const loaded = factoryRegistry.getAdapter( SimpleProduct, AdapterTypes.API ); + + expect( loaded ).toBe( adapter ); + } ); +} ); diff --git a/tests/e2e/factories/src/framework/model-registry.ts b/tests/e2e/factories/src/framework/model-registry.ts new file mode 100644 index 00000000000..abf7a3934a9 --- /dev/null +++ b/tests/e2e/factories/src/framework/model-registry.ts @@ -0,0 +1,125 @@ +import { Adapter } from './adapter'; +import { Model } from './model'; +import { ModelFactory } from './model-factory'; + +type Registry = { [key: string ]: T }; + +/** + * The types of adapters that can be stored in the registry. + * + * @typedef AdapterTypes + * @property {string} API "api" + * @property {string} Custom "custom" + */ +export enum AdapterTypes { + API = 'api', + Custom = 'custom' +} + +/** + * A registry that allows for us to easily manage all of our factories and related state. + */ +export class ModelRegistry { + private readonly factories: Registry> = {}; + private readonly adapters: { [key in AdapterTypes]: Registry> } = { + api: {}, + custom: {}, + }; + + /** + * Registers a factory for the class. + * + * @param {Function} modelClass The class of model we're registering the factory for. + * @param {ModelFactory} factory The factory that we're registering. + */ + public registerFactory( modelClass: new () => T, factory: ModelFactory ): void { + if ( this.factories.hasOwnProperty( modelClass.name ) ) { + throw new Error( 'A factory of this type has already been registered for the model class.' ); + } + + this.factories[ modelClass.name ] = factory; + } + + /** + * Fetches a factory that was registered for the class. + * + * @param {Function} modelClass The class of model for the factory we're fetching. + */ + public getFactory( modelClass: new () => T ): ModelFactory | null { + if ( this.factories.hasOwnProperty( modelClass.name ) ) { + return this.factories[ modelClass.name ]; + } + + return null; + } + + /** + * Registers an adapter for the class. + * + * @param {Function} modelClass The class of model that we're registering the adapter for. + * @param {AdapterTypes} type The type of adapter that we're registering. + * @param {Adapter} adapter The adapter that we're registering. + */ + public registerAdapter( modelClass: new () => T, type: AdapterTypes, adapter: Adapter ): void { + if ( this.adapters[ type ].hasOwnProperty( modelClass.name ) ) { + throw new Error( 'An adapter of this type has already been registered for the model class.' ); + } + + this.adapters[ type ][ modelClass.name ] = adapter; + } + + /** + * Fetches an adapter registered for the class. + * + * @param {Function} modelClass The class of the model for the adapter we're fetching. + * @param {AdapterTypes} type The type of adapter we're fetching. + */ + public getAdapter( modelClass: new () => T, type: AdapterTypes ): Adapter | null { + if ( this.adapters[ type ].hasOwnProperty( modelClass.name ) ) { + return this.adapters[ type ][ modelClass.name ]; + } + + return null; + } + + /** + * Fetches all of the adapters of a given type from the registry. + * + * @param {AdapterTypes} type The type of adapters to fetch. + */ + public getAdapters( type: AdapterTypes ): Adapter[] { + return Object.values( this.adapters[ type ] ); + } + + /** + * Changes the adapter a factory is using. + * + * @param {Function} modelClass The class of the model factory we're changing. + * @param {AdapterTypes} type The type of adapter to set. + */ + public changeFactoryAdapter( modelClass: new () => T, type: AdapterTypes ): void { + const factory = this.getFactory( modelClass ); + if ( ! factory ) { + throw new Error( 'No factory defined for this model class.' ); + } + const adapter = this.getAdapter( modelClass, type ); + if ( ! adapter ) { + throw new Error( 'No adapter of this type registered for this model class.' ); + } + + factory.setAdapter( adapter ); + } + + /** + * Changes the adapters of all factories to the given type or null if one is not registered for that type. + * + * @param {AdapterTypes} type The type of adapter to set. + */ + public changeAllFactoryAdapters( type: AdapterTypes ): void { + for ( const key in this.factories ) { + this.factories[ key ].setAdapter( + this.adapters[ type ][ key ] || null, + ); + } + } +} diff --git a/tests/e2e/factories/src/framework/model.ts b/tests/e2e/factories/src/framework/model.ts new file mode 100644 index 00000000000..80d071818ae --- /dev/null +++ b/tests/e2e/factories/src/framework/model.ts @@ -0,0 +1,20 @@ +import { DeepPartial } from 'fishery'; + +/** + * A base class for all models. + */ +export abstract class Model { + private _id: number = 0; + + protected constructor( partial: DeepPartial = {} ) { + Object.assign( this, partial ); + } + + public get id(): number { + return this._id; + } + + public setID( id: number ): void { + this._id = id; + } +} diff --git a/tests/e2e/factories/src/index.ts b/tests/e2e/factories/src/index.ts new file mode 100644 index 00000000000..c488478486b --- /dev/null +++ b/tests/e2e/factories/src/index.ts @@ -0,0 +1,17 @@ +/** + * FRAMEWORK CLASSES + * These exports relate to the core classes needed to utilize the package. + */ +export { ModelRegistry, AdapterTypes } from './framework/model-registry'; + +/** + * MODELS + * This exports all of the models we have defined and their related functions. + */ +export * from './models'; + +/** + * UTILITIES + * These exports relate to common utilities that can be used to utilize the package. + */ +export { initializeUsingOAuth, initializeUsingBasicAuth } from './utils'; diff --git a/tests/e2e/factories/src/models/index.ts b/tests/e2e/factories/src/models/index.ts new file mode 100644 index 00000000000..57d084aad6f --- /dev/null +++ b/tests/e2e/factories/src/models/index.ts @@ -0,0 +1,2 @@ +export { Product } from './product'; +export { SimpleProduct, registerSimpleProduct } from './simple-product'; diff --git a/tests/e2e/factories/src/models/product.ts b/tests/e2e/factories/src/models/product.ts new file mode 100644 index 00000000000..857495cb749 --- /dev/null +++ b/tests/e2e/factories/src/models/product.ts @@ -0,0 +1,15 @@ +import { Model } from '../framework/model'; +import { DeepPartial } from 'fishery'; + +/** + * The base class for all product types. + */ +export abstract class Product extends Model { + public readonly name: string = ''; + public readonly regularPrice: string = ''; + + protected constructor( partial: DeepPartial = {} ) { + super( partial ); + Object.assign( this, partial ); + } +} diff --git a/tests/e2e/factories/src/models/simple-product.ts b/tests/e2e/factories/src/models/simple-product.ts new file mode 100644 index 00000000000..e225970e988 --- /dev/null +++ b/tests/e2e/factories/src/models/simple-product.ts @@ -0,0 +1,48 @@ +import { DeepPartial } from 'fishery'; +import { Product } from './product'; +import { AdapterTypes, ModelRegistry } from '../framework/model-registry'; +import { ModelFactory } from '../framework/model-factory'; +import { APIAdapter } from '../framework/api/api-adapter'; +import faker from 'faker/locale/en'; + +export class SimpleProduct extends Product { + public constructor( partial: DeepPartial = {} ) { + super( partial ); + Object.assign( this, partial ); + } +} + +/** + * Registers the simple product factory and adapters. + * + * @param {ModelRegistry} registry The registry to hold the model reference. + */ +export function registerSimpleProduct( registry: ModelRegistry ): void { + if ( null !== registry.getFactory( SimpleProduct ) ) { + return; + } + + const factory = ModelFactory.define>( + ( { params } ) => { + return new SimpleProduct( + { + name: params.name ?? faker.commerce.productName(), + regularPrice: params.regularPrice ?? faker.commerce.price(), + }, + ); + }, + ); + registry.registerFactory( SimpleProduct, factory ); + + const apiAdapter = new APIAdapter( + '/wc/v3/products', + ( model ) => { + return { + type: 'simple', + name: model.name, + regular_price: model.regularPrice, + }; + }, + ); + registry.registerAdapter( SimpleProduct, AdapterTypes.API, apiAdapter ); +} diff --git a/tests/e2e/factories/src/utils.ts b/tests/e2e/factories/src/utils.ts new file mode 100644 index 00000000000..cd2f445725e --- /dev/null +++ b/tests/e2e/factories/src/utils.ts @@ -0,0 +1,55 @@ +import { AdapterTypes, ModelRegistry } from './framework/model-registry'; +import { APIAdapter } from './framework/api/api-adapter'; +import { AxiosAPIService } from './framework/api/axios/axios-api-service'; + +/** + * Initializes all of the APIAdapters with a client to communicate with the API. + * + * @param {ModelRegistry} registry The model registry that we want to initialize. + * @param {string} apiURL The base URL for the API. + * @param {string} consumerKey The OAuth consumer key for the API service. + * @param {string} consumerSecret The OAuth consumer secret for the API service. + */ +export function initializeUsingOAuth( + registry: ModelRegistry, + apiURL: string, + consumerKey: string, + consumerSecret: string, +): void { + const adapters = registry.getAdapters( AdapterTypes.API ) as APIAdapter[]; + if ( ! adapters.length ) { + return; + } + + const apiService = AxiosAPIService.createUsingOAuth( apiURL, consumerKey, consumerSecret ); + for ( const adapter of adapters ) { + adapter.setAPIService( apiService ); + } +} + +/** + * Initialize all of the APIAdapters with a client to communicate with the API. + * + * + * + * @param {ModelRegistry} registry The model registry that we want to initialize. + * @param {string} apiURL The base URL for the API. + * @param {string} username The username to use for authentication. + * @param {string} password The password to use for authentication. + */ +export function initializeUsingBasicAuth( + registry: ModelRegistry, + apiURL: string, + username: string, + password: string, +): void { + const adapters = registry.getAdapters( AdapterTypes.API ) as APIAdapter[]; + if ( ! adapters.length ) { + return; + } + + const apiService = AxiosAPIService.createUsingBasicAuth( apiURL, username, password ); + for ( const adapter of adapters ) { + adapter.setAPIService( apiService ); + } +} diff --git a/tests/e2e/factories/tsconfig.json b/tests/e2e/factories/tsconfig.json new file mode 100644 index 00000000000..b94b7d7b9ba --- /dev/null +++ b/tests/e2e/factories/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "types": [ "node", "jest", "faker", "axios", "moxios", "create-hmac" ], + "rootDir": "src", + "outDir": "dist" + }, + "include": [ "src/" ] +} diff --git a/tests/e2e/utils/components.js b/tests/e2e/utils/components.js index 0e6e721c4b0..94be2fb76fd 100644 --- a/tests/e2e/utils/components.js +++ b/tests/e2e/utils/components.js @@ -7,6 +7,8 @@ */ import { StoreOwnerFlow } from './flows'; import { clickTab, uiUnblocked, verifyCheckboxIsUnset } from './index'; +import modelRegistry from './factories'; +import { SimpleProduct } from '@woocommerce/model-factories'; const config = require( 'config' ); const simpleProductName = config.get( 'products.simple.name' ); @@ -116,7 +118,7 @@ const completeOnboardingWizard = async () => { expect( productTypesCheckboxes ).toHaveLength( 8 ); // Select Physical and Downloadable products - for ( let i = 0; i < 2; i++ ) { + for ( let i = 1; i < 2; i++ ) { await productTypesCheckboxes[i].click(); } @@ -342,22 +344,11 @@ const completeOldSetupWizard = async () => { * Create simple product. */ const createSimpleProduct = async () => { - // Go to "add product" page - await StoreOwnerFlow.openNewProduct(); - - // Make sure we're on the add order page - await expect( page.title() ).resolves.toMatch( 'Add new product' ); - - // Set product data - await expect( page ).toFill( '#title', simpleProductName ); - await clickTab( 'General' ); - await expect( page ).toFill( '#_regular_price', '9.99' ); - - await verifyAndPublish(); - - const simplePostId = await page.$( '#post_ID' ); - let simplePostIdValue = ( await ( await simplePostId.getProperty( 'value' ) ).jsonValue() ); - return simplePostIdValue; + const product = await modelRegistry.getFactory( SimpleProduct ).create( { + name: simpleProductName, + regularPrice: '9.99' + } ); + return product.id; } ; /** diff --git a/tests/e2e/utils/factories.js b/tests/e2e/utils/factories.js new file mode 100644 index 00000000000..f80d552513f --- /dev/null +++ b/tests/e2e/utils/factories.js @@ -0,0 +1,24 @@ +import { + AdapterTypes, + initializeUsingBasicAuth, + ModelRegistry, + registerSimpleProduct +} from '@woocommerce/model-factories'; + +const config = require( 'config' ); + +const modelRegistry = new ModelRegistry() + +// Register all of the different factories that we're going to need. +registerSimpleProduct( modelRegistry ); + +// Make sure to perform the initialization AFTER registering all of the factories, otherwise the adapters might be +// missed on subsequent registrations. +initializeUsingBasicAuth( modelRegistry, + config.get( 'url' ) + '/wp-json', + config.get( 'users.admin.username' ), + config.get( 'users.admin.password' ) +); +modelRegistry.changeAllFactoryAdapters( AdapterTypes.API ); + +export default modelRegistry; diff --git a/tests/legacy/framework/class-wc-unit-test-case.php b/tests/legacy/framework/class-wc-unit-test-case.php index d13e4d1fdbc..74289f2b413 100644 --- a/tests/legacy/framework/class-wc-unit-test-case.php +++ b/tests/legacy/framework/class-wc-unit-test-case.php @@ -171,9 +171,9 @@ class WC_Unit_Test_Case extends WP_HTTP_TestCase { */ public function login_as_administrator() { return $this->login_as_role( 'administrator' ); - } + } - /** + /** * Get an instance of a class that has been registered in the dependency injection container. * To get an instance of a legacy class (such as the ones in the 'íncludes' directory) use * 'get_legacy_instance_of' instead. diff --git a/tests/legacy/framework/helpers/class-wc-helper-order.php b/tests/legacy/framework/helpers/class-wc-helper-order.php index fff89757e6f..457a37d011f 100644 --- a/tests/legacy/framework/helpers/class-wc-helper-order.php +++ b/tests/legacy/framework/helpers/class-wc-helper-order.php @@ -2,7 +2,7 @@ /** * Order helpers. * - * @package WooCommerce/Tests + * @package WooCommerce\Tests */ /** diff --git a/tests/legacy/framework/helpers/class-wc-helper-product.php b/tests/legacy/framework/helpers/class-wc-helper-product.php index a7556b46f3a..a9bd4ac746b 100644 --- a/tests/legacy/framework/helpers/class-wc-helper-product.php +++ b/tests/legacy/framework/helpers/class-wc-helper-product.php @@ -2,7 +2,7 @@ /** * Product helpers. * - * @package woocommerce/tests + * @package WooCommerce\Tests */ /** diff --git a/tests/legacy/framework/helpers/class-wc-helper-shipping.php b/tests/legacy/framework/helpers/class-wc-helper-shipping.php index 4436372638d..a54a5bed1f9 100644 --- a/tests/legacy/framework/helpers/class-wc-helper-shipping.php +++ b/tests/legacy/framework/helpers/class-wc-helper-shipping.php @@ -2,7 +2,7 @@ /** * Helper class for shipping related unit tests. * - * @package WooCommerce\Tests|Helper + * @package WooCommerce\Tests\Helper */ /** diff --git a/tests/legacy/unit-tests/checkout/checkout.php b/tests/legacy/unit-tests/checkout/checkout.php index d6639ec88ab..4d99a0c38d7 100644 --- a/tests/legacy/unit-tests/checkout/checkout.php +++ b/tests/legacy/unit-tests/checkout/checkout.php @@ -2,7 +2,7 @@ /** * Checkout tests. * - * @package WooCommerce|Tests|Checkout + * @package WooCommerce\Tests\Checkout */ /** diff --git a/tests/legacy/unit-tests/order/class-wc-tests-order-functions.php b/tests/legacy/unit-tests/order/class-wc-tests-order-functions.php index ce3fba4c728..5c4ffd08cb0 100644 --- a/tests/legacy/unit-tests/order/class-wc-tests-order-functions.php +++ b/tests/legacy/unit-tests/order/class-wc-tests-order-functions.php @@ -2,7 +2,7 @@ /** * Class WC_Tests_Order_Functions file. * - * @package WooCommerce/Tests + * @package WooCommerce\Tests */ use Automattic\Jetpack\Constants; @@ -1476,6 +1476,45 @@ class WC_Tests_Order_Functions extends WC_Unit_Test_Case { $this->assertEquals( 0, $coupon_data_store->get_tentative_usages_for_user( $coupon->get_id(), array( 'a@b.com' ) ) ); } + /** + * Checks if coupons are released when switching from pending to cancelled state. + * + * Tests the fix for issue #26741 + */ + public function test_wc_cancelled_order_releases_coupon_hold_from_pending_state() { + $coupon_code = 'coupon1'; + $coupon_data_store = WC_Data_Store::load( 'coupon' ); + + $coupon = WC_Helper_Coupon::create_coupon( + $coupon_code, + array( + 'usage_limit' => 1, + 'usage_limit_per_user' => 1, + ) + ); + + $product = WC_Helper_Product::create_simple_product( true ); + WC()->cart->add_to_cart( $product->get_id(), 1 ); + WC()->cart->add_discount( $coupon_code ); + + $order_id = WC_Checkout::instance()->create_order( + array( + 'billing_email' => 'a@b.com', + 'payment_method' => 'dummy', + ) + ); + + $this->assertEquals( 1, $coupon_data_store->get_tentative_usage_count( $coupon->get_id() ) ); + $this->assertEquals( 1, $coupon_data_store->get_tentative_usages_for_user( $coupon->get_id(), array( 'a@b.com' ) ) ); + $this->assertEquals( 1, $coupon_data_store->get_usage_by_email( $coupon, 'a@b.com' ) ); + + $order = new WC_Order( $order_id ); + $order->update_status( 'cancelled' ); + $this->assertEquals( 0, $coupon_data_store->get_tentative_usage_count( $coupon->get_id() ) ); + $this->assertEquals( 0, $coupon_data_store->get_tentative_usages_for_user( $coupon->get_id(), array( 'a@b.com' ) ) ); + $this->assertEquals( 0, $coupon_data_store->get_usage_by_email( $coupon, 'a@b.com' ) ); + } + /** * Test if everything works as expected when coupon hold is disabled using filter. */ diff --git a/tests/legacy/unit-tests/order/class-wc-tests-orders.php b/tests/legacy/unit-tests/order/class-wc-tests-orders.php index 4877745a592..9073011470f 100644 --- a/tests/legacy/unit-tests/order/class-wc-tests-orders.php +++ b/tests/legacy/unit-tests/order/class-wc-tests-orders.php @@ -2,7 +2,7 @@ /** * Class WC_Tests_Order file. * - * @package WooCommerce|Tests|Order + * @package WooCommerce\Tests\Order */ /** diff --git a/tests/legacy/unit-tests/packages/packages.php b/tests/legacy/unit-tests/packages/packages.php index 5f96037a8ff..b960122e78f 100644 --- a/tests/legacy/unit-tests/packages/packages.php +++ b/tests/legacy/unit-tests/packages/packages.php @@ -15,7 +15,6 @@ class WC_Tests_Packages extends WC_Unit_Test_Case { */ public function test_packages_exist() { $this->assertTrue( \Automattic\WooCommerce\Packages::package_exists( 'woocommerce-blocks' ) ); - $this->assertTrue( \Automattic\WooCommerce\Packages::package_exists( 'woocommerce-rest-api' ) ); $this->assertTrue( \Automattic\WooCommerce\Packages::package_exists( 'woocommerce-admin' ) ); } diff --git a/tests/legacy/unit-tests/payment-gateways/cod.php b/tests/legacy/unit-tests/payment-gateways/cod.php index 8766df0d37b..8dea53d70e5 100644 --- a/tests/legacy/unit-tests/payment-gateways/cod.php +++ b/tests/legacy/unit-tests/payment-gateways/cod.php @@ -2,7 +2,7 @@ /** * Contains tests for the COD Payment Gateway. * - * @package WooCommerce/Tests/PaymentGateways + * @package WooCommerce\Tests\PaymentGateways */ use Automattic\Jetpack\Constants; diff --git a/tests/legacy/unit-tests/payment-gateways/payment-gateways.php b/tests/legacy/unit-tests/payment-gateways/payment-gateways.php index 7f839c0badd..3428db4971d 100644 --- a/tests/legacy/unit-tests/payment-gateways/payment-gateways.php +++ b/tests/legacy/unit-tests/payment-gateways/payment-gateways.php @@ -1,6 +1,6 @@ user->create( + array( + 'role' => 'administrator', + ) + ); + } + + /** + * Setup test class. + */ + public function setUp() { + parent::setUp(); + wp_set_current_user( self::$user ); + } + + /** + * Test route registration. + */ + public function test_register_routes() { + $actual_routes = $this->server->get_routes(); + $expected_routes = $this->routes; + + foreach ( $expected_routes as $expected_route ) { + $this->assertArrayHasKey( $expected_route, $actual_routes ); + } + } + + /** + * Validate that the returned API schema matches what is expected. + * + * @return void + */ + public function test_schema_properties() { + $request = new \WP_REST_Request( 'OPTIONS', $this->routes[0] ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $properties = $data['schema']['properties']; + + $this->assertEquals( count( array_keys( $this->properties ) ), count( $properties ), print_r( array_diff( array_keys( $properties ), array_keys( $this->properties ) ), true ) ); + + foreach ( array_keys( $this->properties ) as $property ) { + $this->assertArrayHasKey( $property, $properties ); + } + } + + /** + * Test creation using this method. + * If read-only, test to confirm this. + */ + abstract public function test_create(); + + /** + * Test get/read using this method. + */ + abstract public function test_read(); + + /** + * Test updates using this method. + * If read-only, test to confirm this. + */ + abstract public function test_update(); + + /** + * Test delete using this method. + * If read-only, test to confirm this. + */ + abstract public function test_delete(); + + /** + * Perform a request and return the status and returned data. + * + * @param string $endpoint Endpoint to hit. + * @param string $type Type of request e.g GET or POST. + * @param array $params Request body or query. + * @return object + */ + protected function do_request( $endpoint, $type = 'GET', $params = [] ) { + $request = new \WP_REST_Request( $type, untrailingslashit( $endpoint ) ); + 'GET' === $type ? $request->set_query_params( $params ) : $request->set_body_params( $params ); + $response = $this->server->dispatch( $request ); + + return (object) array( + 'status' => $response->get_status(), + 'data' => json_decode( wp_json_encode( $response->get_data() ), true ), + 'raw' => $response->get_data(), + ); + } + + /** + * Test the request/response matched the data we sent. + * + * @param array $response Array of response data from do_request above. + * @param int $status_code Expected status code. + * @param array $data Array of expected data. + */ + protected function assertExpectedResponse( $response, $status_code = 200, $data = array() ) { + $this->assertObjectHasAttribute( 'status', $response ); + $this->assertObjectHasAttribute( 'data', $response ); + $this->assertEquals( $status_code, $response->status, print_r( $response->data, true ) ); + + if ( $data ) { + foreach ( $data as $key => $value ) { + if ( ! isset( $response->data[ $key ] ) ) { + continue; + } + switch ( $key ) { + case 'meta_data': + $this->assertMetaData( $value, $response->data[ $key ] ); + break; + default: + if ( is_array( $value ) ) { + $this->assertArraySubset( $value, $response->data[ $key ] ); + } else { + $this->assertEquals( $value, $response->data[ $key ] ); + } + } + } + } + } + + /** + * Test meta data in a response matches what we expect. + * + * @param array $expected_meta_data Array of data. + * @param array $actual_meta_data Array of data. + */ + protected function assertMetaData( $expected_meta_data, $actual_meta_data ) { + $this->assertTrue( is_array( $actual_meta_data ) ); + $this->assertEquals( count( $expected_meta_data ), count( $actual_meta_data ) ); + + foreach ( $actual_meta_data as $key => $meta ) { + $this->assertArrayHasKey( 'id', $meta ); + $this->assertArrayHasKey( 'key', $meta ); + $this->assertArrayHasKey( 'value', $meta ); + $this->assertEquals( $expected_meta_data[ $key ]['key'], $meta['key'] ); + $this->assertEquals( $expected_meta_data[ $key ]['value'], $meta['value'] ); + } + } + + /** + * Return array of properties for a given context. + * + * @param string $context Context to use. + * @return array + */ + protected function get_properties( $context = 'edit' ) { + return array_keys( array_filter( $this->properties, function( $contexts ) use( $context ) { + return in_array( $context, $contexts ); + } ) ); + } +} diff --git a/tests/legacy/unit-tests/rest-api/Helpers/AdminNotesHelper.php b/tests/legacy/unit-tests/rest-api/Helpers/AdminNotesHelper.php new file mode 100644 index 00000000000..de1c6c9da13 --- /dev/null +++ b/tests/legacy/unit-tests/rest-api/Helpers/AdminNotesHelper.php @@ -0,0 +1,83 @@ +query( "TRUNCATE TABLE {$wpdb->prefix}wc_admin_notes" ); // @codingStandardsIgnoreLine. + $wpdb->query( "TRUNCATE TABLE {$wpdb->prefix}wc_admin_note_actions" ); // @codingStandardsIgnoreLine. + } + + /** + * Create two notes that we can use for notes REST API tests + */ + public static function add_notes_for_tests() { + $data_store = WC_Data_Store::load( 'admin-note' ); + + $note_1 = new WC_Admin_Note(); + $note_1->set_title( 'PHPUNIT_TEST_NOTE_1_TITLE' ); + $note_1->set_content( 'PHPUNIT_TEST_NOTE_1_CONTENT' ); + $note_1->set_content_data( (object) array( 'amount' => 1.23 ) ); + $note_1->set_type( WC_Admin_Note::E_WC_ADMIN_NOTE_INFORMATIONAL ); + $note_1->set_icon( 'info' ); + $note_1->set_name( 'PHPUNIT_TEST_NOTE_NAME' ); + $note_1->set_source( 'PHPUNIT_TEST' ); + $note_1->add_action( + 'PHPUNIT_TEST_NOTE_1_ACTION_1_SLUG', + 'PHPUNIT_TEST_NOTE_1_ACTION_1_LABEL', + '?s=PHPUNIT_TEST_NOTE_1_ACTION_1_URL' + ); + $note_1->add_action( + 'PHPUNIT_TEST_NOTE_1_ACTION_2_SLUG', + 'PHPUNIT_TEST_NOTE_1_ACTION_2_LABEL', + '?s=PHPUNIT_TEST_NOTE_1_ACTION_2_URL' + ); + $note_1->save(); + + $note_2 = new WC_Admin_Note(); + $note_2->set_title( 'PHPUNIT_TEST_NOTE_2_TITLE' ); + $note_2->set_content( 'PHPUNIT_TEST_NOTE_2_CONTENT' ); + $note_2->set_content_data( (object) array( 'amount' => 4.56 ) ); + $note_2->set_type( WC_Admin_Note::E_WC_ADMIN_NOTE_WARNING ); + $note_2->set_icon( 'info' ); + $note_2->set_name( 'PHPUNIT_TEST_NOTE_NAME' ); + $note_2->set_source( 'PHPUNIT_TEST' ); + $note_2->set_status( WC_Admin_Note::E_WC_ADMIN_NOTE_ACTIONED ); + // This note has no actions. + $note_2->save(); + + $note_3 = new WC_Admin_Note(); + $note_3->set_title( 'PHPUNIT_TEST_NOTE_3_TITLE' ); + $note_3->set_content( 'PHPUNIT_TEST_NOTE_3_CONTENT' ); + $note_3->set_content_data( (object) array( 'amount' => 7.89 ) ); + $note_3->set_type( WC_Admin_Note::E_WC_ADMIN_NOTE_INFORMATIONAL ); + $note_3->set_icon( 'info' ); + $note_3->set_name( 'PHPUNIT_TEST_NOTE_NAME' ); + $note_3->set_source( 'PHPUNIT_TEST' ); + $note_3->set_status( WC_Admin_Note::E_WC_ADMIN_NOTE_SNOOZED ); + $note_3->set_date_reminder( time() - HOUR_IN_SECONDS ); + // This note has no actions. + $note_3->save(); + + } +} diff --git a/tests/legacy/unit-tests/rest-api/Helpers/CouponHelper.php b/tests/legacy/unit-tests/rest-api/Helpers/CouponHelper.php new file mode 100644 index 00000000000..2b65987bff5 --- /dev/null +++ b/tests/legacy/unit-tests/rest-api/Helpers/CouponHelper.php @@ -0,0 +1,147 @@ + $coupon_code, + 'post_type' => 'shop_coupon', + 'post_status' => 'publish', + 'post_excerpt' => 'This is a dummy coupon', + ) + ); + + $meta = wp_parse_args( + $meta, + array( + 'discount_type' => 'fixed_cart', + 'coupon_amount' => '1', + 'individual_use' => 'no', + 'product_ids' => '', + 'exclude_product_ids' => '', + 'usage_limit' => '', + 'usage_limit_per_user' => '', + 'limit_usage_to_x_items' => '', + 'expiry_date' => '', + 'free_shipping' => 'no', + 'exclude_sale_items' => 'no', + 'product_categories' => array(), + 'exclude_product_categories' => array(), + 'minimum_amount' => '', + 'maximum_amount' => '', + 'customer_email' => array(), + 'usage_count' => '0', + ) + ); + + // Update meta. + foreach ( $meta as $key => $value ) { + update_post_meta( $coupon_id, $key, $value ); + } + + return new WC_Coupon( $coupon_code ); + } + + /** + * Delete a coupon. + * + * @param $coupon_id + * + * @return bool + */ + public static function delete_coupon( $coupon_id ) { + wp_delete_post( $coupon_id, true ); + + return true; + } + + /** + * Register a custom coupon type. + * + * @param string $coupon_type + */ + public static function register_custom_type( $coupon_type ) { + static $filters_added = false; + if ( isset( self::$custom_types[ $coupon_type ] ) ) { + return; + } + + self::$custom_types[ $coupon_type ] = "Testing custom type {$coupon_type}"; + + if ( ! $filters_added ) { + add_filter( 'woocommerce_coupon_discount_types', array( __CLASS__, 'filter_discount_types' ) ); + add_filter( 'woocommerce_coupon_get_discount_amount', array( __CLASS__, 'filter_get_discount_amount' ), 10, 5 ); + add_filter( 'woocommerce_coupon_is_valid_for_product', '__return_true' ); + $filters_added = true; + } + } + + /** + * Unregister custom coupon type. + * + * @param $coupon_type + */ + public static function unregister_custom_type( $coupon_type ) { + unset( self::$custom_types[ $coupon_type ] ); + if ( empty( self::$custom_types ) ) { + remove_filter( 'woocommerce_coupon_discount_types', array( __CLASS__, 'filter_discount_types' ) ); + remove_filter( 'woocommerce_coupon_get_discount_amount', array( __CLASS__, 'filter_get_discount_amount' ) ); + remove_filter( 'woocommerce_coupon_is_valid_for_product', '__return_true' ); + } + } + + /** + * Register custom discount types. + * + * @param array $discount_types + * @return array + */ + public static function filter_discount_types( $discount_types ) { + return array_merge( $discount_types, self::$custom_types ); + } + + /** + * Get custom discount type amount. Works like 'percent' type. + * + * @param float $discount + * @param float $discounting_amount + * @param array|null $item + * @param bool $single + * @param WC_Coupon $coupon + * + * @return float + */ + public static function filter_get_discount_amount( $discount, $discounting_amount, $item, $single, $coupon ) { + if ( ! isset( self::$custom_types [ $coupon->get_discount_type() ] ) ) { + return $discount; + } + + return (float) $coupon->get_amount() * ( $discounting_amount / 100 ); + } +} diff --git a/tests/legacy/unit-tests/rest-api/Helpers/CustomerHelper.php b/tests/legacy/unit-tests/rest-api/Helpers/CustomerHelper.php new file mode 100644 index 00000000000..5751cf31f32 --- /dev/null +++ b/tests/legacy/unit-tests/rest-api/Helpers/CustomerHelper.php @@ -0,0 +1,138 @@ + 0, + 'date_modified' => null, + 'country' => 'US', + 'state' => 'PA', + 'postcode' => '19123', + 'city' => 'Philadelphia', + 'address' => '123 South Street', + 'address_2' => 'Apt 1', + 'shipping_country' => 'US', + 'shipping_state' => 'PA', + 'shipping_postcode' => '19123', + 'shipping_city' => 'Philadelphia', + 'shipping_address' => '123 South Street', + 'shipping_address_2' => 'Apt 1', + 'is_vat_exempt' => false, + 'calculated_shipping' => false, + ); + + self::set_customer_details( $customer_data ); + + $customer = new WC_Customer( 0, true ); + + return $customer; + } + + /** + * Creates a customer in the tests DB. + */ + public static function create_customer( $username = 'testcustomer', $password = 'hunter2', $email = 'test@woo.local' ) { + $customer = new WC_Customer(); + $customer->set_billing_country( 'US' ); + $customer->set_first_name( 'Justin' ); + $customer->set_billing_state( 'PA' ); + $customer->set_billing_postcode( '19123' ); + $customer->set_billing_city( 'Philadelphia' ); + $customer->set_billing_address( '123 South Street' ); + $customer->set_billing_address_2( 'Apt 1' ); + $customer->set_shipping_country( 'US' ); + $customer->set_shipping_state( 'PA' ); + $customer->set_shipping_postcode( '19123' ); + $customer->set_shipping_city( 'Philadelphia' ); + $customer->set_shipping_address( '123 South Street' ); + $customer->set_shipping_address_2( 'Apt 1' ); + $customer->set_username( $username ); + $customer->set_password( $password ); + $customer->set_email( $email ); + $customer->save(); + return $customer; + } + + /** + * Get the expected output for the store's base location settings. + * + * @return array + */ + public static function get_expected_store_location() { + return array( 'GB', '', '', '' ); + } + + /** + * Get the customer's shipping and billing info from the session. + * + * @return array + */ + public static function get_customer_details() { + return WC()->session->get( 'customer' ); + } + + /** + * Get the user's chosen shipping method. + * + * @return array + */ + public static function get_chosen_shipping_methods() { + return WC()->session->get( 'chosen_shipping_methods' ); + } + + /** + * Get the "Tax Based On" WooCommerce option. + * + * @return string base or billing + */ + public static function get_tax_based_on() { + return get_option( 'woocommerce_tax_based_on' ); + } + + /** + * Set the the current customer's billing details in the session. + * + * @param string $default_shipping_method Shipping Method slug + */ + public static function set_customer_details( $customer_details ) { + WC()->session->set( 'customer', array_map( 'strval', $customer_details ) ); + } + + /** + * Set the user's chosen shipping method. + * + * @param string $chosen_shipping_method Shipping Method slug + */ + public static function set_chosen_shipping_methods( $chosen_shipping_methods ) { + WC()->session->set( 'chosen_shipping_methods', $chosen_shipping_methods ); + } + + /** + * Set the "Tax Based On" WooCommerce option. + * + * @param string $default_shipping_method Shipping Method slug + */ + public static function set_tax_based_on( $default_shipping_method ) { + update_option( 'woocommerce_tax_based_on', $default_shipping_method ); + } +} diff --git a/tests/legacy/unit-tests/rest-api/Helpers/OrderHelper.php b/tests/legacy/unit-tests/rest-api/Helpers/OrderHelper.php new file mode 100644 index 00000000000..59f72d1dcb2 --- /dev/null +++ b/tests/legacy/unit-tests/rest-api/Helpers/OrderHelper.php @@ -0,0 +1,129 @@ +get_items() as $item ) { + \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::delete_product( $item['product_id'] ); + } + + ShippingHelper::delete_simple_flat_rate(); + + // Delete the order post. + $order->delete( true ); + } + + /** + * Create a order. + * + * @since 2.4 + * @version 3.0 New parameter $product. + * + * @param int $customer_id + * @param WC_Product $product + * + * @return WC_Order + */ + public static function create_order( $customer_id = 1, $product = null ) { + + if ( ! is_a( $product, 'WC_Product' ) ) { + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + } + + ShippingHelper::create_simple_flat_rate(); + + $order_data = array( + 'status' => 'pending', + 'customer_id' => $customer_id, + 'customer_note' => '', + 'total' => '', + ); + + $_SERVER['REMOTE_ADDR'] = '127.0.0.1'; // Required, else wc_create_order throws an exception + $order = wc_create_order( $order_data ); + + // Add order products + $item = new WC_Order_Item_Product(); + $item->set_props( + array( + 'product' => $product, + 'quantity' => 4, + 'subtotal' => wc_get_price_excluding_tax( $product, array( 'qty' => 4 ) ), + 'total' => wc_get_price_excluding_tax( $product, array( 'qty' => 4 ) ), + ) + ); + $item->save(); + $order->add_item( $item ); + + // Set billing address + $order->set_billing_first_name( 'Jeroen' ); + $order->set_billing_last_name( 'Sormani' ); + $order->set_billing_company( 'WooCompany' ); + $order->set_billing_address_1( 'WooAddress' ); + $order->set_billing_address_2( '' ); + $order->set_billing_city( 'WooCity' ); + $order->set_billing_state( 'NY' ); + $order->set_billing_postcode( '123456' ); + $order->set_billing_country( 'US' ); + $order->set_billing_email( 'admin@example.org' ); + $order->set_billing_phone( '555-32123' ); + + // Add shipping costs + $shipping_taxes = WC_Tax::calc_shipping_tax( '10', WC_Tax::get_shipping_tax_rates() ); + $rate = new WC_Shipping_Rate( 'flat_rate_shipping', 'Flat rate shipping', '10', $shipping_taxes, 'flat_rate' ); + $item = new WC_Order_Item_Shipping(); + $item->set_props( + array( + 'method_title' => $rate->label, + 'method_id' => $rate->id, + 'total' => wc_format_decimal( $rate->cost ), + 'taxes' => $rate->taxes, + ) + ); + foreach ( $rate->get_meta_data() as $key => $value ) { + $item->add_meta_data( $key, $value, true ); + } + $order->add_item( $item ); + + // Set payment gateway + $payment_gateways = WC()->payment_gateways->payment_gateways(); + $order->set_payment_method( $payment_gateways['bacs'] ); + + // Set totals + $order->set_shipping_total( 10 ); + $order->set_discount_total( 0 ); + $order->set_discount_tax( 0 ); + $order->set_cart_tax( 0 ); + $order->set_shipping_tax( 0 ); + $order->set_total( 50 ); // 4 x $10 simple helper product + $order->save(); + + return $order; + } +} diff --git a/tests/legacy/unit-tests/rest-api/Helpers/ProductHelper.php b/tests/legacy/unit-tests/rest-api/Helpers/ProductHelper.php new file mode 100644 index 00000000000..78d4333610e --- /dev/null +++ b/tests/legacy/unit-tests/rest-api/Helpers/ProductHelper.php @@ -0,0 +1,310 @@ +delete( true ); + } + } + + /** + * Create simple product. + * + * @since 2.3 + * @param bool $save Save or return object. + * @return WC_Product_Simple + */ + public static function create_simple_product( $save = true ) { + $product = new WC_Product_Simple(); + $product->set_props( + array( + 'name' => 'Dummy Product', + 'regular_price' => 10, + 'price' => 10, + 'sku' => 'DUMMY SKU', + 'manage_stock' => false, + 'tax_status' => 'taxable', + 'downloadable' => false, + 'virtual' => false, + 'stock_status' => 'instock', + 'weight' => '1.1', + ) + ); + + if ( $save ) { + $product->save(); + return \wc_get_product( $product->get_id() ); + } else { + return $product; + } + } + + /** + * Create external product. + * + * @since 3.0.0 + * @return WC_Product_External + */ + public static function create_external_product() { + $product = new WC_Product_External(); + $product->set_props( + array( + 'name' => 'Dummy External Product', + 'regular_price' => 10, + 'sku' => 'DUMMY EXTERNAL SKU', + 'product_url' => 'http://woocommerce.com', + 'button_text' => 'Buy external product', + ) + ); + $product->save(); + + return \wc_get_product( $product->get_id() ); + } + + /** + * Create grouped product. + * + * @since 3.0.0 + * @return WC_Product_Grouped + */ + public static function create_grouped_product() { + $simple_product_1 = self::create_simple_product(); + $simple_product_2 = self::create_simple_product(); + $product = new WC_Product_Grouped(); + $product->set_props( + array( + 'name' => 'Dummy Grouped Product', + 'sku' => 'DUMMY GROUPED SKU', + ) + ); + $product->set_children( array( $simple_product_1->get_id(), $simple_product_2->get_id() ) ); + $product->save(); + + return \wc_get_product( $product->get_id() ); + } + + /** + * Create a dummy variation product. + * + * @since 2.3 + * + * @return WC_Product_Variable + */ + public static function create_variation_product() { + $product = new WC_Product_Variable(); + $product->set_props( + array( + 'name' => 'Dummy Variable Product', + 'sku' => 'DUMMY VARIABLE SKU', + ) + ); + + $attribute_data = self::create_attribute( 'size', array( 'small', 'large' ) ); // Create all attribute related things. + $attributes = array(); + $attribute = new WC_Product_Attribute(); + $attribute->set_id( $attribute_data['attribute_id'] ); + $attribute->set_name( $attribute_data['attribute_taxonomy'] ); + $attribute->set_options( $attribute_data['term_ids'] ); + $attribute->set_position( 1 ); + $attribute->set_visible( true ); + $attribute->set_variation( true ); + $attributes[] = $attribute; + + $product->set_attributes( $attributes ); + $product->save(); + + $variation_1 = new WC_Product_Variation(); + $variation_1->set_props( + array( + 'parent_id' => $product->get_id(), + 'sku' => 'DUMMY SKU VARIABLE SMALL', + 'regular_price' => 10, + ) + ); + $variation_1->set_attributes( array( 'pa_size' => 'small' ) ); + $variation_1->save(); + + $variation_2 = new WC_Product_Variation(); + $variation_2->set_props( + array( + 'parent_id' => $product->get_id(), + 'sku' => 'DUMMY SKU VARIABLE LARGE', + 'regular_price' => 15, + ) + ); + $variation_2->set_attributes( array( 'pa_size' => 'large' ) ); + $variation_2->save(); + + return \wc_get_product( $product->get_id() ); + } + + /** + * Create a dummy attribute. + * + * @since 2.3 + * + * @param string $raw_name Name of attribute to create. + * @param array(string) $terms Terms to create for the attribute. + * @return array + */ + public static function create_attribute( $raw_name = 'size', $terms = array( 'small' ) ) { + global $wpdb, $wc_product_attributes; + + // Make sure caches are clean. + \delete_transient( 'wc_attribute_taxonomies' ); + if ( method_exists( '\WC_Cache_Helper', 'invalidate_cache_group' ) ) { + \WC_Cache_Helper::invalidate_cache_group( 'woocommerce-attributes' ); + } else { + \WC_Cache_Helper::incr_cache_prefix( 'woocommerce-attributes' ); + } + + // These are exported as labels, so convert the label to a name if possible first. + $attribute_labels = \wp_list_pluck( wc_get_attribute_taxonomies(), 'attribute_label', 'attribute_name' ); + $attribute_name = \array_search( $raw_name, $attribute_labels, true ); + + if ( ! $attribute_name ) { + $attribute_name = \wc_sanitize_taxonomy_name( $raw_name ); + } + + $attribute_id = \wc_attribute_taxonomy_id_by_name( $attribute_name ); + + if ( ! $attribute_id ) { + $taxonomy_name = \wc_attribute_taxonomy_name( $attribute_name ); + + // Degister taxonomy which other tests may have created... + \unregister_taxonomy( $taxonomy_name ); + + $attribute_id = \wc_create_attribute( + array( + 'name' => $raw_name, + 'slug' => $attribute_name, + 'type' => 'select', + 'order_by' => 'menu_order', + 'has_archives' => 0, + ) + ); + + // Register as taxonomy. + \register_taxonomy( + $taxonomy_name, + apply_filters( 'woocommerce_taxonomy_objects_' . $taxonomy_name, array( 'product' ) ), + apply_filters( + 'woocommerce_taxonomy_args_' . $taxonomy_name, + array( + 'labels' => array( + 'name' => $raw_name, + ), + 'hierarchical' => false, + 'show_ui' => false, + 'query_var' => true, + 'rewrite' => false, + ) + ) + ); + + // Set product attributes global. + $wc_product_attributes = array(); + + foreach ( \wc_get_attribute_taxonomies() as $taxonomy ) { + $wc_product_attributes[ \wc_attribute_taxonomy_name( $taxonomy->attribute_name ) ] = $taxonomy; + } + } + + $attribute = \wc_get_attribute( $attribute_id ); + $return = array( + 'attribute_name' => $attribute->name, + 'attribute_taxonomy' => $attribute->slug, + 'attribute_id' => $attribute_id, + 'term_ids' => array(), + ); + + foreach ( $terms as $term ) { + $result = \term_exists( $term, $attribute->slug ); + + if ( ! $result ) { + $result = wp_insert_term( $term, $attribute->slug ); + $return['term_ids'][] = $result['term_id']; + } else { + $return['term_ids'][] = $result['term_id']; + } + } + + return $return; + } + + /** + * Delete an attribute. + * + * @param int $attribute_id ID to delete. + * + * @since 2.3 + */ + public static function delete_attribute( $attribute_id ) { + global $wpdb; + + $attribute_id = \absint( $attribute_id ); + + $wpdb->query( + $wpdb->prepare( "DELETE FROM {$wpdb->prefix}woocommerce_attribute_taxonomies WHERE attribute_id = %d", $attribute_id ) + ); + } + + /** + * Creates a new product review on a specific product. + * + * @since 3.0 + * @param int $product_id integer Product ID that the review is for. + * @param string $review_content string Content to use for the product review. + * @return integer Product Review ID. + */ + public static function create_product_review( $product_id, $review_content = 'Review content here' ) { + $data = array( + 'comment_post_ID' => $product_id, + 'comment_author' => 'admin', + 'comment_author_email' => 'woo@woo.local', + 'comment_author_url' => '', + 'comment_date' => '2016-01-01T11:11:11', + 'comment_content' => $review_content, + 'comment_approved' => 1, + 'comment_type' => 'review', + ); + return \wp_insert_comment( $data ); + } + + /** + * A helper function for hooking into save_post during the test_product_meta_save_post test. + * @since 3.0.1 + * + * @param int $id ID to update. + */ + public static function save_post_test_update_meta_data_direct( $id ) { + \update_post_meta( $id, '_test2', 'world' ); + } +} diff --git a/tests/legacy/unit-tests/rest-api/Helpers/QueueHelper.php b/tests/legacy/unit-tests/rest-api/Helpers/QueueHelper.php new file mode 100644 index 00000000000..b0afec4c6fd --- /dev/null +++ b/tests/legacy/unit-tests/rest-api/Helpers/QueueHelper.php @@ -0,0 +1,62 @@ +queue()->search( + array( + 'per_page' => -1, + 'status' => 'pending', + 'claimed' => false, + ) + ); + + return $jobs; + } + + /** + * Run all pending queued actions. + * + * @return void + */ + public static function run_all_pending() { + $jobs = self::get_all_pending(); + + foreach ( $jobs as $job ) { + $job->execute(); + } + } + + /** + * Run all pending queued actions. + * + * @return void + */ + public static function process_pending() { + $jobs = self::get_all_pending(); + + $queue_runner = new \ActionScheduler_QueueRunner(); + foreach ( $jobs as $job_id => $job ) { + $queue_runner->process_action( $job_id ); + } + } +} diff --git a/tests/legacy/unit-tests/rest-api/Helpers/SettingsHelper.php b/tests/legacy/unit-tests/rest-api/Helpers/SettingsHelper.php new file mode 100644 index 00000000000..d7a8efd979d --- /dev/null +++ b/tests/legacy/unit-tests/rest-api/Helpers/SettingsHelper.php @@ -0,0 +1,82 @@ + 'test', + 'bad' => 'value', + 'label' => 'Test extension', + 'description' => 'My awesome test settings.', + 'option_key' => '', + ); + $groups[] = array( + 'id' => 'sub-test', + 'parent_id' => 'test', + 'label' => 'Sub test', + 'description' => '', + 'option_key' => '', + ); + $groups[] = array( + 'id' => 'coupon-data', + 'label' => 'Coupon data', + 'option_key' => '', + ); + $groups[] = array( + 'id' => 'invalid', + 'option_key' => '', + ); + return $groups; + } + + /** + * Registers some example settings. + * + * @since 3.0.0 + * @param array $settings + * @return array + */ + public static function register_test_settings( $settings ) { + $settings[] = array( + 'id' => 'woocommerce_shop_page_display', + 'label' => 'Shop page display', + 'description' => 'This controls what is shown on the product archive.', + 'default' => '', + 'type' => 'select', + 'options' => array( + '' => 'Show products', + 'subcategories' => 'Show categories & subcategories', + 'both' => 'Show both', + ), + 'option_key' => 'woocommerce_shop_page_display', + ); + return $settings; + } +} diff --git a/tests/legacy/unit-tests/rest-api/Helpers/ShippingHelper.php b/tests/legacy/unit-tests/rest-api/Helpers/ShippingHelper.php new file mode 100644 index 00000000000..04d17e4274c --- /dev/null +++ b/tests/legacy/unit-tests/rest-api/Helpers/ShippingHelper.php @@ -0,0 +1,50 @@ + 'yes', + 'title' => 'Flat rate', + 'availability' => 'all', + 'countries' => '', + 'tax_status' => 'taxable', + 'cost' => '10', + ); + + update_option( 'woocommerce_flat_rate_settings', $flat_rate_settings ); + update_option( 'woocommerce_flat_rate', array() ); + WC_Cache_Helper::get_transient_version( 'shipping', true ); + WC()->shipping()->load_shipping_methods(); + } + + /** + * Delete the simple flat rate. + * + * @since 2.3 + */ + public static function delete_simple_flat_rate() { + delete_option( 'woocommerce_flat_rate_settings' ); + delete_option( 'woocommerce_flat_rate' ); + WC_Cache_Helper::get_transient_version( 'shipping', true ); + WC()->shipping()->unregister_shipping_methods(); + } +} diff --git a/tests/legacy/unit-tests/rest-api/Tests/Version2/coupons.php b/tests/legacy/unit-tests/rest-api/Tests/Version2/coupons.php new file mode 100644 index 00000000000..8c4d6024d70 --- /dev/null +++ b/tests/legacy/unit-tests/rest-api/Tests/Version2/coupons.php @@ -0,0 +1,471 @@ +endpoint = new WC_REST_Coupons_Controller(); + $this->user = $this->factory->user->create( + array( + 'role' => 'administrator', + ) + ); + } + + /** + * Test route registration. + * @since 3.0.0 + */ + public function test_register_routes() { + $routes = $this->server->get_routes(); + $this->assertArrayHasKey( '/wc/v2/coupons', $routes ); + $this->assertArrayHasKey( '/wc/v2/coupons/(?P[\d]+)', $routes ); + $this->assertArrayHasKey( '/wc/v2/coupons/batch', $routes ); + } + + /** + * Test getting coupons. + * @since 3.0.0 + */ + public function test_get_coupons() { + wp_set_current_user( $this->user ); + + $coupon_1 = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\CouponHelper::create_coupon( 'dummycoupon-1' ); + $post_1 = get_post( $coupon_1->get_id() ); + $coupon_2 = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\CouponHelper::create_coupon( 'dummycoupon-2' ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/coupons' ) ); + $coupons = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 2, count( $coupons ) ); + $this->assertContains( + array( + 'id' => $coupon_1->get_id(), + 'code' => 'dummycoupon-1', + 'amount' => '1.00', + 'date_created' => wc_rest_prepare_date_response( $post_1->post_date_gmt, false ), + 'date_created_gmt' => wc_rest_prepare_date_response( $post_1->post_date_gmt ), + 'date_modified' => wc_rest_prepare_date_response( $post_1->post_modified_gmt, false ), + 'date_modified_gmt' => wc_rest_prepare_date_response( $post_1->post_modified_gmt ), + 'discount_type' => 'fixed_cart', + 'description' => 'This is a dummy coupon', + 'date_expires' => '', + 'date_expires_gmt' => '', + 'usage_count' => 0, + 'individual_use' => false, + 'product_ids' => array(), + 'excluded_product_ids' => array(), + 'usage_limit' => '', + 'usage_limit_per_user' => '', + 'limit_usage_to_x_items' => null, + 'free_shipping' => false, + 'product_categories' => array(), + 'excluded_product_categories' => array(), + 'exclude_sale_items' => false, + 'minimum_amount' => '0.00', + 'maximum_amount' => '0.00', + 'email_restrictions' => array(), + 'used_by' => array(), + 'meta_data' => array(), + '_links' => array( + 'self' => array( + array( + 'href' => rest_url( '/wc/v2/coupons/' . $coupon_1->get_id() ), + ), + ), + 'collection' => array( + array( + 'href' => rest_url( '/wc/v2/coupons' ), + ), + ), + ), + ), + $coupons + ); + } + + /** + * Test getting coupons without valid permissions. + * @since 3.0.0 + */ + public function test_get_coupons_without_permission() { + wp_set_current_user( 0 ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/coupons' ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test getting a single coupon. + * @since 3.0.0 + */ + public function test_get_coupon() { + wp_set_current_user( $this->user ); + $coupon = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\CouponHelper::create_coupon( 'dummycoupon-1' ); + $post = get_post( $coupon->get_id() ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/coupons/' . $coupon->get_id() ) ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( + array( + 'id' => $coupon->get_id(), + 'code' => 'dummycoupon-1', + 'amount' => '1.00', + 'date_created' => wc_rest_prepare_date_response( $post->post_date_gmt, false ), + 'date_created_gmt' => wc_rest_prepare_date_response( $post->post_date_gmt ), + 'date_modified' => wc_rest_prepare_date_response( $post->post_modified_gmt, false ), + 'date_modified_gmt' => wc_rest_prepare_date_response( $post->post_modified_gmt ), + 'discount_type' => 'fixed_cart', + 'description' => 'This is a dummy coupon', + 'date_expires' => null, + 'date_expires_gmt' => null, + 'usage_count' => 0, + 'individual_use' => false, + 'product_ids' => array(), + 'excluded_product_ids' => array(), + 'usage_limit' => null, + 'usage_limit_per_user' => null, + 'limit_usage_to_x_items' => null, + 'free_shipping' => false, + 'product_categories' => array(), + 'excluded_product_categories' => array(), + 'exclude_sale_items' => false, + 'minimum_amount' => '0.00', + 'maximum_amount' => '0.00', + 'email_restrictions' => array(), + 'used_by' => array(), + 'meta_data' => array(), + ), + $data + ); + } + + /** + * Test getting a single coupon with an invalid ID. + * @since 3.0.0 + */ + public function test_get_coupon_invalid_id() { + wp_set_current_user( $this->user ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/coupons/0' ) ); + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * Test getting a single coupon without valid permissions. + * @since 3.0.0 + */ + public function test_get_coupon_without_permission() { + wp_set_current_user( 0 ); + $coupon = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\CouponHelper::create_coupon( 'dummycoupon-1' ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/coupons/' . $coupon->get_id() ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test creating a single coupon. + * @since 3.0.0 + */ + public function test_create_coupon() { + wp_set_current_user( $this->user ); + $request = new WP_REST_Request( 'POST', '/wc/v2/coupons' ); + $request->set_body_params( + array( + 'code' => 'test', + 'amount' => '5.00', + 'discount_type' => 'fixed_product', + 'description' => 'Test', + 'usage_limit' => 10, + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 201, $response->get_status() ); + $this->assertEquals( + array( + 'id' => $data['id'], + 'code' => 'test', + 'amount' => '5.00', + 'date_created' => $data['date_created'], + 'date_created_gmt' => $data['date_created_gmt'], + 'date_modified' => $data['date_modified'], + 'date_modified_gmt' => $data['date_modified_gmt'], + 'discount_type' => 'fixed_product', + 'description' => 'Test', + 'date_expires' => null, + 'date_expires_gmt' => null, + 'usage_count' => 0, + 'individual_use' => false, + 'product_ids' => array(), + 'excluded_product_ids' => array(), + 'usage_limit' => 10, + 'usage_limit_per_user' => null, + 'limit_usage_to_x_items' => null, + 'free_shipping' => false, + 'product_categories' => array(), + 'excluded_product_categories' => array(), + 'exclude_sale_items' => false, + 'minimum_amount' => '0.00', + 'maximum_amount' => '0.00', + 'email_restrictions' => array(), + 'used_by' => array(), + 'meta_data' => array(), + ), + $data + ); + } + + /** + * Test creating a single coupon with invalid fields. + * @since 3.0.0 + */ + public function test_create_coupon_invalid_fields() { + wp_set_current_user( $this->user ); + + // test no code... + $request = new WP_REST_Request( 'POST', '/wc/v2/coupons' ); + $request->set_body_params( + array( + 'amount' => '5.00', + 'discount_type' => 'fixed_product', + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 400, $response->get_status() ); + } + + /** + * Test creating a single coupon without valid permissions. + * @since 3.0.0 + */ + public function test_create_coupon_without_permission() { + wp_set_current_user( 0 ); + + // test no code... + $request = new WP_REST_Request( 'POST', '/wc/v2/coupons' ); + $request->set_body_params( + array( + 'code' => 'fail', + 'amount' => '5.00', + 'discount_type' => 'fixed_product', + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test updating a single coupon. + * @since 3.0.0 + */ + public function test_update_coupon() { + wp_set_current_user( $this->user ); + $coupon = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\CouponHelper::create_coupon( 'dummycoupon-1' ); + $post = get_post( $coupon->get_id() ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/coupons/' . $coupon->get_id() ) ); + $data = $response->get_data(); + $this->assertEquals( 'This is a dummy coupon', $data['description'] ); + $this->assertEquals( 'fixed_cart', $data['discount_type'] ); + $this->assertEquals( '1.00', $data['amount'] ); + + $request = new WP_REST_Request( 'PUT', '/wc/v2/coupons/' . $coupon->get_id() ); + $request->set_body_params( + array( + 'amount' => '10.00', + 'description' => 'New description', + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( '10.00', $data['amount'] ); + $this->assertEquals( 'New description', $data['description'] ); + $this->assertEquals( 'fixed_cart', $data['discount_type'] ); + } + + /** + * Test updating a single coupon with an invalid ID. + * @since 3.0.0 + */ + public function test_update_coupon_invalid_id() { + wp_set_current_user( $this->user ); + + $request = new WP_REST_Request( 'PUT', '/wc/v2/coupons/0' ); + $request->set_body_params( + array( + 'code' => 'tester', + 'amount' => '10.00', + 'description' => 'New description', + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 400, $response->get_status() ); + } + + /** + * Test updating a single coupon without valid permissions. + * @since 3.0.0 + */ + public function test_update_coupon_without_permission() { + wp_set_current_user( 0 ); + $coupon = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\CouponHelper::create_coupon( 'dummycoupon-1' ); + $post = get_post( $coupon->get_id() ); + + $request = new WP_REST_Request( 'PUT', '/wc/v2/coupons/' . $coupon->get_id() ); + $request->set_body_params( + array( + 'amount' => '10.00', + 'description' => 'New description', + ) + ); + $response = $this->server->dispatch( $request ); + + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test deleting a single coupon. + * @since 3.0.0 + */ + public function test_delete_coupon() { + wp_set_current_user( $this->user ); + $coupon = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\CouponHelper::create_coupon( 'dummycoupon-1' ); + $request = new WP_REST_Request( 'DELETE', '/wc/v2/coupons/' . $coupon->get_id() ); + $request->set_param( 'force', true ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 200, $response->get_status() ); + } + + /** + * Test deleting a single coupon with an invalid ID. + * @since 3.0.0 + */ + public function test_delete_coupon_invalid_id() { + wp_set_current_user( $this->user ); + $request = new WP_REST_Request( 'DELETE', '/wc/v2/coupons/0' ); + $request->set_param( 'force', true ); + $response = $this->server->dispatch( $request ); + + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * Test deleting a single coupon without valid permissions. + * @since 3.0.0 + */ + public function test_delete_coupon_without_permission() { + wp_set_current_user( 0 ); + $coupon = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\CouponHelper::create_coupon( 'dummycoupon-1' ); + $request = new WP_REST_Request( 'DELETE', '/wc/v2/coupons/' . $coupon->get_id() ); + $response = $this->server->dispatch( $request ); + + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test batch operations on coupons. + * @since 3.0.0 + */ + public function test_batch_coupon() { + wp_set_current_user( $this->user ); + + $coupon_1 = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\CouponHelper::create_coupon( 'dummycoupon-1' ); + $coupon_2 = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\CouponHelper::create_coupon( 'dummycoupon-2' ); + $coupon_3 = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\CouponHelper::create_coupon( 'dummycoupon-3' ); + $coupon_4 = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\CouponHelper::create_coupon( 'dummycoupon-4' ); + + $request = new WP_REST_Request( 'POST', '/wc/v2/coupons/batch' ); + $request->set_body_params( + array( + 'update' => array( + array( + 'id' => $coupon_1->get_id(), + 'amount' => '5.15', + ), + ), + 'delete' => array( + $coupon_2->get_id(), + $coupon_3->get_id(), + ), + 'create' => array( + array( + 'code' => 'new-coupon', + 'amount' => '11.00', + ), + ), + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( '5.15', $data['update'][0]['amount'] ); + $this->assertEquals( '11.00', $data['create'][0]['amount'] ); + $this->assertEquals( 'new-coupon', $data['create'][0]['code'] ); + $this->assertEquals( $coupon_2->get_id(), $data['delete'][0]['id'] ); + $this->assertEquals( $coupon_3->get_id(), $data['delete'][1]['id'] ); + + $request = new WP_REST_Request( 'GET', '/wc/v2/coupons' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 3, count( $data ) ); + } + + /** + * Test coupon schema. + * @since 3.0.0 + */ + public function test_coupon_schema() { + wp_set_current_user( $this->user ); + $request = new WP_REST_Request( 'OPTIONS', '/wc/v2/coupons' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $properties = $data['schema']['properties']; + + $this->assertEquals( 27, count( $properties ) ); + $this->assertArrayHasKey( 'id', $properties ); + $this->assertArrayHasKey( 'code', $properties ); + $this->assertArrayHasKey( 'date_created', $properties ); + $this->assertArrayHasKey( 'date_created_gmt', $properties ); + $this->assertArrayHasKey( 'date_modified', $properties ); + $this->assertArrayHasKey( 'date_modified_gmt', $properties ); + $this->assertArrayHasKey( 'description', $properties ); + $this->assertArrayHasKey( 'discount_type', $properties ); + $this->assertArrayHasKey( 'amount', $properties ); + $this->assertArrayHasKey( 'date_expires', $properties ); + $this->assertArrayHasKey( 'date_expires_gmt', $properties ); + $this->assertArrayHasKey( 'usage_count', $properties ); + $this->assertArrayHasKey( 'individual_use', $properties ); + $this->assertArrayHasKey( 'product_ids', $properties ); + $this->assertArrayHasKey( 'excluded_product_ids', $properties ); + $this->assertArrayHasKey( 'usage_limit', $properties ); + $this->assertArrayHasKey( 'usage_limit_per_user', $properties ); + $this->assertArrayHasKey( 'limit_usage_to_x_items', $properties ); + $this->assertArrayHasKey( 'free_shipping', $properties ); + $this->assertArrayHasKey( 'product_categories', $properties ); + $this->assertArrayHasKey( 'excluded_product_categories', $properties ); + $this->assertArrayHasKey( 'exclude_sale_items', $properties ); + $this->assertArrayHasKey( 'minimum_amount', $properties ); + $this->assertArrayHasKey( 'maximum_amount', $properties ); + $this->assertArrayHasKey( 'email_restrictions', $properties ); + $this->assertArrayHasKey( 'used_by', $properties ); + } +} diff --git a/tests/legacy/unit-tests/rest-api/Tests/Version2/customers.php b/tests/legacy/unit-tests/rest-api/Tests/Version2/customers.php new file mode 100644 index 00000000000..f9a4d5acca1 --- /dev/null +++ b/tests/legacy/unit-tests/rest-api/Tests/Version2/customers.php @@ -0,0 +1,566 @@ +endpoint = new WC_REST_Customers_Controller(); + } + + /** + * Test route registration. + * + * @since 3.0.0 + */ + public function test_register_routes() { + $routes = $this->server->get_routes(); + + $this->assertArrayHasKey( '/wc/v2/customers', $routes ); + $this->assertArrayHasKey( '/wc/v2/customers/(?P[\d]+)', $routes ); + $this->assertArrayHasKey( '/wc/v2/customers/batch', $routes ); + } + + /** + * Test getting customers. + * + * @since 3.0.0 + */ + public function test_get_customers() { + wp_set_current_user( 1 ); + + $customer_1 = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\CustomerHelper::create_customer(); + \Automattic\WooCommerce\RestApi\UnitTests\Helpers\CustomerHelper::create_customer( 'test2', 'test2', 'test2@woo.local' ); + + $request = new WP_REST_Request( 'GET', '/wc/v2/customers' ); + $request->set_query_params( + array( + 'orderby' => 'id', + ) + ); + $response = $this->server->dispatch( $request ); + $customers = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 2, count( $customers ) ); + + $this->assertContains( + array( + 'id' => $customer_1->get_id(), + 'date_created' => wc_rest_prepare_date_response( $customer_1->get_date_created(), false ), + 'date_created_gmt' => wc_rest_prepare_date_response( $customer_1->get_date_created() ), + 'date_modified' => wc_rest_prepare_date_response( $customer_1->get_date_modified(), false ), + 'date_modified_gmt' => wc_rest_prepare_date_response( $customer_1->get_date_modified() ), + 'email' => 'test@woo.local', + 'first_name' => 'Justin', + 'last_name' => '', + 'role' => 'customer', + 'username' => 'testcustomer', + 'billing' => array( + 'first_name' => '', + 'last_name' => '', + 'company' => '', + 'address_1' => '123 South Street', + 'address_2' => 'Apt 1', + 'city' => 'Philadelphia', + 'state' => 'PA', + 'postcode' => '19123', + 'country' => 'US', + 'email' => '', + 'phone' => '', + ), + 'shipping' => array( + 'first_name' => '', + 'last_name' => '', + 'company' => '', + 'address_1' => '123 South Street', + 'address_2' => 'Apt 1', + 'city' => 'Philadelphia', + 'state' => 'PA', + 'postcode' => '19123', + 'country' => 'US', + ), + 'is_paying_customer' => false, + 'orders_count' => 0, + 'total_spent' => '0.00', + 'avatar_url' => $customer_1->get_avatar_url(), + 'meta_data' => array(), + '_links' => array( + 'self' => array( + array( + 'href' => rest_url( '/wc/v2/customers/' . $customer_1->get_id() . '' ), + ), + ), + 'collection' => array( + array( + 'href' => rest_url( '/wc/v2/customers' ), + ), + ), + ), + ), + $customers + ); + } + + /** + * Test getting customers without valid permissions. + * + * @since 3.0.0 + */ + public function test_get_customers_without_permission() { + wp_set_current_user( 0 ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/customers' ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test creating a new customer. + * + * @since 3.0.0 + */ + public function test_create_customer() { + wp_set_current_user( 1 ); + + // Test just the basics first.. + $request = new WP_REST_Request( 'POST', '/wc/v2/customers' ); + $request->set_body_params( + array( + 'username' => 'create_customer_test', + 'password' => 'test123', + 'email' => 'create_customer_test@woo.local', + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 201, $response->get_status() ); + $this->assertEquals( + array( + 'id' => $data['id'], + 'date_created' => $data['date_created'], + 'date_created_gmt' => $data['date_created_gmt'], + 'date_modified' => $data['date_modified'], + 'date_modified_gmt' => $data['date_modified_gmt'], + 'email' => 'create_customer_test@woo.local', + 'first_name' => '', + 'last_name' => '', + 'role' => 'customer', + 'username' => 'create_customer_test', + 'billing' => array( + 'first_name' => '', + 'last_name' => '', + 'company' => '', + 'address_1' => '', + 'address_2' => '', + 'city' => '', + 'state' => '', + 'postcode' => '', + 'country' => '', + 'email' => '', + 'phone' => '', + ), + 'shipping' => array( + 'first_name' => '', + 'last_name' => '', + 'company' => '', + 'address_1' => '', + 'address_2' => '', + 'city' => '', + 'state' => '', + 'postcode' => '', + 'country' => '', + ), + 'is_paying_customer' => false, + 'meta_data' => array(), + 'orders_count' => 0, + 'total_spent' => '0.00', + 'avatar_url' => $data['avatar_url'], + ), + $data + ); + + // Test extra data + $request = new WP_REST_Request( 'POST', '/wc/v2/customers' ); + $request->set_body_params( + array( + 'username' => 'create_customer_test2', + 'password' => 'test123', + 'email' => 'create_customer_test2@woo.local', + 'first_name' => 'Test', + 'last_name' => 'McTestFace', + 'billing' => array( + 'country' => 'US', + 'state' => 'WA', + ), + 'shipping' => array( + 'state' => 'CA', + 'country' => 'US', + ), + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 201, $response->get_status() ); + $this->assertEquals( + array( + 'id' => $data['id'], + 'date_created' => $data['date_created'], + 'date_created_gmt' => $data['date_created_gmt'], + 'date_modified' => $data['date_modified'], + 'date_modified_gmt' => $data['date_modified_gmt'], + 'email' => 'create_customer_test2@woo.local', + 'first_name' => 'Test', + 'last_name' => 'McTestFace', + 'role' => 'customer', + 'username' => 'create_customer_test2', + 'billing' => array( + 'first_name' => '', + 'last_name' => '', + 'company' => '', + 'address_1' => '', + 'address_2' => '', + 'city' => '', + 'state' => 'WA', + 'postcode' => '', + 'country' => 'US', + 'email' => '', + 'phone' => '', + ), + 'shipping' => array( + 'first_name' => '', + 'last_name' => '', + 'company' => '', + 'address_1' => '', + 'address_2' => '', + 'city' => '', + 'state' => 'CA', + 'postcode' => '', + 'country' => 'US', + ), + 'is_paying_customer' => false, + 'meta_data' => array(), + 'orders_count' => 0, + 'total_spent' => '0.00', + 'avatar_url' => $data['avatar_url'], + ), + $data + ); + + // Test without required field + $request = new WP_REST_Request( 'POST', '/wc/v2/customers' ); + $request->set_body_params( + array( + 'username' => 'create_customer_test3', + 'first_name' => 'Test', + 'last_name' => 'McTestFace', + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 400, $response->get_status() ); + } + + /** + * Test creating customers without valid permissions. + * + * @since 3.0.0 + */ + public function test_create_customer_without_permission() { + wp_set_current_user( 0 ); + $request = new WP_REST_Request( 'POST', '/wc/v2/customers' ); + $request->set_body_params( + array( + 'username' => 'create_customer_test_without_permission', + 'password' => 'test123', + 'email' => 'create_customer_test_without_permission@woo.local', + ) + ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test getting a single customer. + * + * @since 3.0.0 + */ + public function test_get_customer() { + wp_set_current_user( 1 ); + $customer = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\CustomerHelper::create_customer( 'get_customer_test', 'test123', 'get_customer_test@woo.local' ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/customers/' . $customer->get_id() ) ); + $data = $response->get_data(); + + $this->assertEquals( + array( + 'id' => $data['id'], + 'date_created' => $data['date_created'], + 'date_created_gmt' => $data['date_created_gmt'], + 'date_modified' => $data['date_modified'], + 'date_modified_gmt' => $data['date_modified_gmt'], + 'email' => 'get_customer_test@woo.local', + 'first_name' => 'Justin', + 'billing' => array( + 'first_name' => '', + 'last_name' => '', + 'company' => '', + 'address_1' => '123 South Street', + 'address_2' => 'Apt 1', + 'city' => 'Philadelphia', + 'state' => 'PA', + 'postcode' => '19123', + 'country' => 'US', + 'email' => '', + 'phone' => '', + ), + 'shipping' => array( + 'first_name' => '', + 'last_name' => '', + 'company' => '', + 'address_1' => '123 South Street', + 'address_2' => 'Apt 1', + 'city' => 'Philadelphia', + 'state' => 'PA', + 'postcode' => '19123', + 'country' => 'US', + ), + 'is_paying_customer' => false, + 'meta_data' => array(), + 'last_name' => '', + 'role' => 'customer', + 'username' => 'get_customer_test', + 'orders_count' => 0, + 'total_spent' => '0.00', + 'avatar_url' => $data['avatar_url'], + ), + $data + ); + } + + /** + * Test getting a single customer without valid permissions. + * + * @since 3.0.0 + */ + public function test_get_customer_without_permission() { + wp_set_current_user( 0 ); + $customer = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\CustomerHelper::create_customer( 'get_customer_test_without_permission', 'test123', 'get_customer_test_without_permission@woo.local' ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/customers/' . $customer->get_id() ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test getting a single customer with an invalid ID. + * + * @since 3.0.0 + */ + public function test_get_customer_invalid_id() { + wp_set_current_user( 1 ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/customers/0' ) ); + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * Test updating a customer. + * + * @since 3.0.0 + */ + public function test_update_customer() { + wp_set_current_user( 1 ); + $customer = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\CustomerHelper::create_customer( 'update_customer_test', 'test123', 'update_customer_test@woo.local' ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/customers/' . $customer->get_id() ) ); + $data = $response->get_data(); + $this->assertEquals( 'update_customer_test', $data['username'] ); + $this->assertEquals( 'update_customer_test@woo.local', $data['email'] ); + + $request = new WP_REST_Request( 'PUT', '/wc/v2/customers/' . $customer->get_id() ); + $request->set_body_params( + array( + 'email' => 'updated_email@woo.local', + 'first_name' => 'UpdatedTest', + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 'updated_email@woo.local', $data['email'] ); + $this->assertEquals( 'UpdatedTest', $data['first_name'] ); + } + + /** + * Test updating a customer without valid permissions. + * + * @since 3.0.0 + */ + public function test_update_customer_without_permission() { + wp_set_current_user( 0 ); + $customer = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\CustomerHelper::create_customer( 'update_customer_test_without_permission', 'test123', 'update_customer_test_without_permission@woo.local' ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/customers/' . $customer->get_id() ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test updating a customer with an invalid ID. + * + * @since 3.0.0 + */ + public function test_update_customer_invalid_id() { + wp_set_current_user( 1 ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/customers/0' ) ); + $this->assertEquals( 404, $response->get_status() ); + } + + + /** + * Test deleting a customer. + * + * @since 3.0.0 + */ + public function test_delete_customer() { + wp_set_current_user( 1 ); + $customer = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\CustomerHelper::create_customer( 'delete_customer_test', 'test123', 'delete_customer_test@woo.local' ); + $request = new WP_REST_Request( 'DELETE', '/wc/v2/customers/' . $customer->get_id() ); + $request->set_param( 'force', true ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 200, $response->get_status() ); + } + + /** + * Test deleting a customer with an invalid ID. + * + * @since 3.0.0 + */ + public function test_delete_customer_invalid_id() { + wp_set_current_user( 1 ); + $request = new WP_REST_Request( 'DELETE', '/wc/v2/customers/0' ); + $request->set_param( 'force', true ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 400, $response->get_status() ); + } + + /** + * Test deleting a customer without valid permissions. + * + * @since 3.0.0 + */ + public function test_delete_customer_without_permission() { + wp_set_current_user( 0 ); + $customer = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\CustomerHelper::create_customer( 'delete_customer_test_without_permission', 'test123', 'delete_customer_test_without_permission@woo.local' ); + $request = new WP_REST_Request( 'DELETE', '/wc/v2/customers/' . $customer->get_id() ); + $request->set_param( 'force', true ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test customer batch endpoint. + * + * @since 3.0.0 + */ + public function test_batch_customer() { + wp_set_current_user( 1 ); + + $customer_1 = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\CustomerHelper::create_customer( 'test_batch_customer', 'test123', 'test_batch_customer@woo.local' ); + $customer_2 = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\CustomerHelper::create_customer( 'test_batch_customer2', 'test123', 'test_batch_customer2@woo.local' ); + $customer_3 = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\CustomerHelper::create_customer( 'test_batch_customer3', 'test123', 'test_batch_customer3@woo.local' ); + $customer_4 = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\CustomerHelper::create_customer( 'test_batch_customer4', 'test123', 'test_batch_customer4@woo.local' ); + + $request = new WP_REST_Request( 'POST', '/wc/v2/customers/batch' ); + $request->set_body_params( + array( + 'update' => array( + array( + 'id' => $customer_1->get_id(), + 'last_name' => 'McTest', + ), + ), + 'delete' => array( + $customer_2->get_id(), + $customer_3->get_id(), + ), + 'create' => array( + array( + 'username' => 'newuser', + 'password' => 'test123', + 'email' => 'newuser@woo.local', + ), + ), + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 'McTest', $data['update'][0]['last_name'] ); + $this->assertEquals( 'newuser', $data['create'][0]['username'] ); + $this->assertEmpty( $data['create'][0]['last_name'] ); + $this->assertEquals( $customer_2->get_id(), $data['delete'][0]['id'] ); + $this->assertEquals( $customer_3->get_id(), $data['delete'][1]['id'] ); + + $request = new WP_REST_Request( 'GET', '/wc/v2/customers' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 3, count( $data ) ); + } + + /** + * Test customer schema. + * + * @since 3.0.0 + */ + public function test_customer_schema() { + wp_set_current_user( 1 ); + $request = new WP_REST_Request( 'OPTIONS', '/wc/v2/customers' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $properties = $data['schema']['properties']; + + $this->assertEquals( 18, count( $properties ) ); + $this->assertArrayHasKey( 'id', $properties ); + $this->assertArrayHasKey( 'date_created', $properties ); + $this->assertArrayHasKey( 'date_created_gmt', $properties ); + $this->assertArrayHasKey( 'date_modified', $properties ); + $this->assertArrayHasKey( 'date_modified_gmt', $properties ); + $this->assertArrayHasKey( 'email', $properties ); + $this->assertArrayHasKey( 'first_name', $properties ); + $this->assertArrayHasKey( 'last_name', $properties ); + $this->assertArrayHasKey( 'role', $properties ); + $this->assertArrayHasKey( 'username', $properties ); + $this->assertArrayHasKey( 'password', $properties ); + $this->assertArrayHasKey( 'orders_count', $properties ); + $this->assertArrayHasKey( 'total_spent', $properties ); + $this->assertArrayHasKey( 'avatar_url', $properties ); + $this->assertArrayHasKey( 'billing', $properties ); + $this->assertArrayHasKey( 'first_name', $properties['billing']['properties'] ); + $this->assertArrayHasKey( 'last_name', $properties['billing']['properties'] ); + $this->assertArrayHasKey( 'company', $properties['billing']['properties'] ); + $this->assertArrayHasKey( 'address_1', $properties['billing']['properties'] ); + $this->assertArrayHasKey( 'address_2', $properties['billing']['properties'] ); + $this->assertArrayHasKey( 'city', $properties['billing']['properties'] ); + $this->assertArrayHasKey( 'state', $properties['billing']['properties'] ); + $this->assertArrayHasKey( 'postcode', $properties['billing']['properties'] ); + $this->assertArrayHasKey( 'country', $properties['billing']['properties'] ); + $this->assertArrayHasKey( 'email', $properties['billing']['properties'] ); + $this->assertArrayHasKey( 'phone', $properties['billing']['properties'] ); + $this->assertArrayHasKey( 'shipping', $properties ); + $this->assertArrayHasKey( 'first_name', $properties['shipping']['properties'] ); + $this->assertArrayHasKey( 'last_name', $properties['shipping']['properties'] ); + $this->assertArrayHasKey( 'company', $properties['shipping']['properties'] ); + $this->assertArrayHasKey( 'address_1', $properties['shipping']['properties'] ); + $this->assertArrayHasKey( 'address_2', $properties['shipping']['properties'] ); + $this->assertArrayHasKey( 'city', $properties['shipping']['properties'] ); + $this->assertArrayHasKey( 'state', $properties['shipping']['properties'] ); + $this->assertArrayHasKey( 'postcode', $properties['shipping']['properties'] ); + $this->assertArrayHasKey( 'country', $properties['shipping']['properties'] ); + } +} diff --git a/tests/legacy/unit-tests/rest-api/Tests/Version2/orders.php b/tests/legacy/unit-tests/rest-api/Tests/Version2/orders.php new file mode 100644 index 00000000000..e80fbab5fb8 --- /dev/null +++ b/tests/legacy/unit-tests/rest-api/Tests/Version2/orders.php @@ -0,0 +1,721 @@ +endpoint = new WC_REST_Orders_Controller(); + $this->user = $this->factory->user->create( + array( + 'role' => 'administrator', + ) + ); + } + + /** + * Test route registration. + * @since 3.0.0 + */ + public function test_register_routes() { + $routes = $this->server->get_routes(); + $this->assertArrayHasKey( '/wc/v2/orders', $routes ); + $this->assertArrayHasKey( '/wc/v2/orders/batch', $routes ); + $this->assertArrayHasKey( '/wc/v2/orders/(?P[\d]+)', $routes ); + } + + /** + * Test getting all orders. + * @since 3.0.0 + */ + public function test_get_items() { + wp_set_current_user( $this->user ); + + // Create 10 orders. + for ( $i = 0; $i < 10; $i++ ) { + $this->orders[] = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order( $this->user ); + } + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/orders' ) ); + $orders = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 10, count( $orders ) ); + } + + /** + * Tests to make sure orders cannot be viewed without valid permissions. + * + * @since 3.0.0 + */ + public function test_get_items_without_permission() { + wp_set_current_user( 0 ); + $this->orders[] = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order(); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/orders' ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Tests getting a single order. + * @since 3.0.0 + */ + public function test_get_item() { + wp_set_current_user( $this->user ); + $order = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order(); + $order->add_meta_data( 'key', 'value' ); + $order->add_meta_data( 'key2', 'value2' ); + $order->save(); + $this->orders[] = $order; + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/orders/' . $order->get_id() ) ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( $order->get_id(), $data['id'] ); + + // Test meta data is set. + $this->assertEquals( 'key', $data['meta_data'][0]->key ); + $this->assertEquals( 'value', $data['meta_data'][0]->value ); + $this->assertEquals( 'key2', $data['meta_data'][1]->key ); + $this->assertEquals( 'value2', $data['meta_data'][1]->value ); + } + + /** + * Tests getting a single order without the correct permissions. + * @since 3.0.0 + */ + public function test_get_item_without_permission() { + wp_set_current_user( 0 ); + $order = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order(); + $this->orders[] = $order; + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/orders/' . $order->get_id() ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Tests getting an order with an invalid ID. + * @since 3.0.0 + */ + public function test_get_item_invalid_id() { + wp_set_current_user( $this->user ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/orders/99999999' ) ); + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * Tests getting an order with an invalid ID. + * @since 3.5.0 + */ + public function test_get_item_refund_id() { + wp_set_current_user( $this->user ); + $order = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order(); + $refund = wc_create_refund( + array( + 'order_id' => $order->get_id(), + ) + ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/orders/' . $refund->get_id() ) ); + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * Tests creating an order. + * @since 3.0.0 + */ + public function test_create_order() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $request = new WP_REST_Request( 'POST', '/wc/v2/orders' ); + $request->set_body_params( + array( + 'payment_method' => 'bacs', + 'payment_method_title' => 'Direct Bank Transfer', + 'set_paid' => true, + 'billing' => array( + 'first_name' => 'John', + 'last_name' => 'Doe', + 'address_1' => '969 Market', + 'address_2' => '', + 'city' => 'San Francisco', + 'state' => 'CA', + 'postcode' => '94103', + 'country' => 'US', + 'email' => 'john.doe@example.com', + 'phone' => '(555) 555-5555', + ), + 'shipping' => array( + 'first_name' => 'John', + 'last_name' => 'Doe', + 'address_1' => '969 Market', + 'address_2' => '', + 'city' => 'San Francisco', + 'state' => 'CA', + 'postcode' => '94103', + 'country' => 'US', + ), + 'line_items' => array( + array( + 'product_id' => $product->get_id(), + 'quantity' => 2, + ), + ), + 'shipping_lines' => array( + array( + 'method_id' => 'flat_rate', + 'method_title' => 'Flat rate', + 'total' => '10.00', + 'instance_id' => '1', + ), + ), + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $order = wc_get_order( $data['id'] ); + $this->assertEquals( 201, $response->get_status() ); + $this->assertEquals( $order->get_payment_method(), $data['payment_method'] ); + $this->assertEquals( $order->get_payment_method_title(), $data['payment_method_title'] ); + $this->assertEquals( $order->get_billing_first_name(), $data['billing']['first_name'] ); + $this->assertEquals( $order->get_billing_last_name(), $data['billing']['last_name'] ); + $this->assertEquals( '', $data['billing']['company'] ); + $this->assertEquals( $order->get_billing_address_1(), $data['billing']['address_1'] ); + $this->assertEquals( $order->get_billing_address_2(), $data['billing']['address_2'] ); + $this->assertEquals( $order->get_billing_city(), $data['billing']['city'] ); + $this->assertEquals( $order->get_billing_state(), $data['billing']['state'] ); + $this->assertEquals( $order->get_billing_postcode(), $data['billing']['postcode'] ); + $this->assertEquals( $order->get_billing_country(), $data['billing']['country'] ); + $this->assertEquals( $order->get_billing_email(), $data['billing']['email'] ); + $this->assertEquals( $order->get_billing_phone(), $data['billing']['phone'] ); + $this->assertEquals( $order->get_shipping_first_name(), $data['shipping']['first_name'] ); + $this->assertEquals( $order->get_shipping_last_name(), $data['shipping']['last_name'] ); + $this->assertEquals( '', $data['shipping']['company'] ); + $this->assertEquals( $order->get_shipping_address_1(), $data['shipping']['address_1'] ); + $this->assertEquals( $order->get_shipping_address_2(), $data['shipping']['address_2'] ); + $this->assertEquals( $order->get_shipping_city(), $data['shipping']['city'] ); + $this->assertEquals( $order->get_shipping_state(), $data['shipping']['state'] ); + $this->assertEquals( $order->get_shipping_postcode(), $data['shipping']['postcode'] ); + $this->assertEquals( $order->get_shipping_country(), $data['shipping']['country'] ); + $this->assertEquals( 1, count( $data['line_items'] ) ); + $this->assertEquals( 1, count( $data['shipping_lines'] ) ); + $shipping = current( $order->get_items( 'shipping' ) ); + $expected = array( + 'id' => $shipping->get_id(), + 'method_title' => $shipping->get_method_title(), + 'method_id' => $shipping->get_method_id(), + 'instance_id' => $shipping->get_instance_id(), + 'total' => wc_format_decimal( $shipping->get_total(), '' ), + 'total_tax' => wc_format_decimal( $shipping->get_total_tax(), '' ), + 'taxes' => array(), + 'meta_data' => $shipping->get_meta_data(), + ); + $this->assertEquals( $expected, $data['shipping_lines'][0] ); + } + + /** + * Test the sanitization of the payment_method_title field through the API. + * + * @since 3.5.2 + */ + public function test_create_update_order_payment_method_title_sanitize() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + + // Test when creating order. + $request = new WP_REST_Request( 'POST', '/wc/v3/orders' ); + $request->set_body_params( + array( + 'payment_method' => 'bacs', + 'payment_method_title' => '

Sanitize this

', + 'set_paid' => true, + 'billing' => array( + 'first_name' => 'John', + 'last_name' => 'Doe', + 'address_1' => '969 Market', + 'address_2' => '', + 'city' => 'San Francisco', + 'state' => 'CA', + 'postcode' => '94103', + 'country' => 'US', + 'email' => 'john.doe@example.com', + 'phone' => '(555) 555-5555', + ), + 'shipping' => array( + 'first_name' => 'John', + 'last_name' => 'Doe', + 'address_1' => '969 Market', + 'address_2' => '', + 'city' => 'San Francisco', + 'state' => 'CA', + 'postcode' => '94103', + 'country' => 'US', + ), + 'line_items' => array( + array( + 'product_id' => $product->get_id(), + 'quantity' => 2, + ), + ), + 'shipping_lines' => array( + array( + 'method_id' => 'flat_rate', + 'method_title' => 'Flat rate', + 'total' => '10', + ), + ), + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $order = wc_get_order( $data['id'] ); + $this->assertEquals( 201, $response->get_status() ); + $this->assertEquals( $order->get_payment_method(), $data['payment_method'] ); + $this->assertEquals( $order->get_payment_method_title(), 'Sanitize this' ); + + // Test when updating order. + $request = new WP_REST_Request( 'PUT', '/wc/v3/orders/' . $data['id'] ); + $request->set_body_params( + array( + 'payment_method' => 'bacs', + 'payment_method_title' => '

Sanitize this too

', + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $order = wc_get_order( $data['id'] ); + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( $order->get_payment_method(), $data['payment_method'] ); + $this->assertEquals( $order->get_payment_method_title(), 'Sanitize this too' ); + } + + /** + * Tests creating an order without required fields. + * @since 3.0.0 + */ + public function test_create_order_invalid_fields() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + + // non-existent customer + $request = new WP_REST_Request( 'POST', '/wc/v2/orders' ); + $request->set_body_params( + array( + 'payment_method' => 'bacs', + 'payment_method_title' => 'Direct Bank Transfer', + 'set_paid' => true, + 'customer_id' => 99999, + 'billing' => array( + 'first_name' => 'John', + 'last_name' => 'Doe', + 'address_1' => '969 Market', + 'address_2' => '', + 'city' => 'San Francisco', + 'state' => 'CA', + 'postcode' => '94103', + 'country' => 'US', + 'email' => 'john.doe@example.com', + 'phone' => '(555) 555-5555', + ), + 'shipping' => array( + 'first_name' => 'John', + 'last_name' => 'Doe', + 'address_1' => '969 Market', + 'address_2' => '', + 'city' => 'San Francisco', + 'state' => 'CA', + 'postcode' => '94103', + 'country' => 'US', + ), + 'line_items' => array( + array( + 'product_id' => $product->get_id(), + 'quantity' => 2, + ), + ), + 'shipping_lines' => array( + array( + 'method_id' => 'flat_rate', + 'method_title' => 'Flat rate', + 'total' => 10, + ), + ), + ) + ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 400, $response->get_status() ); + } + + /** + * Tests create an order with an invalid product. + * + * @since 3.9.0 + */ + public function test_create_order_with_invalid_product() { + wp_set_current_user( $this->user ); + + $request = new WP_REST_Request( 'POST', '/wc/v2/orders' ); + $request->set_body_params( + array( + 'line_items' => array( + array( + 'quantity' => 2, + ), + ), + ) + ); + + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $this->assertEquals( 'woocommerce_rest_required_product_reference', $data['code'] ); + $this->assertEquals( 400, $response->get_status() ); + } + + /** + * Tests updating an order. + * + * @since 3.0.0 + */ + public function test_update_order() { + wp_set_current_user( $this->user ); + $order = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order(); + $request = new WP_REST_Request( 'PUT', '/wc/v2/orders/' . $order->get_id() ); + $request->set_body_params( + array( + 'payment_method' => 'test-update', + 'billing' => array( + 'first_name' => 'Fish', + 'last_name' => 'Face', + ), + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 'test-update', $data['payment_method'] ); + $this->assertEquals( 'Fish', $data['billing']['first_name'] ); + $this->assertEquals( 'Face', $data['billing']['last_name'] ); + } + + /** + * Tests updating an order and removing items. + * + * @since 3.0.0 + */ + public function test_update_order_remove_items() { + wp_set_current_user( $this->user ); + $order = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order(); + $fee = new WC_Order_Item_Fee(); + $fee->set_props( + array( + 'name' => 'Some Fee', + 'tax_status' => 'taxable', + 'total' => '100', + 'tax_class' => '', + ) + ); + $order->add_item( $fee ); + $order->save(); + + $request = new WP_REST_Request( 'PUT', '/wc/v2/orders/' . $order->get_id() ); + $fee_data = current( $order->get_items( 'fee' ) ); + + $request->set_body_params( + array( + 'fee_lines' => array( + array( + 'id' => $fee_data->get_id(), + 'name' => null, + ), + ), + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertTrue( empty( $data['fee_lines'] ) ); + } + + /** + * Tests updating an order after deleting a product. + * + * @since 3.9.0 + */ + public function test_update_order_after_delete_product() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $order = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order( 1, $product ); + $product->delete( true ); + + $request = new WP_REST_Request( 'PUT', '/wc/v2/orders/' . $order->get_id() ); + $line_items = $order->get_items( 'line_item' ); + $item = current( $line_items ); + + $request->set_body_params( + array( + 'line_items' => array( + array( + 'id' => $item->get_id(), + 'quantity' => 10, + ), + ), + ) + ); + + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $expected = array( + 'id' => $item->get_id(), + 'name' => 'Dummy Product', + 'product_id' => 0, + 'variation_id' => 0, + 'quantity' => 10, + 'tax_class' => '', + 'subtotal' => '40.00', + 'subtotal_tax' => '0.00', + 'total' => '40.00', + 'total_tax' => '0.00', + 'taxes' => array(), + 'meta_data' => array(), + 'sku' => null, + 'price' => 4, + ); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( $expected, $data['line_items'][0] ); + } + + /** + * Tests updating an order and adding a coupon. + * + * @since 3.3.0 + */ + public function test_update_order_add_coupons() { + wp_set_current_user( $this->user ); + $order = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order(); + $order_item = current( $order->get_items() ); + $coupon = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\CouponHelper::create_coupon( 'fake-coupon' ); + $coupon->set_amount( 5 ); + $coupon->save(); + $request = new WP_REST_Request( 'PUT', '/wc/v2/orders/' . $order->get_id() ); + $request->set_body_params( + array( + 'coupon_lines' => array( + array( + 'code' => 'fake-coupon', + 'discount_total' => '5', + 'discount_tax' => '0', + ), + ), + 'line_items' => array( + array( + 'id' => $order_item->get_id(), + 'product_id' => $order_item->get_product_id(), + 'total' => '35.00', + ), + ), + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertCount( 1, $data['coupon_lines'] ); + $this->assertEquals( '45.00', $data['total'] ); + } + + /** + * Tests updating an order and removing a coupon. + * + * @since 3.3.0 + */ + public function test_update_order_remove_coupons() { + wp_set_current_user( $this->user ); + $order = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order(); + $order_item = current( $order->get_items() ); + $coupon = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\CouponHelper::create_coupon( 'fake-coupon' ); + $coupon->set_amount( 5 ); + $coupon->save(); + + $order->apply_coupon( $coupon ); + $order->save(); + + // Check that the coupon is applied. + $this->assertEquals( '45.00', $order->get_total() ); + + $request = new WP_REST_Request( 'PUT', '/wc/v2/orders/' . $order->get_id() ); + $coupon_data = current( $order->get_items( 'coupon' ) ); + + $request->set_body_params( + array( + 'coupon_lines' => array( + array( + 'id' => $coupon_data->get_id(), + 'code' => null, + ), + ), + 'line_items' => array( + array( + 'id' => $order_item->get_id(), + 'product_id' => $order_item->get_product_id(), + 'total' => '40.00', + ), + ), + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertTrue( empty( $data['coupon_lines'] ) ); + $this->assertEquals( '50.00', $data['total'] ); + } + + /** + * Tests updating an order without the correct permissions. + * + * @since 3.0.0 + */ + public function test_update_order_without_permission() { + wp_set_current_user( 0 ); + $order = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order(); + $request = new WP_REST_Request( 'PUT', '/wc/v2/orders/' . $order->get_id() ); + $request->set_body_params( + array( + 'payment_method' => 'test-update', + 'billing' => array( + 'first_name' => 'Fish', + 'last_name' => 'Face', + ), + ) + ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Tests that updating an order with an invalid id fails. + * @since 3.0.0 + */ + public function test_update_order_invalid_id() { + wp_set_current_user( $this->user ); + $request = new WP_REST_Request( 'POST', '/wc/v2/orders/999999' ); + $request->set_body_params( + array( + 'payment_method' => 'test-update', + 'billing' => array( + 'first_name' => 'Fish', + 'last_name' => 'Face', + ), + ) + ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 400, $response->get_status() ); + } + + /** + * Test deleting an order. + * @since 3.0.0 + */ + public function test_delete_order() { + wp_set_current_user( $this->user ); + $order = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order(); + $request = new WP_REST_Request( 'DELETE', '/wc/v2/orders/' . $order->get_id() ); + $request->set_param( 'force', true ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( null, get_post( $order->get_id() ) ); + } + + /** + * Test deleting an order without permission/creds. + * @since 3.0.0 + */ + public function test_delete_order_without_permission() { + wp_set_current_user( 0 ); + $order = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order(); + $request = new WP_REST_Request( 'DELETE', '/wc/v2/orders/' . $order->get_id() ); + $request->set_param( 'force', true ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test deleting an order with an invalid id. + * + * @since 3.0.0 + */ + public function test_delete_order_invalid_id() { + wp_set_current_user( $this->user ); + $request = new WP_REST_Request( 'DELETE', '/wc/v2/orders/9999999' ); + $request->set_param( 'force', true ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * Test batch managing product reviews. + */ + public function test_orders_batch() { + wp_set_current_user( $this->user ); + + $order1 = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order(); + $order2 = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order(); + $order3 = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order(); + + $request = new WP_REST_Request( 'POST', '/wc/v2/orders/batch' ); + $request->set_body_params( + array( + 'update' => array( + array( + 'id' => $order1->get_id(), + 'payment_method' => 'updated', + ), + ), + 'delete' => array( + $order2->get_id(), + $order3->get_id(), + ), + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 'updated', $data['update'][0]['payment_method'] ); + $this->assertEquals( $order2->get_id(), $data['delete'][0]['id'] ); + $this->assertEquals( $order3->get_id(), $data['delete'][1]['id'] ); + + $request = new WP_REST_Request( 'GET', '/wc/v2/orders' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $this->assertEquals( 1, count( $data ) ); + } + + /** + * Test the order schema. + * @since 3.0.0 + */ + public function test_order_schema() { + wp_set_current_user( $this->user ); + $order = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order(); + $request = new WP_REST_Request( 'OPTIONS', '/wc/v2/orders/' . $order->get_id() ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $properties = $data['schema']['properties']; + + $this->assertEquals( 42, count( $properties ) ); + $this->assertArrayHasKey( 'id', $properties ); + } +} diff --git a/tests/legacy/unit-tests/rest-api/Tests/Version2/payment-gateways.php b/tests/legacy/unit-tests/rest-api/Tests/Version2/payment-gateways.php new file mode 100644 index 00000000000..d8757c5d31d --- /dev/null +++ b/tests/legacy/unit-tests/rest-api/Tests/Version2/payment-gateways.php @@ -0,0 +1,337 @@ +endpoint = new WC_REST_Payment_Gateways_Controller(); + $this->user = $this->factory->user->create( + array( + 'role' => 'administrator', + ) + ); + } + + /** + * Test route registration. + * + * @since 3.0.0 + */ + public function test_register_routes() { + $routes = $this->server->get_routes(); + $this->assertArrayHasKey( '/wc/v2/payment_gateways', $routes ); + $this->assertArrayHasKey( '/wc/v2/payment_gateways/(?P[\w-]+)', $routes ); + } + + /** + * Test getting all payment gateways. + * + * @since 3.0.0 + */ + public function test_get_payment_gateways() { + wp_set_current_user( $this->user ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/payment_gateways' ) ); + $gateways = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertContains( + array( + 'id' => 'cheque', + 'title' => 'Check payments', + 'description' => 'Please send a check to Store Name, Store Street, Store Town, Store State / County, Store Postcode.', + 'order' => '', + 'enabled' => false, + 'method_title' => 'Check payments', + 'method_description' => 'Take payments in person via checks. This offline gateway can also be useful to test purchases.', + 'settings' => array_diff_key( + $this->get_settings( 'WC_Gateway_Cheque' ), + array( + 'enabled' => false, + 'description' => false, + ) + ), + '_links' => array( + 'self' => array( + array( + 'href' => rest_url( '/wc/v2/payment_gateways/cheque' ), + ), + ), + 'collection' => array( + array( + 'href' => rest_url( '/wc/v2/payment_gateways' ), + ), + ), + ), + ), + $gateways + ); + } + + /** + * Tests to make sure payment gateways cannot viewed without valid permissions. + * + * @since 3.0.0 + */ + public function test_get_payment_gateways_without_permission() { + wp_set_current_user( 0 ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/payment_gateways' ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test getting a single payment gateway. + * + * @since 3.0.0 + */ + public function test_get_payment_gateway() { + wp_set_current_user( $this->user ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/payment_gateways/paypal' ) ); + $paypal = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( + array( + 'id' => 'paypal', + 'title' => 'PayPal', + 'description' => "Pay via PayPal; you can pay with your credit card if you don't have a PayPal account.", + 'order' => '', + 'enabled' => false, + 'method_title' => 'PayPal', + 'method_description' => 'PayPal Standard redirects customers to PayPal to enter their payment information.', + 'settings' => array_diff_key( + $this->get_settings( 'WC_Gateway_Paypal' ), + array( + 'enabled' => false, + 'description' => false, + ) + ), + ), + $paypal + ); + } + + /** + * Test getting a payment gateway without valid permissions. + * + * @since 3.0.0 + */ + public function test_get_payment_gateway_without_permission() { + wp_set_current_user( 0 ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/payment_gateways/paypal' ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test getting a payment gateway with an invalid id. + * + * @since 3.0.0 + */ + public function test_get_payment_gateway_invalid_id() { + wp_set_current_user( $this->user ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/payment_gateways/totally_fake_method' ) ); + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * Test updating a single payment gateway. + * + * @since 3.0.0 + */ + public function test_update_payment_gateway() { + wp_set_current_user( $this->user ); + + // Test defaults + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/payment_gateways/paypal' ) ); + $paypal = $response->get_data(); + + $this->assertEquals( 'PayPal', $paypal['settings']['title']['value'] ); + $this->assertEquals( 'admin@example.org', $paypal['settings']['email']['value'] ); + $this->assertEquals( 'no', $paypal['settings']['testmode']['value'] ); + + // test updating single setting + $request = new WP_REST_Request( 'POST', '/wc/v2/payment_gateways/paypal' ); + $request->set_body_params( + array( + 'settings' => array( + 'email' => 'woo@woo.local', + ), + ) + ); + $response = $this->server->dispatch( $request ); + $paypal = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 'PayPal', $paypal['settings']['title']['value'] ); + $this->assertEquals( 'woo@woo.local', $paypal['settings']['email']['value'] ); + $this->assertEquals( 'no', $paypal['settings']['testmode']['value'] ); + + // test updating multiple settings + $request = new WP_REST_Request( 'POST', '/wc/v2/payment_gateways/paypal' ); + $request->set_body_params( + array( + 'settings' => array( + 'testmode' => 'yes', + 'title' => 'PayPal - New Title', + ), + ) + ); + $response = $this->server->dispatch( $request ); + $paypal = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 'PayPal - New Title', $paypal['settings']['title']['value'] ); + $this->assertEquals( 'woo@woo.local', $paypal['settings']['email']['value'] ); + $this->assertEquals( 'yes', $paypal['settings']['testmode']['value'] ); + + // Test other parameters, and recheck settings + $request = new WP_REST_Request( 'POST', '/wc/v2/payment_gateways/paypal' ); + $request->set_body_params( + array( + 'enabled' => false, + 'order' => 2, + ) + ); + $response = $this->server->dispatch( $request ); + $paypal = $response->get_data(); + + $this->assertFalse( $paypal['enabled'] ); + $this->assertEquals( 2, $paypal['order'] ); + $this->assertEquals( 'PayPal - New Title', $paypal['settings']['title']['value'] ); + $this->assertEquals( 'woo@woo.local', $paypal['settings']['email']['value'] ); + $this->assertEquals( 'yes', $paypal['settings']['testmode']['value'] ); + + // test bogus + $request = new WP_REST_Request( 'POST', '/wc/v2/payment_gateways/paypal' ); + $request->set_body_params( + array( + 'settings' => array( + 'paymentaction' => 'afasfasf', + ), + ) + ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 400, $response->get_status() ); + + $request = new WP_REST_Request( 'POST', '/wc/v2/payment_gateways/paypal' ); + $request->set_body_params( + array( + 'settings' => array( + 'paymentaction' => 'authorization', + ), + ) + ); + $response = $this->server->dispatch( $request ); + $paypal = $response->get_data(); + $this->assertEquals( 'authorization', $paypal['settings']['paymentaction']['value'] ); + } + + /** + * Test updating a payment gateway without valid permissions. + * + * @since 3.0.0 + */ + public function test_update_payment_gateway_without_permission() { + wp_set_current_user( 0 ); + $request = new WP_REST_Request( 'POST', '/wc/v2/payment_gateways/paypal' ); + $request->set_body_params( + array( + 'settings' => array( + 'testmode' => 'yes', + 'title' => 'PayPal - New Title', + ), + ) + ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test updating a payment gateway with an invalid id. + * + * @since 3.0.0 + */ + public function test_update_payment_gateway_invalid_id() { + wp_set_current_user( $this->user ); + $request = new WP_REST_Request( 'POST', '/wc/v2/payment_gateways/totally_fake_method' ); + $request->set_body_params( + array( + 'enabled' => true, + ) + ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * Test the payment gateway schema. + * + * @since 3.0.0 + */ + public function test_payment_gateway_schema() { + wp_set_current_user( $this->user ); + + $request = new WP_REST_Request( 'OPTIONS', '/wc/v2/payment_gateways' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $properties = $data['schema']['properties']; + + $this->assertEquals( 8, count( $properties ) ); + $this->assertArrayHasKey( 'id', $properties ); + $this->assertArrayHasKey( 'title', $properties ); + $this->assertArrayHasKey( 'description', $properties ); + $this->assertArrayHasKey( 'order', $properties ); + $this->assertArrayHasKey( 'enabled', $properties ); + $this->assertArrayHasKey( 'method_title', $properties ); + $this->assertArrayHasKey( 'method_description', $properties ); + $this->assertArrayHasKey( 'settings', $properties ); + } + + /** + * Loads a particular gateway's settings so we can correctly test API output. + * + * @since 3.0.0 + * @param string $gateway_class Name of WC_Payment_Gateway class. + */ + private function get_settings( $gateway_class ) { + $gateway = new $gateway_class(); + $settings = array(); + $gateway->init_form_fields(); + foreach ( $gateway->form_fields as $id => $field ) { + // Make sure we at least have a title and type + if ( empty( $field['title'] ) || empty( $field['type'] ) ) { + continue; + } + // Ignore 'title' settings/fields -- they are UI only + if ( 'title' === $field['type'] ) { + continue; + } + $data = array( + 'id' => $id, + 'label' => empty( $field['label'] ) ? $field['title'] : $field['label'], + 'description' => empty( $field['description'] ) ? '' : $field['description'], + 'type' => $field['type'], + 'value' => $gateway->settings[ $id ], + 'default' => empty( $field['default'] ) ? '' : $field['default'], + 'tip' => empty( $field['description'] ) ? '' : $field['description'], + 'placeholder' => empty( $field['placeholder'] ) ? '' : $field['placeholder'], + ); + if ( ! empty( $field['options'] ) ) { + $data['options'] = $field['options']; + } + $settings[ $id ] = $data; + } + return $settings; + } + +} diff --git a/tests/legacy/unit-tests/rest-api/Tests/Version2/product-reviews.php b/tests/legacy/unit-tests/rest-api/Tests/Version2/product-reviews.php new file mode 100644 index 00000000000..57f8804c1c9 --- /dev/null +++ b/tests/legacy/unit-tests/rest-api/Tests/Version2/product-reviews.php @@ -0,0 +1,468 @@ +user = $this->factory->user->create( + array( + 'role' => 'administrator', + ) + ); + } + + /** + * Test route registration. + * + * @since 3.0.0 + */ + public function test_register_routes() { + $routes = $this->server->get_routes(); + $this->assertArrayHasKey( '/wc/v3/products/reviews', $routes ); + $this->assertArrayHasKey( '/wc/v3/products/reviews/(?P[\d]+)', $routes ); + $this->assertArrayHasKey( '/wc/v3/products/reviews/batch', $routes ); + } + + /** + * Test getting all product reviews. + * + * @since 3.0.0 + */ + public function test_get_product_reviews() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + // Create 10 products reviews for the product + for ( $i = 0; $i < 10; $i++ ) { + $review_id = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_product_review( $product->get_id() ); + } + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/products/reviews' ) ); + $product_reviews = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 10, count( $product_reviews ) ); + $this->assertContains( + array( + 'id' => $review_id, + 'date_created' => $product_reviews[0]['date_created'], + 'date_created_gmt' => $product_reviews[0]['date_created_gmt'], + 'product_id' => $product->get_id(), + 'status' => 'approved', + 'reviewer' => 'admin', + 'reviewer_email' => 'woo@woo.local', + 'review' => "

Review content here

\n", + 'rating' => 0, + 'verified' => false, + 'reviewer_avatar_urls' => $product_reviews[0]['reviewer_avatar_urls'], + '_links' => array( + 'self' => array( + array( + 'href' => rest_url( '/wc/v3/products/reviews/' . $review_id ), + ), + ), + 'collection' => array( + array( + 'href' => rest_url( '/wc/v3/products/reviews' ), + ), + ), + 'up' => array( + array( + 'href' => rest_url( '/wc/v3/products/' . $product->get_id() ), + ), + ), + ), + ), + $product_reviews + ); + } + + /** + * Tests to make sure product reviews cannot be viewed without valid permissions. + * + * @since 3.0.0 + */ + public function test_get_product_reviews_without_permission() { + wp_set_current_user( 0 ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/products/reviews' ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Tests to make sure an error is returned when an invalid product is loaded. + * + * @since 3.0.0 + */ + public function test_get_product_reviews_invalid_product() { + wp_set_current_user( $this->user ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/products/0/reviews' ) ); + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * Tests getting a single product review. + * + * @since 3.0.0 + */ + public function test_get_product_review() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $product_review_id = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_product_review( $product->get_id() ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/products/reviews/' . $product_review_id ) ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( + array( + 'id' => $product_review_id, + 'date_created' => $data['date_created'], + 'date_created_gmt' => $data['date_created_gmt'], + 'product_id' => $product->get_id(), + 'status' => 'approved', + 'reviewer' => 'admin', + 'reviewer_email' => 'woo@woo.local', + 'review' => "

Review content here

\n", + 'rating' => 0, + 'verified' => false, + 'reviewer_avatar_urls' => $data['reviewer_avatar_urls'], + ), + $data + ); + } + + /** + * Tests getting a single product review without the correct permissions. + * + * @since 3.0.0 + */ + public function test_get_product_review_without_permission() { + wp_set_current_user( 0 ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $product_review_id = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_product_review( $product->get_id() ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/products/reviews/' . $product_review_id ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Tests getting a product review with an invalid ID. + * + * @since 3.0.0 + */ + public function test_get_product_review_invalid_id() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/products/reviews/0' ) ); + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * Tests creating a product review. + * + * @since 3.0.0 + */ + public function test_create_product_review() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $request = new WP_REST_Request( 'POST', '/wc/v3/products/reviews' ); + $request->set_body_params( + array( + 'review' => 'Hello world.', + 'reviewer' => 'Admin', + 'reviewer_email' => 'woo@woo.local', + 'rating' => '5', + 'product_id' => $product->get_id(), + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 201, $response->get_status() ); + $this->assertEquals( + array( + 'id' => $data['id'], + 'date_created' => $data['date_created'], + 'date_created_gmt' => $data['date_created_gmt'], + 'product_id' => $product->get_id(), + 'status' => 'approved', + 'reviewer' => 'Admin', + 'reviewer_email' => 'woo@woo.local', + 'review' => 'Hello world.', + 'rating' => 5, + 'verified' => false, + 'reviewer_avatar_urls' => $data['reviewer_avatar_urls'], + ), + $data + ); + } + + /** + * Tests creating a product review without required fields. + * + * @since 3.0.0 + */ + public function test_create_product_review_invalid_fields() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + + // missing review + $request = new WP_REST_Request( 'POST', '/wc/v3/products/reviews' ); + $request->set_body_params( + array( + 'reviewer' => 'Admin', + 'reviewer_email' => 'woo@woo.local', + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 400, $response->get_status() ); + + // Missing reviewer. + $request = new WP_REST_Request( 'POST', '/wc/v3/products/reviews' ); + $request->set_body_params( + array( + 'review' => 'Hello world.', + 'reviewer_email' => 'woo@woo.local', + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 400, $response->get_status() ); + + // missing reviewer_email + $request = new WP_REST_Request( 'POST', '/wc/v3/products/reviews' ); + $request->set_body_params( + array( + 'review' => 'Hello world.', + 'reviewer' => 'Admin', + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 400, $response->get_status() ); + } + + /** + * Tests updating a product review. + * + * @since 3.0.0 + */ + public function test_update_product_review() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $product_review_id = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_product_review( $product->get_id() ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/products/reviews/' . $product_review_id ) ); + $data = $response->get_data(); + $this->assertEquals( "

Review content here

\n", $data['review'] ); + $this->assertEquals( 'admin', $data['reviewer'] ); + $this->assertEquals( 'woo@woo.local', $data['reviewer_email'] ); + $this->assertEquals( 0, $data['rating'] ); + + $request = new WP_REST_Request( 'PUT', '/wc/v3/products/reviews/' . $product_review_id ); + $request->set_body_params( + array( + 'review' => 'Hello world - updated.', + 'reviewer' => 'Justin', + 'reviewer_email' => 'woo2@woo.local', + 'rating' => 3, + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $this->assertEquals( 'Hello world - updated.', $data['review'] ); + $this->assertEquals( 'Justin', $data['reviewer'] ); + $this->assertEquals( 'woo2@woo.local', $data['reviewer_email'] ); + $this->assertEquals( 3, $data['rating'] ); + } + + /** + * Tests updating a product review without the correct permissions. + * + * @since 3.0.0 + */ + public function test_update_product_review_without_permission() { + wp_set_current_user( 0 ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $product_review_id = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_product_review( $product->get_id() ); + + $request = new WP_REST_Request( 'PUT', '/wc/v3/products/reviews/' . $product_review_id ); + $request->set_body_params( + array( + 'review' => 'Hello world.', + 'reviewer' => 'Admin', + 'reviewer_email' => 'woo@woo.dev', + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Tests that updating a product review with an invalid id fails. + * + * @since 3.0.0 + */ + public function test_update_product_review_invalid_id() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + + $request = new WP_REST_Request( 'PUT', '/wc/v3/products/reviews/0' ); + $request->set_body_params( + array( + 'review' => 'Hello world.', + 'reviewer' => 'Admin', + 'reviewer_email' => 'woo@woo.dev', + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * Test deleting a product review. + * + * @since 3.0.0 + */ + public function test_delete_product_review() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $product_review_id = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_product_review( $product->get_id() ); + + $request = new WP_REST_Request( 'DELETE', '/wc/v3/products/reviews/' . $product_review_id ); + $request->set_param( 'force', true ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 200, $response->get_status() ); + } + + /** + * Test deleting a product review without permission/creds. + * + * @since 3.0.0 + */ + public function test_delete_product_without_permission() { + wp_set_current_user( 0 ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $product_review_id = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_product_review( $product->get_id() ); + + $request = new WP_REST_Request( 'DELETE', '/wc/v3/products/reviews/' . $product_review_id ); + $response = $this->server->dispatch( $request ); + + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test deleting a product review with an invalid id. + * + * @since 3.0.0 + */ + public function test_delete_product_review_invalid_id() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $product_review_id = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_product_review( $product->get_id() ); + + $request = new WP_REST_Request( 'DELETE', '/wc/v3/products/reviews/0' ); + $request->set_param( 'force', true ); + $response = $this->server->dispatch( $request ); + + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * Test batch managing product reviews. + */ + public function test_product_reviews_batch() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + + $review_1_id = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_product_review( $product->get_id() ); + $review_2_id = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_product_review( $product->get_id() ); + $review_3_id = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_product_review( $product->get_id() ); + $review_4_id = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_product_review( $product->get_id() ); + + $request = new WP_REST_Request( 'POST', '/wc/v3/products/reviews/batch' ); + $request->set_body_params( + array( + 'update' => array( + array( + 'id' => $review_1_id, + 'review' => 'Updated review.', + ), + ), + 'delete' => array( + $review_2_id, + $review_3_id, + ), + 'create' => array( + array( + 'review' => 'New review.', + 'reviewer' => 'Justin', + 'reviewer_email' => 'woo3@woo.local', + 'product_id' => $product->get_id(), + ), + ), + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 'Updated review.', $data['update'][0]['review'] ); + $this->assertEquals( 'New review.', $data['create'][0]['review'] ); + $this->assertEquals( $review_2_id, $data['delete'][0]['previous']['id'] ); + $this->assertEquals( $review_3_id, $data['delete'][1]['previous']['id'] ); + + $request = new WP_REST_Request( 'GET', '/wc/v3/products/reviews' ); + $request->set_param( 'product', $product->get_id() ); + + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 3, count( $data ) ); + } + + /** + * Test the product review schema. + * + * @since 3.0.0 + */ + public function test_product_review_schema() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $request = new WP_REST_Request( 'OPTIONS', '/wc/v3/products/reviews' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $properties = $data['schema']['properties']; + + $this->assertEquals( 11, count( $properties ) ); + $this->assertArrayHasKey( 'id', $properties ); + $this->assertArrayHasKey( 'date_created', $properties ); + $this->assertArrayHasKey( 'date_created_gmt', $properties ); + $this->assertArrayHasKey( 'product_id', $properties ); + $this->assertArrayHasKey( 'status', $properties ); + $this->assertArrayHasKey( 'reviewer', $properties ); + $this->assertArrayHasKey( 'reviewer_email', $properties ); + $this->assertArrayHasKey( 'review', $properties ); + $this->assertArrayHasKey( 'rating', $properties ); + $this->assertArrayHasKey( 'verified', $properties ); + + if ( get_option( 'show_avatars' ) ) { + $this->assertArrayHasKey( 'reviewer_avatar_urls', $properties ); + } + } +} diff --git a/tests/legacy/unit-tests/rest-api/Tests/Version2/product-variations.php b/tests/legacy/unit-tests/rest-api/Tests/Version2/product-variations.php new file mode 100644 index 00000000000..22a53d4d829 --- /dev/null +++ b/tests/legacy/unit-tests/rest-api/Tests/Version2/product-variations.php @@ -0,0 +1,495 @@ +endpoint = new WC_REST_Product_Variations_Controller(); + $this->user = $this->factory->user->create( + array( + 'role' => 'administrator', + ) + ); + } + + /** + * Test route registration. + * + * @since 3.0.0 + */ + public function test_register_routes() { + $routes = $this->server->get_routes(); + $this->assertArrayHasKey( '/wc/v2/products/(?P[\d]+)/variations', $routes ); + $this->assertArrayHasKey( '/wc/v2/products/(?P[\d]+)/variations/(?P[\d]+)', $routes ); + $this->assertArrayHasKey( '/wc/v2/products/(?P[\d]+)/variations/batch', $routes ); + } + + /** + * Test getting variations. + * + * @since 3.0.0 + */ + public function test_get_variations() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_variation_product(); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/products/' . $product->get_id() . '/variations' ) ); + $variations = $response->get_data(); + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 2, count( $variations ) ); + $this->assertEquals( 'DUMMY SKU VARIABLE LARGE', $variations[0]['sku'] ); + $this->assertEquals( 'size', $variations[0]['attributes'][0]['name'] ); + } + + /** + * Test getting variations with an orderby clause. + * + * @since 3.9.0 + */ + public function test_get_variations_with_orderby() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_variation_product(); + $request = new WP_REST_Request( 'GET', '/wc/v2/products/' . $product->get_id() . '/variations' ); + $request->set_query_params( array( 'orderby' => 'menu_order' ) ); + $response = $this->server->dispatch( $request ); + $variations = $response->get_data(); + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 2, count( $variations ) ); + $this->assertEquals( 'DUMMY SKU VARIABLE SMALL', $variations[0]['sku'] ); + $this->assertEquals( 'size', $variations[0]['attributes'][0]['name'] ); + } + + /** + * Test getting variations without permission. + * + * @since 3.0.0 + */ + public function test_get_variations_without_permission() { + wp_set_current_user( 0 ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_variation_product(); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/products/' . $product->get_id() . '/variations' ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test getting a single variation. + * + * @since 3.0.0 + */ + public function test_get_variation() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_variation_product(); + $children = $product->get_children(); + $variation_id = $children[0]; + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/products/' . $product->get_id() . '/variations/' . $variation_id ) ); + $variation = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( $variation_id, $variation['id'] ); + $this->assertEquals( 'size', $variation['attributes'][0]['name'] ); + } + + /** + * Test getting single variation without permission. + * + * @since 3.0.0 + */ + public function test_get_variation_without_permission() { + wp_set_current_user( 0 ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_variation_product(); + $children = $product->get_children(); + $variation_id = $children[0]; + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/products/' . $product->get_id() . '/variations/' . $variation_id ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test deleting a single variation. + * + * @since 3.0.0 + */ + public function test_delete_variation() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_variation_product(); + $children = $product->get_children(); + $variation_id = $children[0]; + + $request = new WP_REST_Request( 'DELETE', '/wc/v2/products/' . $product->get_id() . '/variations/' . $variation_id ); + $request->set_param( 'force', true ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 200, $response->get_status() ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/products/' . $product->get_id() . '/variations' ) ); + $variations = $response->get_data(); + $this->assertEquals( 1, count( $variations ) ); + } + + /** + * Test deleting a single variation without permission. + * + * @since 3.0.0 + */ + public function test_delete_variation_without_permission() { + wp_set_current_user( 0 ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_variation_product(); + $children = $product->get_children(); + $variation_id = $children[0]; + + $request = new WP_REST_Request( 'DELETE', '/wc/v2/products/' . $product->get_id() . '/variations/' . $variation_id ); + $request->set_param( 'force', true ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test deleting a single variation with an invalid ID. + * + * @since 3.0.0 + */ + public function test_delete_variation_with_invalid_id() { + wp_set_current_user( 0 ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_variation_product(); + $request = new WP_REST_Request( 'DELETE', '/wc/v2/products/' . $product->get_id() . '/variations/0' ); + $request->set_param( 'force', true ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * Test editing a single variation. + * + * @since 3.0.0 + */ + public function test_update_variation() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_variation_product(); + $children = $product->get_children(); + $variation_id = $children[0]; + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/products/' . $product->get_id() . '/variations/' . $variation_id ) ); + $variation = $response->get_data(); + + $this->assertEquals( 'DUMMY SKU VARIABLE SMALL', $variation['sku'] ); + $this->assertEquals( 10, $variation['regular_price'] ); + $this->assertEmpty( $variation['sale_price'] ); + $this->assertEquals( 'small', $variation['attributes'][0]['option'] ); + + $request = new WP_REST_Request( 'PUT', '/wc/v2/products/' . $product->get_id() . '/variations/' . $variation_id ); + $request->set_body_params( + array( + 'sku' => 'FIXED-SKU', + 'sale_price' => '8', + 'description' => 'O_O', + 'image' => array( + 'position' => 0, + 'src' => 'http://cldup.com/Dr1Bczxq4q.png', + 'alt' => 'test upload image', + ), + 'attributes' => array( + array( + 'name' => 'pa_size', + 'option' => 'medium', + ), + ), + ) + ); + $response = $this->server->dispatch( $request ); + $variation = $response->get_data(); + + $this->assertTrue( isset( $variation['description'] ), print_r( $variation, true ) ); + $this->assertContains( 'O_O', $variation['description'], print_r( $variation, true ) ); + $this->assertEquals( '8', $variation['price'], print_r( $variation, true ) ); + $this->assertEquals( '8', $variation['sale_price'], print_r( $variation, true ) ); + $this->assertEquals( '10', $variation['regular_price'], print_r( $variation, true ) ); + $this->assertEquals( 'FIXED-SKU', $variation['sku'], print_r( $variation, true ) ); + $this->assertEquals( 'medium', $variation['attributes'][0]['option'], print_r( $variation, true ) ); + $this->assertContains( 'Dr1Bczxq4q', $variation['image']['src'], print_r( $variation, true ) ); + $this->assertContains( 'test upload image', $variation['image']['alt'], print_r( $variation, true ) ); + + wp_delete_attachment( $variation['image']['id'], true ); + } + + /** + * Test updating a single variation without permission. + * + * @since 3.0.0 + */ + public function test_update_variation_without_permission() { + wp_set_current_user( 0 ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_variation_product(); + $children = $product->get_children(); + $variation_id = $children[0]; + + $request = new WP_REST_Request( 'PUT', '/wc/v2/products/' . $product->get_id() . '/variations/' . $variation_id ); + $request->set_body_params( + array( + 'sku' => 'FIXED-SKU-NO-PERMISSION', + ) + ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test updating a single variation with an invalid ID. + * + * @since 3.0.0 + */ + public function test_update_variation_with_invalid_id() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_variation_product(); + $request = new WP_REST_Request( 'PUT', '/wc/v2/products/' . $product->get_id() . '/variations/0' ); + $request->set_body_params( + array( + 'sku' => 'FIXED-SKU-NO-PERMISSION', + ) + ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 400, $response->get_status() ); + } + + /** + * Test creating a single variation. + * + * @since 3.0.0 + */ + public function test_create_variation() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_variation_product(); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/products/' . $product->get_id() . '/variations' ) ); + $variations = $response->get_data(); + $this->assertEquals( 2, count( $variations ) ); + + $request = new WP_REST_Request( 'POST', '/wc/v2/products/' . $product->get_id() . '/variations' ); + $request->set_body_params( + array( + 'sku' => 'DUMMY SKU VARIABLE MEDIUM', + 'regular_price' => '12', + 'description' => 'A medium size.', + 'attributes' => array( + array( + 'name' => 'pa_size', + 'option' => 'medium', + ), + ), + ) + ); + $response = $this->server->dispatch( $request ); + $variation = $response->get_data(); + + $this->assertContains( 'A medium size.', $variation['description'] ); + $this->assertEquals( '12', $variation['price'] ); + $this->assertEquals( '12', $variation['regular_price'] ); + $this->assertTrue( $variation['purchasable'] ); + $this->assertEquals( 'DUMMY SKU VARIABLE MEDIUM', $variation['sku'] ); + $this->assertEquals( 'medium', $variation['attributes'][0]['option'] ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/products/' . $product->get_id() . '/variations' ) ); + $variations = $response->get_data(); + $this->assertEquals( 3, count( $variations ) ); + } + + /** + * Test creating a single variation without permission. + * + * @since 3.0.0 + */ + public function test_create_variation_without_permission() { + wp_set_current_user( 0 ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_variation_product(); + + $request = new WP_REST_Request( 'POST', '/wc/v2/products/' . $product->get_id() . '/variations' ); + $request->set_body_params( + array( + 'sku' => 'DUMMY SKU VARIABLE MEDIUM', + 'regular_price' => '12', + 'description' => 'A medium size.', + 'attributes' => array( + array( + 'name' => 'pa_size', + 'option' => 'medium', + ), + ), + ) + ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test batch managing product variations. + */ + public function test_product_variations_batch() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_variation_product(); + $children = $product->get_children(); + $request = new WP_REST_Request( 'POST', '/wc/v2/products/' . $product->get_id() . '/variations/batch' ); + $request->set_body_params( + array( + 'update' => array( + array( + 'id' => $children[0], + 'description' => 'Updated description.', + 'image' => array( + 'position' => 0, + 'src' => 'http://cldup.com/Dr1Bczxq4q.png', + 'alt' => 'test upload image', + ), + ), + ), + 'delete' => array( + $children[1], + ), + 'create' => array( + array( + 'sku' => 'DUMMY SKU VARIABLE MEDIUM', + 'regular_price' => '12', + 'description' => 'A medium size.', + 'attributes' => array( + array( + 'name' => 'pa_size', + 'option' => 'medium', + ), + ), + ), + ), + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertContains( 'Updated description.', $data['update'][0]['description'] ); + $this->assertEquals( 'DUMMY SKU VARIABLE MEDIUM', $data['create'][0]['sku'] ); + $this->assertEquals( 'medium', $data['create'][0]['attributes'][0]['option'] ); + $this->assertEquals( $children[1], $data['delete'][0]['id'] ); + + $request = new WP_REST_Request( 'GET', '/wc/v2/products/' . $product->get_id() . '/variations' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 2, count( $data ) ); + + wp_delete_attachment( $data[1]['image']['id'], true ); + } + + /** + * Test variation schema. + * + * @since 3.0.0 + */ + public function test_variation_schema() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $request = new WP_REST_Request( 'OPTIONS', '/wc/v2/products/' . $product->get_id() . '/variations' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $properties = $data['schema']['properties']; + + $this->assertEquals( 37, count( $properties ) ); + $this->assertArrayHasKey( 'id', $properties ); + $this->assertArrayHasKey( 'date_created', $properties ); + $this->assertArrayHasKey( 'date_modified', $properties ); + $this->assertArrayHasKey( 'description', $properties ); + $this->assertArrayHasKey( 'permalink', $properties ); + $this->assertArrayHasKey( 'sku', $properties ); + $this->assertArrayHasKey( 'price', $properties ); + $this->assertArrayHasKey( 'regular_price', $properties ); + $this->assertArrayHasKey( 'sale_price', $properties ); + $this->assertArrayHasKey( 'date_on_sale_from', $properties ); + $this->assertArrayHasKey( 'date_on_sale_to', $properties ); + $this->assertArrayHasKey( 'on_sale', $properties ); + $this->assertArrayHasKey( 'visible', $properties ); + $this->assertArrayHasKey( 'purchasable', $properties ); + $this->assertArrayHasKey( 'virtual', $properties ); + $this->assertArrayHasKey( 'downloadable', $properties ); + $this->assertArrayHasKey( 'downloads', $properties ); + $this->assertArrayHasKey( 'download_limit', $properties ); + $this->assertArrayHasKey( 'download_expiry', $properties ); + $this->assertArrayHasKey( 'tax_status', $properties ); + $this->assertArrayHasKey( 'tax_class', $properties ); + $this->assertArrayHasKey( 'manage_stock', $properties ); + $this->assertArrayHasKey( 'stock_quantity', $properties ); + $this->assertArrayHasKey( 'in_stock', $properties ); + $this->assertArrayHasKey( 'backorders', $properties ); + $this->assertArrayHasKey( 'backorders_allowed', $properties ); + $this->assertArrayHasKey( 'backordered', $properties ); + $this->assertArrayHasKey( 'weight', $properties ); + $this->assertArrayHasKey( 'dimensions', $properties ); + $this->assertArrayHasKey( 'shipping_class', $properties ); + $this->assertArrayHasKey( 'shipping_class_id', $properties ); + $this->assertArrayHasKey( 'image', $properties ); + $this->assertArrayHasKey( 'attributes', $properties ); + $this->assertArrayHasKey( 'menu_order', $properties ); + $this->assertArrayHasKey( 'meta_data', $properties ); + } + + /** + * Test updating a variation stock. + * + * @since 3.0.0 + */ + public function test_update_variation_manage_stock() { + wp_set_current_user( $this->user ); + + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_variation_product(); + $product->set_manage_stock( false ); + $product->save(); + + $children = $product->get_children(); + $variation_id = $children[0]; + + // Set stock to true. + $request = new WP_REST_Request( 'PUT', '/wc/v2/products/' . $product->get_id() . '/variations/' . $variation_id ); + $request->set_body_params( + array( + 'manage_stock' => true, + ) + ); + + $response = $this->server->dispatch( $request ); + $variation = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( true, $variation['manage_stock'] ); + + // Set stock to false. + $request = new WP_REST_Request( 'PUT', '/wc/v2/products/' . $product->get_id() . '/variations/' . $variation_id ); + $request->set_body_params( + array( + 'manage_stock' => false, + ) + ); + + $response = $this->server->dispatch( $request ); + $variation = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( false, $variation['manage_stock'] ); + + // Set stock to false but parent is managing stock. + $product->set_manage_stock( true ); + $product->save(); + $request = new WP_REST_Request( 'PUT', '/wc/v2/products/' . $product->get_id() . '/variations/' . $variation_id ); + $request->set_body_params( + array( + 'manage_stock' => false, + ) + ); + + $response = $this->server->dispatch( $request ); + $variation = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 'parent', $variation['manage_stock'] ); + } +} diff --git a/tests/legacy/unit-tests/rest-api/Tests/Version2/products.php b/tests/legacy/unit-tests/rest-api/Tests/Version2/products.php new file mode 100644 index 00000000000..ea67735ae4e --- /dev/null +++ b/tests/legacy/unit-tests/rest-api/Tests/Version2/products.php @@ -0,0 +1,536 @@ +endpoint = new WC_REST_Products_Controller(); + $this->user = $this->factory->user->create( + array( + 'role' => 'administrator', + ) + ); + } + + /** + * Test route registration. + * + * @since 3.0.0 + */ + public function test_register_routes() { + $routes = $this->server->get_routes(); + $this->assertArrayHasKey( '/wc/v2/products', $routes ); + $this->assertArrayHasKey( '/wc/v2/products/(?P[\d]+)', $routes ); + $this->assertArrayHasKey( '/wc/v2/products/batch', $routes ); + } + + /** + * Test getting products. + * + * @since 3.0.0 + */ + public function test_get_products() { + wp_set_current_user( $this->user ); + \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_external_product(); + sleep( 1 ); // So both products have different timestamps. + \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/products' ) ); + $products = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + + $this->assertEquals( 2, count( $products ) ); + $this->assertEquals( 'Dummy Product', $products[0]['name'] ); + $this->assertEquals( 'DUMMY SKU', $products[0]['sku'] ); + $this->assertEquals( 'Dummy External Product', $products[1]['name'] ); + $this->assertEquals( 'DUMMY EXTERNAL SKU', $products[1]['sku'] ); + } + + /** + * Test getting products without permission. + * + * @since 3.0.0 + */ + public function test_get_products_without_permission() { + wp_set_current_user( 0 ); + \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/products' ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test getting a single product. + * + * @since 3.0.0 + */ + public function test_get_product() { + wp_set_current_user( $this->user ); + $simple = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_external_product(); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/products/' . $simple->get_id() ) ); + $product = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertContains( + array( + 'id' => $simple->get_id(), + 'name' => 'Dummy External Product', + 'type' => 'simple', + 'status' => 'publish', + 'sku' => 'DUMMY EXTERNAL SKU', + 'regular_price' => 10, + ), + $product + ); + } + + /** + * Test getting single product without permission. + * + * @since 3.0.0 + */ + public function test_get_product_without_permission() { + wp_set_current_user( 0 ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/products/' . $product->get_id() ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test deleting a single product. + * + * @since 3.0.0 + */ + public function test_delete_product() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + + $request = new WP_REST_Request( 'DELETE', '/wc/v2/products/' . $product->get_id() ); + $request->set_param( 'force', true ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 200, $response->get_status() ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/products' ) ); + $variations = $response->get_data(); + $this->assertEquals( 0, count( $variations ) ); + } + + /** + * Test deleting a single product without permission. + * + * @since 3.0.0 + */ + public function test_delete_product_without_permission() { + wp_set_current_user( 0 ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $request = new WP_REST_Request( 'DELETE', '/wc/v2/products/' . $product->get_id() ); + $request->set_param( 'force', true ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test deleting a single product with an invalid ID. + * + * @since 3.0.0 + */ + public function test_delete_product_with_invalid_id() { + wp_set_current_user( 0 ); + $request = new WP_REST_Request( 'DELETE', '/wc/v2/products/0' ); + $request->set_param( 'force', true ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * Test editing a single product. Tests multiple product types. + * + * @since 3.0.0 + */ + public function test_update_product() { + wp_set_current_user( $this->user ); + + // test simple products. + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/products/' . $product->get_id() ) ); + $data = $response->get_data(); + + $this->assertEquals( 'DUMMY SKU', $data['sku'] ); + $this->assertEquals( 10, $data['regular_price'] ); + $this->assertEmpty( $data['sale_price'] ); + + $request = new WP_REST_Request( 'PUT', '/wc/v2/products/' . $product->get_id() ); + $request->set_body_params( + array( + 'sku' => 'FIXED-SKU', + 'sale_price' => '8', + 'description' => 'Testing', + 'images' => array( + array( + 'position' => 0, + 'src' => 'http://cldup.com/Dr1Bczxq4q.png', + 'alt' => 'test upload image', + ), + ), + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertContains( 'Testing', $data['description'] ); + $this->assertEquals( '8', $data['price'] ); + $this->assertEquals( '8', $data['sale_price'] ); + $this->assertEquals( '10', $data['regular_price'] ); + $this->assertEquals( 'FIXED-SKU', $data['sku'] ); + $this->assertContains( 'Dr1Bczxq4q', $data['images'][0]['src'] ); + $this->assertContains( 'test upload image', $data['images'][0]['alt'] ); + $product->delete( true ); + wp_delete_attachment( $data['images'][0]['id'], true ); + + // test variable product (variations are tested in product-variations.php). + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_variation_product(); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/products/' . $product->get_id() ) ); + $data = $response->get_data(); + + foreach ( array( 'small', 'large' ) as $term_name ) { + $this->assertContains( $term_name, $data['attributes'][0]['options'] ); + } + + $request = new WP_REST_Request( 'PUT', '/wc/v2/products/' . $product->get_id() ); + $request->set_body_params( + array( + 'attributes' => array( + array( + 'id' => 0, + 'name' => 'pa_color', + 'options' => array( + 'red', + 'yellow', + ), + 'visible' => false, + 'variation' => 1, + ), + array( + 'id' => 0, + 'name' => 'pa_size', + 'options' => array( + 'small', + ), + 'visible' => false, + 'variation' => 1, + ), + ), + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( array( 'small' ), $data['attributes'][0]['options'] ); + $this->assertEquals( array( 'red', 'yellow' ), $data['attributes'][1]['options'] ); + $product->delete( true ); + + // test external product. + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_external_product(); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/products/' . $product->get_id() ) ); + $data = $response->get_data(); + + $this->assertEquals( 'Buy external product', $data['button_text'] ); + $this->assertEquals( 'http://woocommerce.com', $data['external_url'] ); + + $request = new WP_REST_Request( 'PUT', '/wc/v2/products/' . $product->get_id() ); + $request->set_body_params( + array( + 'button_text' => 'Test API Update', + 'external_url' => 'http://automattic.com', + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 'Test API Update', $data['button_text'] ); + $this->assertEquals( 'http://automattic.com', $data['external_url'] ); + } + + /** + * Test updating a single product without permission. + * + * @since 3.0.0 + */ + public function test_update_product_without_permission() { + wp_set_current_user( 0 ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $request = new WP_REST_Request( 'PUT', '/wc/v2/products/' . $product->get_id() ); + $request->set_body_params( + array( + 'sku' => 'FIXED-SKU-NO-PERMISSION', + ) + ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test updating a single product with an invalid ID. + * + * @since 3.0.0 + */ + public function test_update_product_with_invalid_id() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $request = new WP_REST_Request( 'PUT', '/wc/v2/products/0' ); + $request->set_body_params( + array( + 'sku' => 'FIXED-SKU-INVALID-ID', + ) + ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 400, $response->get_status() ); + } + + /** + * Test creating a single product. + * + * @since 3.0.0 + */ + public function test_create_product() { + wp_set_current_user( $this->user ); + + $request = new WP_REST_Request( 'POST', '/wc/v2/products/shipping_classes' ); + $request->set_body_params( + array( + 'name' => 'Test', + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $shipping_class_id = $data['id']; + + // Create simple. + $request = new WP_REST_Request( 'POST', '/wc/v2/products' ); + $request->set_body_params( + array( + 'type' => 'simple', + 'name' => 'Test Simple Product', + 'sku' => 'DUMMY SKU SIMPLE API', + 'regular_price' => '10', + 'shipping_class' => 'test', + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( '10', $data['price'] ); + $this->assertEquals( '10', $data['regular_price'] ); + $this->assertTrue( $data['purchasable'] ); + $this->assertEquals( 'DUMMY SKU SIMPLE API', $data['sku'] ); + $this->assertEquals( 'Test Simple Product', $data['name'] ); + $this->assertEquals( 'simple', $data['type'] ); + $this->assertEquals( $shipping_class_id, $data['shipping_class_id'] ); + + // Create external. + $request = new WP_REST_Request( 'POST', '/wc/v2/products' ); + $request->set_body_params( + array( + 'type' => 'external', + 'name' => 'Test External Product', + 'sku' => 'DUMMY SKU EXTERNAL API', + 'regular_price' => '10', + 'button_text' => 'Test Button', + 'external_url' => 'https://wordpress.org', + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( '10', $data['price'] ); + $this->assertEquals( '10', $data['regular_price'] ); + $this->assertFalse( $data['purchasable'] ); + $this->assertEquals( 'DUMMY SKU EXTERNAL API', $data['sku'] ); + $this->assertEquals( 'Test External Product', $data['name'] ); + $this->assertEquals( 'external', $data['type'] ); + $this->assertEquals( 'Test Button', $data['button_text'] ); + $this->assertEquals( 'https://wordpress.org', $data['external_url'] ); + + // Create variable. + $request = new WP_REST_Request( 'POST', '/wc/v2/products' ); + $request->set_body_params( + array( + 'type' => 'variable', + 'name' => 'Test Variable Product', + 'sku' => 'DUMMY SKU VARIABLE API', + 'attributes' => array( + array( + 'id' => 0, + 'name' => 'pa_size', + 'options' => array( + 'small', + 'medium', + ), + 'visible' => false, + 'variation' => 1, + ), + ), + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 'DUMMY SKU VARIABLE API', $data['sku'] ); + $this->assertEquals( 'Test Variable Product', $data['name'] ); + $this->assertEquals( 'variable', $data['type'] ); + $this->assertEquals( array( 'small', 'medium' ), $data['attributes'][0]['options'] ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/products' ) ); + $products = $response->get_data(); + $this->assertEquals( 3, count( $products ) ); + } + + /** + * Test creating a single product without permission. + * + * @since 3.0.0 + */ + public function test_create_product_without_permission() { + wp_set_current_user( 0 ); + + $request = new WP_REST_Request( 'POST', '/wc/v2/products' ); + $request->set_body_params( + array( + 'name' => 'Test Product', + 'regular_price' => '12', + ) + ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test batch managing products. + */ + public function test_products_batch() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $product_2 = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $request = new WP_REST_Request( 'POST', '/wc/v2/products/batch' ); + $request->set_body_params( + array( + 'update' => array( + array( + 'id' => $product->get_id(), + 'description' => 'Updated description.', + ), + ), + 'delete' => array( + $product_2->get_id(), + ), + 'create' => array( + array( + 'sku' => 'DUMMY SKU BATCH TEST 1', + 'regular_price' => '10', + 'name' => 'Test Batch Create 1', + 'type' => 'external', + 'button_text' => 'Test Button', + ), + array( + 'sku' => 'DUMMY SKU BATCH TEST 2', + 'regular_price' => '20', + 'name' => 'Test Batch Create 2', + 'type' => 'simple', + ), + ), + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertContains( 'Updated description.', $data['update'][0]['description'] ); + $this->assertEquals( 'DUMMY SKU BATCH TEST 1', $data['create'][0]['sku'] ); + $this->assertEquals( 'DUMMY SKU BATCH TEST 2', $data['create'][1]['sku'] ); + $this->assertEquals( 'Test Button', $data['create'][0]['button_text'] ); + $this->assertEquals( 'external', $data['create'][0]['type'] ); + $this->assertEquals( 'simple', $data['create'][1]['type'] ); + $this->assertEquals( $product_2->get_id(), $data['delete'][0]['id'] ); + + $request = new WP_REST_Request( 'GET', '/wc/v2/products' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 3, count( $data ) ); + } + + /** + * Tests to make sure you can filter products post statuses by both + * the status query arg and WP_Query. + * + * @since 3.0.0 + */ + public function test_products_filter_post_status() { + wp_set_current_user( $this->user ); + for ( $i = 0; $i < 8; $i++ ) { + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + if ( 0 === $i % 2 ) { + wp_update_post( + array( + 'ID' => $product->get_id(), + 'post_status' => 'draft', + ) + ); + } + } + + // Test filtering with status=publish. + $request = new WP_REST_Request( 'GET', '/wc/v2/products' ); + $request->set_param( 'status', 'publish' ); + $response = $this->server->dispatch( $request ); + $products = $response->get_data(); + + $this->assertEquals( 4, count( $products ) ); + foreach ( $products as $product ) { + $this->assertEquals( 'publish', $product['status'] ); + } + + // Test filtering with status=draft. + $request = new WP_REST_Request( 'GET', '/wc/v2/products' ); + $request->set_param( 'status', 'draft' ); + $response = $this->server->dispatch( $request ); + $products = $response->get_data(); + + $this->assertEquals( 4, count( $products ) ); + foreach ( $products as $product ) { + $this->assertEquals( 'draft', $product['status'] ); + } + + // Test filtering with no filters - which should return 'any' (all 8). + $request = new WP_REST_Request( 'GET', '/wc/v2/products' ); + $response = $this->server->dispatch( $request ); + $products = $response->get_data(); + + $this->assertEquals( 8, count( $products ) ); + } + + /** + * Test product schema. + * + * @since 3.0.0 + */ + public function test_product_schema() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $request = new WP_REST_Request( 'OPTIONS', '/wc/v2/products/' . $product->get_id() ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $properties = $data['schema']['properties']; + $this->assertEquals( 65, count( $properties ) ); + } +} diff --git a/tests/legacy/unit-tests/rest-api/Tests/Version2/settings.php b/tests/legacy/unit-tests/rest-api/Tests/Version2/settings.php new file mode 100644 index 00000000000..6117fdc87e0 --- /dev/null +++ b/tests/legacy/unit-tests/rest-api/Tests/Version2/settings.php @@ -0,0 +1,903 @@ +endpoint = new WC_REST_Setting_Options_Controller(); + \Automattic\WooCommerce\RestApi\UnitTests\Helpers\SettingsHelper::register(); + $this->user = $this->factory->user->create( + array( + 'role' => 'administrator', + ) + ); + } + + /** + * Test route registration. + * + * @since 3.0.0 + */ + public function test_register_routes() { + $routes = $this->server->get_routes(); + $this->assertArrayHasKey( '/wc/v2/settings', $routes ); + $this->assertArrayHasKey( '/wc/v2/settings/(?P[\w-]+)', $routes ); + $this->assertArrayHasKey( '/wc/v2/settings/(?P[\w-]+)/(?P[\w-]+)', $routes ); + } + + /** + * Test getting all groups. + * + * @since 3.0.0 + */ + public function test_get_groups() { + wp_set_current_user( $this->user ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/settings' ) ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + + $this->assertContains( + array( + 'id' => 'test', + 'label' => 'Test extension', + 'parent_id' => '', + 'description' => 'My awesome test settings.', + 'sub_groups' => array( 'sub-test' ), + '_links' => array( + 'options' => array( + array( + 'href' => rest_url( '/wc/v2/settings/test' ), + ), + ), + ), + ), + $data + ); + + $this->assertContains( + array( + 'id' => 'sub-test', + 'label' => 'Sub test', + 'parent_id' => 'test', + 'description' => '', + 'sub_groups' => array(), + '_links' => array( + 'options' => array( + array( + 'href' => rest_url( '/wc/v2/settings/sub-test' ), + ), + ), + ), + ), + $data + ); + } + + /** + * Test /settings without valid permissions/creds. + * + * @since 3.0.0 + */ + public function test_get_groups_without_permission() { + wp_set_current_user( 0 ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/settings' ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test /settings without valid permissions/creds. + * + * @since 3.0.0 + * @covers WC_Rest_Settings_Controller::get_items + */ + public function test_get_groups_none_registered() { + wp_set_current_user( $this->user ); + + remove_all_filters( 'woocommerce_settings_groups' ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/settings' ) ); + $this->assertEquals( 500, $response->get_status() ); + + \Automattic\WooCommerce\RestApi\UnitTests\Helpers\SettingsHelper::register(); + } + + /** + * Test groups schema. + * + * @since 3.0.0 + */ + public function test_get_group_schema() { + $request = new WP_REST_Request( 'OPTIONS', '/wc/v2/settings' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $properties = $data['schema']['properties']; + $this->assertEquals( 5, count( $properties ) ); + $this->assertArrayHasKey( 'id', $properties ); + $this->assertArrayHasKey( 'parent_id', $properties ); + $this->assertArrayHasKey( 'label', $properties ); + $this->assertArrayHasKey( 'description', $properties ); + $this->assertArrayHasKey( 'sub_groups', $properties ); + } + + /** + * Test settings schema. + * + * @since 3.0.0 + */ + public function test_get_setting_schema() { + $request = new WP_REST_Request( 'OPTIONS', '/wc/v2/settings/test/woocommerce_shop_page_display' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $properties = $data['schema']['properties']; + $this->assertEquals( 9, count( $properties ) ); + $this->assertArrayHasKey( 'id', $properties ); + $this->assertArrayHasKey( 'label', $properties ); + $this->assertArrayHasKey( 'description', $properties ); + $this->assertArrayHasKey( 'value', $properties ); + $this->assertArrayHasKey( 'default', $properties ); + $this->assertArrayHasKey( 'tip', $properties ); + $this->assertArrayHasKey( 'placeholder', $properties ); + $this->assertArrayHasKey( 'type', $properties ); + $this->assertArrayHasKey( 'options', $properties ); + } + + /** + * Test getting a single group. + * + * @since 3.0.0 + */ + public function test_get_group() { + wp_set_current_user( $this->user ); + + // test route callback receiving an empty group id. + $result = $this->endpoint->get_group_settings( '' ); + $this->assertWPError( $result ); + + // test getting a group that does not exist. + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/settings/not-real' ) ); + $this->assertEquals( 404, $response->get_status() ); + + // test getting the 'invalid' group. + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/settings/invalid' ) ); + $this->assertEquals( 404, $response->get_status() ); + + // test getting a valid group with settings attached to it. + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/settings/test' ) ); + $data = $response->get_data(); + $this->assertEquals( 1, count( $data ) ); + $this->assertEquals( 'woocommerce_shop_page_display', $data[0]['id'] ); + $this->assertEmpty( $data[0]['value'] ); + } + + /** + * Test getting a single group without permission. + * + * @since 3.0.0 + */ + public function test_get_group_without_permission() { + wp_set_current_user( 0 ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/settings/coupon-data' ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test updating a single setting. + * + * @since 3.0.0 + */ + public function test_update_setting() { + wp_set_current_user( $this->user ); + + // test defaults first. + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/settings/test/woocommerce_shop_page_display' ) ); + $data = $response->get_data(); + $this->assertEquals( '', $data['value'] ); + + // test updating shop display setting. + $request = new WP_REST_Request( 'PUT', sprintf( '/wc/v2/settings/%s/%s', 'test', 'woocommerce_shop_page_display' ) ); + $request->set_body_params( + array( + 'value' => 'both', + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 'both', $data['value'] ); + $this->assertEquals( 'both', get_option( 'woocommerce_shop_page_display' ) ); + + $request = new WP_REST_Request( 'PUT', sprintf( '/wc/v2/settings/%s/%s', 'test', 'woocommerce_shop_page_display' ) ); + $request->set_body_params( + array( + 'value' => 'subcategories', + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 'subcategories', $data['value'] ); + $this->assertEquals( 'subcategories', get_option( 'woocommerce_shop_page_display' ) ); + + $request = new WP_REST_Request( 'PUT', sprintf( '/wc/v2/settings/%s/%s', 'test', 'woocommerce_shop_page_display' ) ); + $request->set_body_params( + array( + 'value' => '', + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( '', $data['value'] ); + $this->assertEquals( '', get_option( 'woocommerce_shop_page_display' ) ); + } + + /** + * Test updating multiple settings at once. + * + * @since 3.0.0 + */ + public function test_update_settings() { + wp_set_current_user( $this->user ); + + // test defaults first. + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/settings/test' ) ); + $data = $response->get_data(); + $this->assertEquals( '', $data[0]['value'] ); + + // test setting both at once. + $request = new WP_REST_Request( 'POST', '/wc/v2/settings/test/batch' ); + $request->set_body_params( + array( + 'update' => array( + array( + 'id' => 'woocommerce_shop_page_display', + 'value' => 'both', + ), + ), + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 'both', $data['update'][0]['value'] ); + $this->assertEquals( 'both', get_option( 'woocommerce_shop_page_display' ) ); + + // test updating one, but making sure the other value stays the same. + $request = new WP_REST_Request( 'POST', '/wc/v2/settings/test/batch' ); + $request->set_body_params( + array( + 'update' => array( + array( + 'id' => 'woocommerce_shop_page_display', + 'value' => 'subcategories', + ), + ), + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $this->assertEquals( 'subcategories', $data['update'][0]['value'] ); + $this->assertEquals( 'subcategories', get_option( 'woocommerce_shop_page_display' ) ); + } + + /** + * Test getting a single setting. + * + * @since 3.0.0 + */ + public function test_get_setting() { + wp_set_current_user( $this->user ); + + // test getting an invalid setting from a group that does not exist. + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/settings/not-real/woocommerce_shop_page_display' ) ); + $data = $response->get_data(); + $this->assertEquals( 404, $response->get_status() ); + + // test getting an invalid setting from a group that does exist. + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/settings/invalid/invalid' ) ); + $data = $response->get_data(); + $this->assertEquals( 404, $response->get_status() ); + + // test getting a valid setting. + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/settings/test/woocommerce_shop_page_display' ) ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + + $this->assertEquals( 'woocommerce_shop_page_display', $data['id'] ); + $this->assertEquals( 'Shop page display', $data['label'] ); + $this->assertEquals( '', $data['default'] ); + $this->assertEquals( 'select', $data['type'] ); + $this->assertEquals( '', $data['value'] ); + } + + /** + * Test getting a single setting without valid user permissions. + * + * @since 3.0.0 + */ + public function test_get_setting_without_permission() { + wp_set_current_user( 0 ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/settings/test/woocommerce_shop_page_display' ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Tests the GET single setting route handler receiving an empty setting ID. + * + * @since 3.0.0 + */ + public function test_get_setting_empty_setting_id() { + $result = $this->endpoint->get_setting( 'test', '' ); + + $this->assertWPError( $result ); + } + + /** + * Tests the GET single setting route handler receiving an invalid setting ID. + * + * @since 3.0.0 + */ + public function test_get_setting_invalid_setting_id() { + $result = $this->endpoint->get_setting( 'test', 'invalid' ); + + $this->assertWPError( $result ); + } + + /** + * Tests the GET single setting route handler encountering an invalid setting type. + * + * @since 3.0.0 + */ + public function test_get_setting_invalid_setting_type() { + // $controller = $this->getMock( 'WC_Rest_Setting_Options_Controller', array( 'get_group_settings', 'is_setting_type_valid' ) ); + $controller = $this->getMockBuilder( 'WC_Rest_Setting_Options_Controller' )->setMethods( array( 'get_group_settings', 'is_setting_type_valid' ) )->getMock(); + + $controller + ->expects( $this->any() ) + ->method( 'get_group_settings' ) + ->will( $this->returnValue( \Automattic\WooCommerce\RestApi\UnitTests\Helpers\SettingsHelper::register_test_settings( array() ) ) ); + + $controller + ->expects( $this->any() ) + ->method( 'is_setting_type_valid' ) + ->will( $this->returnValue( false ) ); + + $result = $controller->get_setting( 'test', 'woocommerce_shop_page_display' ); + + $this->assertWPError( $result ); + } + + /** + * Test updating a single setting without valid user permissions. + * + * @since 3.0.0 + */ + public function test_update_setting_without_permission() { + wp_set_current_user( 0 ); + + $request = new WP_REST_Request( 'PUT', sprintf( '/wc/v2/settings/%s/%s', 'test', 'woocommerce_shop_page_display' ) ); + $request->set_body_params( + array( + 'value' => 'subcategories', + ) + ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 401, $response->get_status() ); + } + + + /** + * Test updating multiple settings without valid user permissions. + * + * @since 3.0.0 + */ + public function test_update_settings_without_permission() { + wp_set_current_user( 0 ); + + $request = new WP_REST_Request( 'POST', '/wc/v2/settings/test/batch' ); + $request->set_body_params( + array( + 'update' => array( + array( + 'id' => 'woocommerce_shop_page_display', + 'value' => 'subcategories', + ), + ), + ) + ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test updating a bad setting ID. + * + * @since 3.0.0 + * @covers WC_Rest_Setting_Options_Controller::update_item + */ + public function test_update_setting_bad_setting_id() { + wp_set_current_user( $this->user ); + + $request = new WP_REST_Request( 'PUT', '/wc/v2/settings/test/invalid' ); + $request->set_body_params( + array( + 'value' => 'test', + ) + ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * Tests our classic setting registration to make sure settings added for WP-Admin are available over the API. + * + * @since 3.0.0 + */ + public function test_classic_settings() { + wp_set_current_user( $this->user ); + + // Make sure the group is properly registered. + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/settings/products' ) ); + $data = $response->get_data(); + $this->assertTrue( is_array( $data ) ); + $this->assertContains( + array( + 'id' => 'woocommerce_downloads_require_login', + 'label' => 'Access restriction', + 'description' => 'Downloads require login', + 'type' => 'checkbox', + 'default' => 'no', + 'tip' => 'This setting does not apply to guest purchases.', + 'value' => 'no', + '_links' => array( + 'self' => array( + array( + 'href' => rest_url( '/wc/v2/settings/products/woocommerce_downloads_require_login' ), + ), + ), + 'collection' => array( + array( + 'href' => rest_url( '/wc/v2/settings/products' ), + ), + ), + ), + ), + $data + ); + + // test get single. + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/settings/products/woocommerce_dimension_unit' ) ); + $data = $response->get_data(); + + $this->assertEquals( 'cm', $data['default'] ); + + // test update. + $request = new WP_REST_Request( 'PUT', sprintf( '/wc/v2/settings/%s/%s', 'products', 'woocommerce_dimension_unit' ) ); + $request->set_body_params( + array( + 'value' => 'yd', + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 'yd', $data['value'] ); + $this->assertEquals( 'yd', get_option( 'woocommerce_dimension_unit' ) ); + } + + /** + * Tests our email etting registration to make sure settings added for WP-Admin are available over the API. + * + * @since 3.0.0 + */ + public function test_email_settings() { + wp_set_current_user( $this->user ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/settings/email_new_order' ) ); + $settings = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + + $this->assertContains( + array( + 'id' => 'recipient', + 'label' => 'Recipient(s)', + 'description' => 'Enter recipients (comma separated) for this email. Defaults to admin@example.org.', + 'type' => 'text', + 'default' => '', + 'tip' => 'Enter recipients (comma separated) for this email. Defaults to admin@example.org.', + 'value' => '', + '_links' => array( + 'self' => array( + array( + 'href' => rest_url( '/wc/v2/settings/email_new_order/recipient' ), + ), + ), + 'collection' => array( + array( + 'href' => rest_url( '/wc/v2/settings/email_new_order' ), + ), + ), + ), + ), + $settings + ); + + // test get single. + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/settings/email_new_order/subject' ) ); + $setting = $response->get_data(); + + $this->assertEquals( + array( + 'id' => 'subject', + 'label' => 'Subject', + 'description' => 'Available placeholders: {site_title}, {site_address}, {site_url}, {order_date}, {order_number}', + 'type' => 'text', + 'default' => '', + 'tip' => 'Available placeholders: {site_title}, {site_address}, {site_url}, {order_date}, {order_number}', + 'value' => '', + ), + $setting + ); + + // test update. + $request = new WP_REST_Request( 'PUT', sprintf( '/wc/v2/settings/%s/%s', 'email_new_order', 'subject' ) ); + $request->set_body_params( + array( + 'value' => 'This is my subject', + ) + ); + $response = $this->server->dispatch( $request ); + $setting = $response->get_data(); + + $this->assertEquals( + array( + 'id' => 'subject', + 'label' => 'Subject', + 'description' => 'Available placeholders: {site_title}, {site_address}, {site_url}, {order_date}, {order_number}', + 'type' => 'text', + 'default' => '', + 'tip' => 'Available placeholders: {site_title}, {site_address}, {site_url}, {order_date}, {order_number}', + 'value' => 'This is my subject', + ), + $setting + ); + + // test updating another subject and making sure it works with a "similar" id. + $request = new WP_REST_Request( 'GET', sprintf( '/wc/v2/settings/%s/%s', 'email_customer_new_account', 'subject' ) ); + $response = $this->server->dispatch( $request ); + $setting = $response->get_data(); + + $this->assertEmpty( $setting['value'] ); + + // test update. + $request = new WP_REST_Request( 'PUT', sprintf( '/wc/v2/settings/%s/%s', 'email_customer_new_account', 'subject' ) ); + $request->set_body_params( + array( + 'value' => 'This is my new subject', + ) + ); + $response = $this->server->dispatch( $request ); + $setting = $response->get_data(); + + $this->assertEquals( 'This is my new subject', $setting['value'] ); + + // make sure the other is what we left it. + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/settings/email_new_order/subject' ) ); + $setting = $response->get_data(); + + $this->assertEquals( 'This is my subject', $setting['value'] ); + } + + /** + * Test validation of checkbox settings. + * + * @since 3.0.0 + */ + public function test_validation_checkbox() { + wp_set_current_user( $this->user ); + + // test bogus value. + $request = new WP_REST_Request( 'PUT', sprintf( '/wc/v2/settings/%s/%s', 'email_cancelled_order', 'enabled' ) ); + $request->set_body_params( + array( + 'value' => 'not_yes_or_no', + ) + ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 400, $response->get_status() ); + + // test yes. + $request = new WP_REST_Request( 'PUT', sprintf( '/wc/v2/settings/%s/%s', 'email_cancelled_order', 'enabled' ) ); + $request->set_body_params( + array( + 'value' => 'yes', + ) + ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 200, $response->get_status() ); + + // test no. + $request = new WP_REST_Request( 'PUT', sprintf( '/wc/v2/settings/%s/%s', 'email_cancelled_order', 'enabled' ) ); + $request->set_body_params( + array( + 'value' => 'no', + ) + ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 200, $response->get_status() ); + } + + /** + * Test validation of radio settings. + * + * @since 3.0.0 + */ + public function test_validation_radio() { + wp_set_current_user( $this->user ); + + // not a valid option. + $request = new WP_REST_Request( 'PUT', sprintf( '/wc/v2/settings/%s/%s', 'shipping', 'woocommerce_ship_to_destination' ) ); + $request->set_body_params( + array( + 'value' => 'billing2', + ) + ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 400, $response->get_status() ); + + // valid. + $request = new WP_REST_Request( 'PUT', sprintf( '/wc/v2/settings/%s/%s', 'shipping', 'woocommerce_ship_to_destination' ) ); + $request->set_body_params( + array( + 'value' => 'billing', + ) + ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 200, $response->get_status() ); + } + + /** + * Test validation of multiselect. + * + * @since 3.0.0 + */ + public function test_validation_multiselect() { + wp_set_current_user( $this->user ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', sprintf( '/wc/v2/settings/%s/%s', 'general', 'woocommerce_specific_allowed_countries' ) ) ); + $setting = $response->get_data(); + $this->assertEmpty( $setting['value'] ); + + $request = new WP_REST_Request( 'PUT', sprintf( '/wc/v2/settings/%s/%s', 'general', 'woocommerce_specific_allowed_countries' ) ); + $request->set_body_params( + array( + 'value' => array( 'AX', 'DZ', 'MMM' ), + ) + ); + $response = $this->server->dispatch( $request ); + $setting = $response->get_data(); + $this->assertEquals( array( 'AX', 'DZ' ), $setting['value'] ); + } + + /** + * Test validation of select. + * + * @since 3.0.0 + */ + public function test_validation_select() { + wp_set_current_user( $this->user ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', sprintf( '/wc/v2/settings/%s/%s', 'products', 'woocommerce_weight_unit' ) ) ); + $setting = $response->get_data(); + $this->assertEquals( 'kg', $setting['value'] ); + + // invalid. + $request = new WP_REST_Request( 'PUT', sprintf( '/wc/v2/settings/%s/%s', 'products', 'woocommerce_weight_unit' ) ); + $request->set_body_params( + array( + 'value' => 'pounds', // invalid, should be lbs. + ) + ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 400, $response->get_status() ); + + // valid. + $request = new WP_REST_Request( 'PUT', sprintf( '/wc/v2/settings/%s/%s', 'products', 'woocommerce_weight_unit' ) ); + $request->set_body_params( + array( + 'value' => 'lbs', // invalid, should be lbs. + ) + ); + $response = $this->server->dispatch( $request ); + $setting = $response->get_data(); + $this->assertEquals( 'lbs', $setting['value'] ); + } + + /** + * Test to make sure the 'base location' setting is present in the response. + * That it is returned as 'select' and not 'single_select_country', + * and that both state and country options are returned. + * + * @since 3.0.7 + */ + public function test_woocommerce_default_country() { + wp_set_current_user( $this->user ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/settings/general/woocommerce_default_country' ) ); + $setting = $response->get_data(); + + $this->assertEquals( 'select', $setting['type'] ); + $this->assertArrayHasKey( 'GB', $setting['options'] ); + $this->assertArrayHasKey( 'US:OR', $setting['options'] ); + } + + /** + * Test to make sure the store address setting can be fetched and updated. + * + * @since 3.1.1 + */ + public function test_woocommerce_store_address() { + wp_set_current_user( $this->user ); + update_option( 'woocommerce_store_address', rand( 1000, 9999 ) ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/settings/general/woocommerce_store_address' ) ); + $setting = $response->get_data(); + $this->assertEquals( 'text', $setting['type'] ); + + // Repalce the old value with something uniquely new. + $old_value = $setting['value']; + $new_value = $old_value . ' ' . rand( 1000, 9999 ); + $request = new WP_REST_Request( 'PUT', '/wc/v2/settings/general/woocommerce_store_address' ); + $request->set_body_params( + array( + 'value' => $new_value, + ) + ); + $response = $this->server->dispatch( $request ); + $setting = $response->get_data(); + $this->assertEquals( $new_value, $setting['value'] ); + + // Put the original value back. + $request = new WP_REST_Request( 'PUT', '/wc/v2/settings/general/woocommerce_store_address' ); + $request->set_body_params( + array( + 'value' => $old_value, + ) + ); + $response = $this->server->dispatch( $request ); + $setting = $response->get_data(); + $this->assertEquals( $old_value, $setting['value'] ); + } + + /** + * Test to make sure the store address 2 (line 2) setting can be fetched and updated. + * + * @since 3.1.1 + */ + public function test_woocommerce_store_address_2() { + wp_set_current_user( $this->user ); + update_option( 'woocommerce_store_address_2', rand( 1000, 9999 ) ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/settings/general/woocommerce_store_address_2' ) ); + $setting = $response->get_data(); + $this->assertEquals( 'text', $setting['type'] ); + + // Repalce the old value with something uniquely new. + $old_value = $setting['value']; + $new_value = $old_value . ' ' . rand( 1000, 9999 ); + $request = new WP_REST_Request( 'PUT', '/wc/v2/settings/general/woocommerce_store_address_2' ); + $request->set_body_params( + array( + 'value' => $new_value, + ) + ); + $response = $this->server->dispatch( $request ); + $setting = $response->get_data(); + $this->assertEquals( $new_value, $setting['value'] ); + + // Put the original value back. + $request = new WP_REST_Request( 'PUT', '/wc/v2/settings/general/woocommerce_store_address_2' ); + $request->set_body_params( + array( + 'value' => $old_value, + ) + ); + $response = $this->server->dispatch( $request ); + $setting = $response->get_data(); + $this->assertEquals( $old_value, $setting['value'] ); + } + + /** + * Test to make sure the store city setting can be fetched and updated. + * + * @since 3.1.1 + */ + public function test_woocommerce_store_city() { + wp_set_current_user( $this->user ); + update_option( 'woocommerce_store_city', rand( 1000, 9999 ) ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/settings/general/woocommerce_store_city' ) ); + $setting = $response->get_data(); + $this->assertEquals( 'text', $setting['type'] ); + + // Repalce the old value with something uniquely new. + $old_value = $setting['value']; + $new_value = $old_value . ' ' . rand( 1000, 9999 ); + $request = new WP_REST_Request( 'PUT', '/wc/v2/settings/general/woocommerce_store_city' ); + $request->set_body_params( + array( + 'value' => $new_value, + ) + ); + $response = $this->server->dispatch( $request ); + $setting = $response->get_data(); + $this->assertEquals( $new_value, $setting['value'] ); + + // Put the original value back. + $request = new WP_REST_Request( 'PUT', '/wc/v2/settings/general/woocommerce_store_city' ); + $request->set_body_params( + array( + 'value' => $old_value, + ) + ); + $response = $this->server->dispatch( $request ); + $setting = $response->get_data(); + $this->assertEquals( $old_value, $setting['value'] ); + } + + /** + * Test to make sure the store postcode setting can be fetched and updated. + * + * @since 3.1.1 + */ + public function test_woocommerce_store_postcode() { + wp_set_current_user( $this->user ); + update_option( 'woocommerce_store_postcode', rand( 1000, 9999 ) ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/settings/general/woocommerce_store_postcode' ) ); + $setting = $response->get_data(); + $this->assertEquals( 'text', $setting['type'] ); + + // Repalce the old value with something uniquely new. + $old_value = $setting['value']; + $new_value = $old_value . ' ' . rand( 1000, 9999 ); + $request = new WP_REST_Request( 'PUT', '/wc/v2/settings/general/woocommerce_store_postcode' ); + $request->set_body_params( + array( + 'value' => $new_value, + ) + ); + $response = $this->server->dispatch( $request ); + $setting = $response->get_data(); + $this->assertEquals( $new_value, $setting['value'] ); + + // Put the original value back. + $request = new WP_REST_Request( 'PUT', '/wc/v2/settings/general/woocommerce_store_postcode' ); + $request->set_body_params( + array( + 'value' => $old_value, + ) + ); + $response = $this->server->dispatch( $request ); + $setting = $response->get_data(); + $this->assertEquals( $old_value, $setting['value'] ); + } +} diff --git a/tests/legacy/unit-tests/rest-api/Tests/Version2/shipping-methods.php b/tests/legacy/unit-tests/rest-api/Tests/Version2/shipping-methods.php new file mode 100644 index 00000000000..65c153e8f31 --- /dev/null +++ b/tests/legacy/unit-tests/rest-api/Tests/Version2/shipping-methods.php @@ -0,0 +1,143 @@ +endpoint = new WC_REST_Shipping_Methods_Controller(); + $this->user = $this->factory->user->create( + array( + 'role' => 'administrator', + ) + ); + } + + /** + * Test route registration. + * + * @since 3.0.0 + */ + public function test_register_routes() { + $routes = $this->server->get_routes(); + $this->assertArrayHasKey( '/wc/v2/shipping_methods', $routes ); + $this->assertArrayHasKey( '/wc/v2/shipping_methods/(?P[\w-]+)', $routes ); + } + + /** + * Test getting all shipping methods. + * + * @since 3.0.0 + */ + public function test_get_shipping_methods() { + wp_set_current_user( $this->user ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/shipping_methods' ) ); + $methods = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertContains( + array( + 'id' => 'free_shipping', + 'title' => 'Free shipping', + 'description' => 'Free shipping is a special method which can be triggered with coupons and minimum spends.', + '_links' => array( + 'self' => array( + array( + 'href' => rest_url( '/wc/v2/shipping_methods/free_shipping' ), + ), + ), + 'collection' => array( + array( + 'href' => rest_url( '/wc/v2/shipping_methods' ), + ), + ), + ), + ), + $methods + ); + } + + /** + * Tests to make sure shipping methods cannot viewed without valid permissions. + * + * @since 3.0.0 + */ + public function test_get_shipping_methods_without_permission() { + wp_set_current_user( 0 ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/shipping_methods' ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Tests getting a single shipping method. + * + * @since 3.0.0 + */ + public function test_get_shipping_method() { + wp_set_current_user( $this->user ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/shipping_methods/local_pickup' ) ); + $method = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( + array( + 'id' => 'local_pickup', + 'title' => 'Local pickup', + 'description' => 'Allow customers to pick up orders themselves. By default, when using local pickup store base taxes will apply regardless of customer address.', + ), + $method + ); + } + + /** + * Tests getting a single shipping method without the correct permissions. + * + * @since 3.0.0 + */ + public function test_get_shipping_method_without_permission() { + wp_set_current_user( 0 ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/shipping_methods/local_pickup' ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Tests getting a shipping method with an invalid ID. + * + * @since 3.0.0 + */ + public function test_get_shipping_method_invalid_id() { + wp_set_current_user( $this->user ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/shipping_methods/fake_method' ) ); + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * Test the shipping method schema. + * + * @since 3.0.0 + */ + public function test_shipping_method_schema() { + wp_set_current_user( $this->user ); + + $request = new WP_REST_Request( 'OPTIONS', '/wc/v2/shipping_methods' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $properties = $data['schema']['properties']; + + $this->assertEquals( 3, count( $properties ) ); + $this->assertArrayHasKey( 'id', $properties ); + $this->assertArrayHasKey( 'title', $properties ); + $this->assertArrayHasKey( 'description', $properties ); + } +} diff --git a/tests/legacy/unit-tests/rest-api/Tests/Version2/shipping-zones.php b/tests/legacy/unit-tests/rest-api/Tests/Version2/shipping-zones.php new file mode 100644 index 00000000000..1d9ea5368d3 --- /dev/null +++ b/tests/legacy/unit-tests/rest-api/Tests/Version2/shipping-zones.php @@ -0,0 +1,800 @@ +endpoint = new WC_REST_Shipping_Zones_Controller(); + $this->user = $this->factory->user->create( + array( + 'role' => 'administrator', + ) + ); + $this->zones = array(); + } + + /** + * Helper method to create a Shipping Zone. + * + * @param string $name Zone name. + * @param int $order Optional. Zone sort order. + * @return WC_Shipping_Zone + */ + protected function create_shipping_zone( $name, $order = 0, $locations = array() ) { + $zone = new WC_Shipping_Zone( null ); + $zone->set_zone_name( $name ); + $zone->set_zone_order( $order ); + $zone->set_locations( $locations ); + $zone->save(); + + $this->zones[] = $zone; + + return $zone; + } + + /** + * Test route registration. + * @since 3.0.0 + */ + public function test_register_routes() { + $routes = $this->server->get_routes(); + $this->assertArrayHasKey( '/wc/v2/shipping/zones', $routes ); + $this->assertArrayHasKey( '/wc/v2/shipping/zones/(?P[\d]+)', $routes ); + $this->assertArrayHasKey( '/wc/v2/shipping/zones/(?P[\d]+)/locations', $routes ); + $this->assertArrayHasKey( '/wc/v2/shipping/zones/(?P[\d]+)/methods', $routes ); + $this->assertArrayHasKey( '/wc/v2/shipping/zones/(?P[\d]+)/methods/(?P[\d]+)', $routes ); + } + + /** + * Test getting all Shipping Zones. + * @since 3.0.0 + */ + public function test_get_zones() { + wp_set_current_user( $this->user ); + + // "Rest of the World" zone exists by default + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/shipping/zones' ) ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( count( $data ), 1 ); + $this->assertContains( + array( + 'id' => $data[0]['id'], + 'name' => 'Locations not covered by your other zones', + 'order' => 0, + '_links' => array( + 'self' => array( + array( + 'href' => rest_url( '/wc/v2/shipping/zones/' . $data[0]['id'] ), + ), + ), + 'collection' => array( + array( + 'href' => rest_url( '/wc/v2/shipping/zones' ), + ), + ), + 'describedby' => array( + array( + 'href' => rest_url( '/wc/v2/shipping/zones/' . $data[0]['id'] . '/locations' ), + ), + ), + ), + ), + $data + ); + + // Create a zone and make sure it's in the response + $this->create_shipping_zone( 'Zone 1' ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/shipping/zones' ) ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( count( $data ), 2 ); + $this->assertContains( + array( + 'id' => $data[1]['id'], + 'name' => 'Zone 1', + 'order' => 0, + '_links' => array( + 'self' => array( + array( + 'href' => rest_url( '/wc/v2/shipping/zones/' . $data[1]['id'] ), + ), + ), + 'collection' => array( + array( + 'href' => rest_url( '/wc/v2/shipping/zones' ), + ), + ), + 'describedby' => array( + array( + 'href' => rest_url( '/wc/v2/shipping/zones/' . $data[1]['id'] . '/locations' ), + ), + ), + ), + ), + $data + ); + } + + /** + * Test /shipping/zones without valid permissions/creds. + * @since 3.0.0 + */ + public function test_get_shipping_zones_without_permission() { + wp_set_current_user( 0 ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/shipping/zones' ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test /shipping/zones while Shipping is disabled in WooCommerce. + * @since 3.0.0 + */ + public function test_get_shipping_zones_disabled_shipping() { + wp_set_current_user( $this->user ); + + add_filter( 'wc_shipping_enabled', '__return_false' ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/shipping/zones' ) ); + $this->assertEquals( 404, $response->get_status() ); + + remove_filter( 'wc_shipping_enabled', '__return_false' ); + } + + /** + * Test Shipping Zone schema. + * @since 3.0.0 + */ + public function test_get_shipping_zone_schema() { + $request = new WP_REST_Request( 'OPTIONS', '/wc/v2/shipping/zones' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $properties = $data['schema']['properties']; + $this->assertEquals( 3, count( $properties ) ); + $this->assertArrayHasKey( 'id', $properties ); + $this->assertTrue( $properties['id']['readonly'] ); + $this->assertArrayHasKey( 'name', $properties ); + $this->assertArrayHasKey( 'order', $properties ); + } + + /** + * Test Shipping Zone create endpoint. + * @since 3.0.0 + */ + public function test_create_shipping_zone() { + wp_set_current_user( $this->user ); + + $request = new WP_REST_Request( 'POST', '/wc/v2/shipping/zones' ); + $request->set_body_params( + array( + 'name' => 'Test Zone', + 'order' => 1, + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 201, $response->get_status() ); + $this->assertEquals( + array( + 'id' => $data['id'], + 'name' => 'Test Zone', + 'order' => 1, + '_links' => array( + 'self' => array( + array( + 'href' => rest_url( '/wc/v2/shipping/zones/' . $data['id'] ), + ), + ), + 'collection' => array( + array( + 'href' => rest_url( '/wc/v2/shipping/zones' ), + ), + ), + 'describedby' => array( + array( + 'href' => rest_url( '/wc/v2/shipping/zones/' . $data['id'] . '/locations' ), + ), + ), + ), + ), + $data + ); + } + + /** + * Test Shipping Zone create endpoint. + * @since 3.0.0 + */ + public function test_create_shipping_zone_without_permission() { + wp_set_current_user( 0 ); + + $request = new WP_REST_Request( 'POST', '/wc/v2/shipping/zones' ); + $request->set_body_params( + array( + 'name' => 'Test Zone', + 'order' => 1, + ) + ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test Shipping Zone update endpoint. + * @since 3.0.0 + */ + public function test_update_shipping_zone() { + wp_set_current_user( $this->user ); + + $zone = $this->create_shipping_zone( 'Test Zone' ); + + $request = new WP_REST_Request( 'PUT', '/wc/v2/shipping/zones/' . $zone->get_id() ); + $request->set_body_params( + array( + 'name' => 'Zone Test', + 'order' => 2, + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( + array( + 'id' => $zone->get_id(), + 'name' => 'Zone Test', + 'order' => 2, + '_links' => array( + 'self' => array( + array( + 'href' => rest_url( '/wc/v2/shipping/zones/' . $zone->get_id() ), + ), + ), + 'collection' => array( + array( + 'href' => rest_url( '/wc/v2/shipping/zones' ), + ), + ), + 'describedby' => array( + array( + 'href' => rest_url( '/wc/v2/shipping/zones/' . $zone->get_id() . '/locations' ), + ), + ), + ), + ), + $data + ); + } + + /** + * Test Shipping Zone update endpoint with a bad zone ID. + * @since 3.0.0 + */ + public function test_update_shipping_zone_invalid_id() { + wp_set_current_user( $this->user ); + + $request = new WP_REST_Request( 'PUT', '/wc/v2/shipping/zones/555555' ); + $request->set_body_params( + array( + 'name' => 'Zone Test', + 'order' => 2, + ) + ); + $response = $this->server->dispatch( $request ); + + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * Test Shipping Zone delete endpoint. + * @since 3.0.0 + */ + public function test_delete_shipping_zone() { + wp_set_current_user( $this->user ); + $zone = $this->create_shipping_zone( 'Zone 1' ); + + $request = new WP_REST_Request( 'DELETE', '/wc/v2/shipping/zones/' . $zone->get_id() ); + $request->set_param( 'force', true ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + } + + /** + * Test Shipping Zone delete endpoint without permissions. + * @since 3.0.0 + */ + public function test_delete_shipping_zone_without_permission() { + wp_set_current_user( 0 ); + $zone = $this->create_shipping_zone( 'Zone 1' ); + + $request = new WP_REST_Request( 'DELETE', '/wc/v2/shipping/zones/' . $zone->get_id() ); + $request->set_param( 'force', true ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test Shipping Zone delete endpoint with a bad zone ID. + * @since 3.0.0 + */ + public function test_delete_shipping_zone_invalid_id() { + wp_set_current_user( $this->user ); + $request = new WP_REST_Request( 'DELETE', '/wc/v2/shipping/zones/555555' ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * Test getting a single Shipping Zone. + * @since 3.0.0 + */ + public function test_get_single_shipping_zone() { + wp_set_current_user( $this->user ); + + $zone = $this->create_shipping_zone( 'Test Zone' ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/shipping/zones/' . $zone->get_id() ) ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( + array( + 'id' => $zone->get_id(), + 'name' => 'Test Zone', + 'order' => 0, + '_links' => array( + 'self' => array( + array( + 'href' => rest_url( '/wc/v2/shipping/zones/' . $zone->get_id() ), + ), + ), + 'collection' => array( + array( + 'href' => rest_url( '/wc/v2/shipping/zones' ), + ), + ), + 'describedby' => array( + array( + 'href' => rest_url( '/wc/v2/shipping/zones/' . $zone->get_id() . '/locations' ), + ), + ), + ), + ), + $data + ); + } + + /** + * Test getting a single Shipping Zone with a bad zone ID. + * @since 3.0.0 + */ + public function test_get_single_shipping_zone_invalid_id() { + wp_set_current_user( $this->user ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/shipping/zones/1' ) ); + + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * Test getting Shipping Zone Locations. + * @since 3.0.0 + */ + public function test_get_locations() { + wp_set_current_user( $this->user ); + + // Create a zone + $zone = $this->create_shipping_zone( + 'Zone 1', + 0, + array( + array( + 'code' => 'US', + 'type' => 'country', + ), + ) + ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/shipping/zones/' . $zone->get_id() . '/locations' ) ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( count( $data ), 1 ); + $this->assertEquals( + array( + array( + 'code' => 'US', + 'type' => 'country', + '_links' => array( + 'collection' => array( + array( + 'href' => rest_url( '/wc/v2/shipping/zones/' . $zone->get_id() . '/locations' ), + ), + ), + 'describes' => array( + array( + 'href' => rest_url( '/wc/v2/shipping/zones/' . $zone->get_id() ), + ), + ), + ), + ), + ), + $data + ); + } + + /** + * Test getting Shipping Zone Locations with a bad zone ID. + * @since 3.0.0 + */ + public function test_get_locations_invalid_id() { + wp_set_current_user( $this->user ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/shipping/zones/1/locations' ) ); + + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * Test Shipping Zone Locations update endpoint. + * @since 3.0.0 + */ + public function test_update_locations() { + wp_set_current_user( $this->user ); + + $zone = $this->create_shipping_zone( 'Test Zone' ); + + $request = new WP_REST_Request( 'PUT', '/wc/v2/shipping/zones/' . $zone->get_id() . '/locations' ); + $request->add_header( 'Content-Type', 'application/json' ); + $request->set_body( + json_encode( + array( + array( + 'code' => 'UK', + 'type' => 'country', + ), + array( + 'code' => 'US', // test that locations missing "type" treated as country. + ), + array( + 'code' => 'SW1A0AA', + 'type' => 'postcode', + ), + array( + 'type' => 'continent', // test that locations missing "code" aren't saved + ), + ) + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 3, count( $data ) ); + $this->assertEquals( + array( + array( + 'code' => 'UK', + 'type' => 'country', + '_links' => array( + 'collection' => array( + array( + 'href' => rest_url( '/wc/v2/shipping/zones/' . $zone->get_id() . '/locations' ), + ), + ), + 'describes' => array( + array( + 'href' => rest_url( '/wc/v2/shipping/zones/' . $zone->get_id() ), + ), + ), + ), + ), + array( + 'code' => 'US', + 'type' => 'country', + '_links' => array( + 'collection' => array( + array( + 'href' => rest_url( '/wc/v2/shipping/zones/' . $zone->get_id() . '/locations' ), + ), + ), + 'describes' => array( + array( + 'href' => rest_url( '/wc/v2/shipping/zones/' . $zone->get_id() ), + ), + ), + ), + ), + array( + 'code' => 'SW1A0AA', + 'type' => 'postcode', + '_links' => array( + 'collection' => array( + array( + 'href' => rest_url( '/wc/v2/shipping/zones/' . $zone->get_id() . '/locations' ), + ), + ), + 'describes' => array( + array( + 'href' => rest_url( '/wc/v2/shipping/zones/' . $zone->get_id() ), + ), + ), + ), + ), + ), + $data + ); + } + + /** + * Test updating Shipping Zone Locations with a bad zone ID. + * @since 3.0.0 + */ + public function test_update_locations_invalid_id() { + wp_set_current_user( $this->user ); + + $response = $this->server->dispatch( new WP_REST_Request( 'PUT', '/wc/v2/shipping/zones/1/locations' ) ); + + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * Test getting all Shipping Zone Methods and getting a single Shipping Zone Method. + * @since 3.0.0 + */ + public function test_get_methods() { + wp_set_current_user( $this->user ); + + // Create a shipping method and make sure it's in the response + $zone = $this->create_shipping_zone( 'Zone 1' ); + $instance_id = $zone->add_shipping_method( 'flat_rate' ); + $methods = $zone->get_shipping_methods(); + $method = $methods[ $instance_id ]; + + $settings = array(); + $method->init_instance_settings(); + foreach ( $method->get_instance_form_fields() as $id => $field ) { + $data = array( + 'id' => $id, + 'label' => $field['title'], + 'description' => ( empty( $field['description'] ) ? '' : $field['description'] ), + 'type' => $field['type'], + 'value' => $method->instance_settings[ $id ], + 'default' => ( empty( $field['default'] ) ? '' : $field['default'] ), + 'tip' => ( empty( $field['description'] ) ? '' : $field['description'] ), + 'placeholder' => ( empty( $field['placeholder'] ) ? '' : $field['placeholder'] ), + ); + if ( ! empty( $field['options'] ) ) { + $data['options'] = $field['options']; + } + $settings[ $id ] = $data; + } + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/shipping/zones/' . $zone->get_id() . '/methods' ) ); + $data = $response->get_data(); + $expected = array( + 'id' => $instance_id, + 'instance_id' => $instance_id, + 'title' => $method->instance_settings['title'], + 'order' => $method->method_order, + 'enabled' => ( 'yes' === $method->enabled ), + 'method_id' => $method->id, + 'method_title' => $method->method_title, + 'method_description' => $method->method_description, + 'settings' => $settings, + '_links' => array( + 'self' => array( + array( + 'href' => rest_url( '/wc/v2/shipping/zones/' . $zone->get_id() . '/methods/' . $instance_id ), + ), + ), + 'collection' => array( + array( + 'href' => rest_url( '/wc/v2/shipping/zones/' . $zone->get_id() . '/methods' ), + ), + ), + 'describes' => array( + array( + 'href' => rest_url( '/wc/v2/shipping/zones/' . $zone->get_id() ), + ), + ), + ), + ); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( count( $data ), 1 ); + $this->assertContains( $expected, $data ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/shipping/zones/' . $zone->get_id() . '/methods/' . $instance_id ) ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( $expected, $data ); + } + + /** + * Test getting all Shipping Zone Methods with a bad zone ID. + * @since 3.0.0 + */ + public function test_get_methods_invalid_zone_id() { + wp_set_current_user( $this->user ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/shipping/zones/1/methods' ) ); + + $this->assertEquals( 404, $response->get_status() ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/shipping/zones/1/methods/1' ) ); + + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * Test getting a single Shipping Zone Method with a bad ID. + * @since 3.0.0 + */ + public function test_get_methods_invalid_method_id() { + wp_set_current_user( $this->user ); + + $zone = $this->create_shipping_zone( 'Zone 1' ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/shipping/zones/' . $zone->get_id() . '/methods/1' ) ); + + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * Test updating a Shipping Zone Method. + * @since 3.0.0 + */ + public function test_update_methods() { + wp_set_current_user( $this->user ); + + $zone = $this->create_shipping_zone( 'Zone 1' ); + $instance_id = $zone->add_shipping_method( 'flat_rate' ); + $methods = $zone->get_shipping_methods(); + $method = $methods[ $instance_id ]; + + // Test defaults + $request = new WP_REST_Request( 'GET', '/wc/v2/shipping/zones/' . $zone->get_id() . '/methods/' . $instance_id ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertArrayHasKey( 'title', $data['settings'] ); + $this->assertEquals( 'Flat rate', $data['settings']['title']['value'] ); + $this->assertArrayHasKey( 'tax_status', $data['settings'] ); + $this->assertEquals( 'taxable', $data['settings']['tax_status']['value'] ); + $this->assertArrayHasKey( 'cost', $data['settings'] ); + $this->assertEquals( '0', $data['settings']['cost']['value'] ); + + // Update a single value + $request = new WP_REST_Request( 'POST', '/wc/v2/shipping/zones/' . $zone->get_id() . '/methods/' . $instance_id ); + $request->set_body_params( + array( + 'settings' => array( + 'cost' => 5, + ), + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertArrayHasKey( 'title', $data['settings'] ); + $this->assertEquals( 'Flat rate', $data['settings']['title']['value'] ); + $this->assertArrayHasKey( 'tax_status', $data['settings'] ); + $this->assertEquals( 'taxable', $data['settings']['tax_status']['value'] ); + $this->assertArrayHasKey( 'cost', $data['settings'] ); + $this->assertEquals( '5', $data['settings']['cost']['value'] ); + + // Test multiple settings + $request = new WP_REST_Request( 'POST', '/wc/v2/shipping/zones/' . $zone->get_id() . '/methods/' . $instance_id ); + $request->set_body_params( + array( + 'settings' => array( + 'cost' => 10, + 'tax_status' => 'none', + ), + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertArrayHasKey( 'title', $data['settings'] ); + $this->assertEquals( 'Flat rate', $data['settings']['title']['value'] ); + $this->assertArrayHasKey( 'tax_status', $data['settings'] ); + $this->assertEquals( 'none', $data['settings']['tax_status']['value'] ); + $this->assertArrayHasKey( 'cost', $data['settings'] ); + $this->assertEquals( '10', $data['settings']['cost']['value'] ); + + // Test bogus + $request = new WP_REST_Request( 'POST', '/wc/v2/shipping/zones/' . $zone->get_id() . '/methods/' . $instance_id ); + $request->set_body_params( + array( + 'settings' => array( + 'cost' => 10, + 'tax_status' => 'this_is_not_a_valid_option', + ), + ) + ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 400, $response->get_status() ); + + // Test other parameters + $this->assertTrue( $data['enabled'] ); + $this->assertEquals( 1, $data['order'] ); + + $request = new WP_REST_Request( 'POST', '/wc/v2/shipping/zones/' . $zone->get_id() . '/methods/' . $instance_id ); + $request->set_body_params( + array( + 'enabled' => false, + 'order' => 2, + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertFalse( $data['enabled'] ); + $this->assertEquals( 2, $data['order'] ); + $this->assertArrayHasKey( 'cost', $data['settings'] ); + $this->assertEquals( '10', $data['settings']['cost']['value'] ); + } + + /** + * Test creating a Shipping Zone Method. + * @since 3.0.0 + */ + public function test_create_method() { + wp_set_current_user( $this->user ); + $zone = $this->create_shipping_zone( 'Zone 1' ); + $request = new WP_REST_Request( 'POST', '/wc/v2/shipping/zones/' . $zone->get_id() . '/methods' ); + $request->set_body_params( + array( + 'method_id' => 'flat_rate', + 'enabled' => false, + 'order' => 2, + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertFalse( $data['enabled'] ); + $this->assertEquals( 2, $data['order'] ); + $this->assertArrayHasKey( 'cost', $data['settings'] ); + $this->assertEquals( '0', $data['settings']['cost']['value'] ); + } + + /** + * Test deleting a Shipping Zone Method. + * @since 3.0.0 + */ + public function test_delete_method() { + wp_set_current_user( $this->user ); + $zone = $this->create_shipping_zone( 'Zone 1' ); + $instance_id = $zone->add_shipping_method( 'flat_rate' ); + $methods = $zone->get_shipping_methods(); + $method = $methods[ $instance_id ]; + $request = new WP_REST_Request( 'DELETE', '/wc/v2/shipping/zones/' . $zone->get_id() . '/methods/' . $instance_id ); + $request->set_param( 'force', true ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 200, $response->get_status() ); + } +} diff --git a/tests/legacy/unit-tests/rest-api/Tests/Version2/system-status.php b/tests/legacy/unit-tests/rest-api/Tests/Version2/system-status.php new file mode 100644 index 00000000000..cb359b0630b --- /dev/null +++ b/tests/legacy/unit-tests/rest-api/Tests/Version2/system-status.php @@ -0,0 +1,355 @@ +endpoint = new WC_REST_System_Status_Controller(); + $this->user = $this->factory->user->create( + array( + 'role' => 'administrator', + ) + ); + } + + /** + * Test route registration. + */ + public function test_register_routes() { + $routes = $this->server->get_routes(); + $this->assertArrayHasKey( '/wc/v2/system_status', $routes ); + $this->assertArrayHasKey( '/wc/v2/system_status/tools', $routes ); + $this->assertArrayHasKey( '/wc/v2/system_status/tools/(?P[\w-]+)', $routes ); + } + + /** + * Test to make sure system status cannot be accessed without valid creds + * + * @since 3.0.0 + */ + public function test_get_system_status_info_without_permission() { + wp_set_current_user( 0 ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/system_status' ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test to make sure root properties are present. + * (environment, theme, database, etc). + * + * @since 3.0.0 + */ + public function test_get_system_status_info_returns_root_properties() { + wp_set_current_user( $this->user ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/system_status' ) ); + $data = $response->get_data(); + + $this->assertArrayHasKey( 'environment', $data ); + $this->assertArrayHasKey( 'database', $data ); + $this->assertArrayHasKey( 'active_plugins', $data ); + $this->assertArrayHasKey( 'theme', $data ); + $this->assertArrayHasKey( 'settings', $data ); + $this->assertArrayHasKey( 'security', $data ); + $this->assertArrayHasKey( 'pages', $data ); + } + + /** + * Test to make sure environment response is correct. + * + * @since 3.0.0 + */ + public function test_get_system_status_info_environment() { + wp_set_current_user( $this->user ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/system_status' ) ); + $data = $response->get_data(); + $environment = (array) $data['environment']; + + // Make sure all expected data is present. + $this->assertEquals( 32, count( $environment ) ); + + // Test some responses to make sure they match up. + $this->assertEquals( get_option( 'home' ), $environment['home_url'] ); + $this->assertEquals( get_option( 'siteurl' ), $environment['site_url'] ); + $this->assertEquals( WC()->version, $environment['version'] ); + } + + /** + * Test to make sure database response is correct. + * + * @since 3.0.0 + */ + public function test_get_system_status_info_database() { + global $wpdb; + wp_set_current_user( $this->user ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/system_status' ) ); + $data = $response->get_data(); + $database = (array) $data['database']; + + $this->assertEquals( get_option( 'woocommerce_db_version' ), $database['wc_database_version'] ); + $this->assertEquals( $wpdb->prefix, $database['database_prefix'] ); + $this->assertArrayHasKey( 'woocommerce', $database['database_tables'], print_r( $database, true ) ); + $this->assertArrayHasKey( $wpdb->prefix . 'woocommerce_payment_tokens', $database['database_tables']['woocommerce'], print_r( $database, true ) ); + } + + /** + * Test to make sure active plugins response is correct. + * + * @since 3.0.0 + */ + public function test_get_system_status_info_active_plugins() { + wp_set_current_user( $this->user ); + + $actual_plugins = array( 'hello.php' ); + update_option( 'active_plugins', $actual_plugins ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/system_status' ) ); + update_option( 'active_plugins', array() ); + + $data = $response->get_data(); + $plugins = (array) $data['active_plugins']; + + $this->assertEquals( 1, count( $plugins ) ); + $this->assertEquals( 'Hello Dolly', $plugins[0]['name'] ); + } + + /** + * Test to make sure theme response is correct. + * + * @since 3.0.0 + */ + public function test_get_system_status_info_theme() { + wp_set_current_user( $this->user ); + $active_theme = wp_get_theme(); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/system_status' ) ); + $data = $response->get_data(); + $theme = (array) $data['theme']; + + $this->assertEquals( 13, count( $theme ) ); + $this->assertEquals( $active_theme->Name, $theme['name'] ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.NotSnakeCaseMemberVar + } + + /** + * Test to make sure settings response is correct. + * + * @since 3.0.0 + */ + public function test_get_system_status_info_settings() { + wp_set_current_user( $this->user ); + + $term_response = array(); + $terms = get_terms( 'product_type', array( 'hide_empty' => 0 ) ); + foreach ( $terms as $term ) { + $term_response[ $term->slug ] = strtolower( $term->name ); + } + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/system_status' ) ); + $data = $response->get_data(); + $settings = (array) $data['settings']; + + $this->assertEquals( 12, count( $settings ) ); + $this->assertEquals( ( 'yes' === get_option( 'woocommerce_api_enabled' ) ), $settings['api_enabled'] ); + $this->assertEquals( get_woocommerce_currency(), $settings['currency'] ); + $this->assertEquals( $term_response, $settings['taxonomies'] ); + } + + /** + * Test to make sure security response is correct. + * + * @since 3.0.0 + */ + public function test_get_system_status_info_security() { + wp_set_current_user( $this->user ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/system_status' ) ); + $data = $response->get_data(); + $settings = (array) $data['security']; + + $this->assertEquals( 2, count( $settings ) ); + $this->assertEquals( 'https' === substr( wc_get_page_permalink( 'shop' ), 0, 5 ), $settings['secure_connection'] ); + $this->assertEquals( ! ( defined( 'WP_DEBUG' ) && defined( 'WP_DEBUG_DISPLAY' ) && WP_DEBUG && WP_DEBUG_DISPLAY ) || 0 === intval( ini_get( 'display_errors' ) ), $settings['hide_errors'] ); + } + + /** + * Test to make sure pages response is correct. + * + * @since 3.0.0 + */ + public function test_get_system_status_info_pages() { + wp_set_current_user( $this->user ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/system_status' ) ); + $data = $response->get_data(); + $pages = $data['pages']; + $this->assertEquals( 5, count( $pages ) ); + } + + /** + * Test system status schema. + * + * @since 3.0.0 + */ + public function test_system_status_schema() { + $request = new WP_REST_Request( 'OPTIONS', '/wc/v2/system_status' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $properties = $data['schema']['properties']; + $this->assertEquals( 10, count( $properties ) ); + $this->assertArrayHasKey( 'environment', $properties ); + $this->assertArrayHasKey( 'database', $properties ); + $this->assertArrayHasKey( 'active_plugins', $properties ); + $this->assertArrayHasKey( 'theme', $properties ); + $this->assertArrayHasKey( 'settings', $properties ); + $this->assertArrayHasKey( 'security', $properties ); + $this->assertArrayHasKey( 'pages', $properties ); + } + + /** + * Test to make sure get_items (all tools) response is correct. + * + * @since 3.0.0 + */ + public function test_get_system_tools() { + wp_set_current_user( $this->user ); + + $tools_controller = new WC_REST_System_Status_Tools_Controller(); + $raw_tools = $tools_controller->get_tools(); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/system_status/tools' ) ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( count( $raw_tools ), count( $data ) ); + $this->assertContains( + array( + 'id' => 'regenerate_thumbnails', + 'name' => 'Regenerate shop thumbnails', + 'action' => 'Regenerate', + 'description' => 'This will regenerate all shop thumbnails to match your theme and/or image settings.', + '_links' => array( + 'item' => array( + array( + 'href' => rest_url( '/wc/v2/system_status/tools/regenerate_thumbnails' ), + 'embeddable' => true, + ), + ), + ), + ), + $data + ); + } + + /** + * Test to make sure system status tools cannot be accessed without valid creds + * + * @since 3.0.0 + */ + public function test_get_system_status_tools_without_permission() { + wp_set_current_user( 0 ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/system_status/tools' ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test to make sure we can load a single tool correctly. + * + * @since 3.0.0 + */ + public function test_get_system_tool() { + wp_set_current_user( $this->user ); + + $tools_controller = new WC_REST_System_Status_Tools_Controller(); + $raw_tools = $tools_controller->get_tools(); + $raw_tool = $raw_tools['recount_terms']; + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/system_status/tools/recount_terms' ) ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + + $this->assertEquals( 'recount_terms', $data['id'] ); + $this->assertEquals( 'Term counts', $data['name'] ); + $this->assertEquals( 'Recount terms', $data['action'] ); + $this->assertEquals( 'This tool will recount product terms - useful when changing your settings in a way which hides products from the catalog.', $data['description'] ); + } + + /** + * Test to make sure a single system status toolscannot be accessed without valid creds. + * + * @since 3.0.0 + */ + public function test_get_system_status_tool_without_permission() { + wp_set_current_user( 0 ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v2/system_status/tools/recount_terms' ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test to make sure we can RUN a tool correctly. + * + * @since 3.0.0 + */ + public function test_execute_system_tool() { + wp_set_current_user( $this->user ); + + $tools_controller = new WC_REST_System_Status_Tools_Controller(); + $raw_tools = $tools_controller->get_tools(); + $raw_tool = $raw_tools['recount_terms']; + + $response = $this->server->dispatch( new WP_REST_Request( 'POST', '/wc/v2/system_status/tools/recount_terms' ) ); + $data = $response->get_data(); + + $this->assertEquals( 'recount_terms', $data['id'] ); + $this->assertEquals( 'Term counts', $data['name'] ); + $this->assertEquals( 'Recount terms', $data['action'] ); + $this->assertEquals( 'This tool will recount product terms - useful when changing your settings in a way which hides products from the catalog.', $data['description'] ); + $this->assertTrue( $data['success'] ); + $this->assertEquals( 1, did_action( 'woocommerce_rest_insert_system_status_tool' ) ); + + $response = $this->server->dispatch( new WP_REST_Request( 'POST', '/wc/v2/system_status/tools/not_a_real_tool' ) ); + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * Test to make sure a tool cannot be run without valid creds. + * + * @since 3.0.0 + */ + public function test_execute_system_status_tool_without_permission() { + wp_set_current_user( 0 ); + $response = $this->server->dispatch( new WP_REST_Request( 'POST', '/wc/v2/system_status/tools/recount_terms' ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test system status schema. + * + * @since 3.0.0 + */ + public function test_system_status_tool_schema() { + $request = new WP_REST_Request( 'OPTIONS', '/wc/v2/system_status/tools' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $properties = $data['schema']['properties']; + + $this->assertEquals( 6, count( $properties ) ); + $this->assertArrayHasKey( 'id', $properties ); + $this->assertArrayHasKey( 'name', $properties ); + $this->assertArrayHasKey( 'action', $properties ); + $this->assertArrayHasKey( 'description', $properties ); + $this->assertArrayHasKey( 'success', $properties ); + $this->assertArrayHasKey( 'message', $properties ); + } +} diff --git a/tests/legacy/unit-tests/rest-api/Tests/Version3/coupons.php b/tests/legacy/unit-tests/rest-api/Tests/Version3/coupons.php new file mode 100644 index 00000000000..bd0564de6be --- /dev/null +++ b/tests/legacy/unit-tests/rest-api/Tests/Version3/coupons.php @@ -0,0 +1,471 @@ +endpoint = new WC_REST_Coupons_Controller(); + $this->user = $this->factory->user->create( + array( + 'role' => 'administrator', + ) + ); + } + + /** + * Test route registration. + * @since 3.5.0 + */ + public function test_register_routes() { + $routes = $this->server->get_routes(); + $this->assertArrayHasKey( '/wc/v3/coupons', $routes ); + $this->assertArrayHasKey( '/wc/v3/coupons/(?P[\d]+)', $routes ); + $this->assertArrayHasKey( '/wc/v3/coupons/batch', $routes ); + } + + /** + * Test getting coupons. + * @since 3.5.0 + */ + public function test_get_coupons() { + wp_set_current_user( $this->user ); + + $coupon_1 = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\CouponHelper::create_coupon( 'dummycoupon-1' ); + $post_1 = get_post( $coupon_1->get_id() ); + $coupon_2 = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\CouponHelper::create_coupon( 'dummycoupon-2' ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/coupons' ) ); + $coupons = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 2, count( $coupons ) ); + $this->assertContains( + array( + 'id' => $coupon_1->get_id(), + 'code' => 'dummycoupon-1', + 'amount' => '1.00', + 'date_created' => wc_rest_prepare_date_response( $post_1->post_date_gmt, false ), + 'date_created_gmt' => wc_rest_prepare_date_response( $post_1->post_date_gmt ), + 'date_modified' => wc_rest_prepare_date_response( $post_1->post_modified_gmt, false ), + 'date_modified_gmt' => wc_rest_prepare_date_response( $post_1->post_modified_gmt ), + 'discount_type' => 'fixed_cart', + 'description' => 'This is a dummy coupon', + 'date_expires' => '', + 'date_expires_gmt' => '', + 'usage_count' => 0, + 'individual_use' => false, + 'product_ids' => array(), + 'excluded_product_ids' => array(), + 'usage_limit' => '', + 'usage_limit_per_user' => '', + 'limit_usage_to_x_items' => null, + 'free_shipping' => false, + 'product_categories' => array(), + 'excluded_product_categories' => array(), + 'exclude_sale_items' => false, + 'minimum_amount' => '0.00', + 'maximum_amount' => '0.00', + 'email_restrictions' => array(), + 'used_by' => array(), + 'meta_data' => array(), + '_links' => array( + 'self' => array( + array( + 'href' => rest_url( '/wc/v3/coupons/' . $coupon_1->get_id() ), + ), + ), + 'collection' => array( + array( + 'href' => rest_url( '/wc/v3/coupons' ), + ), + ), + ), + ), + $coupons + ); + } + + /** + * Test getting coupons without valid permissions. + * @since 3.5.0 + */ + public function test_get_coupons_without_permission() { + wp_set_current_user( 0 ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/coupons' ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test getting a single coupon. + * @since 3.5.0 + */ + public function test_get_coupon() { + wp_set_current_user( $this->user ); + $coupon = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\CouponHelper::create_coupon( 'dummycoupon-1' ); + $post = get_post( $coupon->get_id() ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/coupons/' . $coupon->get_id() ) ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( + array( + 'id' => $coupon->get_id(), + 'code' => 'dummycoupon-1', + 'amount' => '1.00', + 'date_created' => wc_rest_prepare_date_response( $post->post_date_gmt, false ), + 'date_created_gmt' => wc_rest_prepare_date_response( $post->post_date_gmt ), + 'date_modified' => wc_rest_prepare_date_response( $post->post_modified_gmt, false ), + 'date_modified_gmt' => wc_rest_prepare_date_response( $post->post_modified_gmt ), + 'discount_type' => 'fixed_cart', + 'description' => 'This is a dummy coupon', + 'date_expires' => null, + 'date_expires_gmt' => null, + 'usage_count' => 0, + 'individual_use' => false, + 'product_ids' => array(), + 'excluded_product_ids' => array(), + 'usage_limit' => null, + 'usage_limit_per_user' => null, + 'limit_usage_to_x_items' => null, + 'free_shipping' => false, + 'product_categories' => array(), + 'excluded_product_categories' => array(), + 'exclude_sale_items' => false, + 'minimum_amount' => '0.00', + 'maximum_amount' => '0.00', + 'email_restrictions' => array(), + 'used_by' => array(), + 'meta_data' => array(), + ), + $data + ); + } + + /** + * Test getting a single coupon with an invalid ID. + * @since 3.5.0 + */ + public function test_get_coupon_invalid_id() { + wp_set_current_user( $this->user ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/coupons/0' ) ); + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * Test getting a single coupon without valid permissions. + * @since 3.5.0 + */ + public function test_get_coupon_without_permission() { + wp_set_current_user( 0 ); + $coupon = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\CouponHelper::create_coupon( 'dummycoupon-1' ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/coupons/' . $coupon->get_id() ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test creating a single coupon. + * @since 3.5.0 + */ + public function test_create_coupon() { + wp_set_current_user( $this->user ); + $request = new WP_REST_Request( 'POST', '/wc/v3/coupons' ); + $request->set_body_params( + array( + 'code' => 'test', + 'amount' => '5.00', + 'discount_type' => 'fixed_product', + 'description' => 'Test', + 'usage_limit' => 10, + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 201, $response->get_status() ); + $this->assertEquals( + array( + 'id' => $data['id'], + 'code' => 'test', + 'amount' => '5.00', + 'date_created' => $data['date_created'], + 'date_created_gmt' => $data['date_created_gmt'], + 'date_modified' => $data['date_modified'], + 'date_modified_gmt' => $data['date_modified_gmt'], + 'discount_type' => 'fixed_product', + 'description' => 'Test', + 'date_expires' => null, + 'date_expires_gmt' => null, + 'usage_count' => 0, + 'individual_use' => false, + 'product_ids' => array(), + 'excluded_product_ids' => array(), + 'usage_limit' => 10, + 'usage_limit_per_user' => null, + 'limit_usage_to_x_items' => null, + 'free_shipping' => false, + 'product_categories' => array(), + 'excluded_product_categories' => array(), + 'exclude_sale_items' => false, + 'minimum_amount' => '0.00', + 'maximum_amount' => '0.00', + 'email_restrictions' => array(), + 'used_by' => array(), + 'meta_data' => array(), + ), + $data + ); + } + + /** + * Test creating a single coupon with invalid fields. + * @since 3.5.0 + */ + public function test_create_coupon_invalid_fields() { + wp_set_current_user( $this->user ); + + // test no code... + $request = new WP_REST_Request( 'POST', '/wc/v3/coupons' ); + $request->set_body_params( + array( + 'amount' => '5.00', + 'discount_type' => 'fixed_product', + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 400, $response->get_status() ); + } + + /** + * Test creating a single coupon without valid permissions. + * @since 3.5.0 + */ + public function test_create_coupon_without_permission() { + wp_set_current_user( 0 ); + + // test no code... + $request = new WP_REST_Request( 'POST', '/wc/v3/coupons' ); + $request->set_body_params( + array( + 'code' => 'fail', + 'amount' => '5.00', + 'discount_type' => 'fixed_product', + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test updating a single coupon. + * @since 3.5.0 + */ + public function test_update_coupon() { + wp_set_current_user( $this->user ); + $coupon = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\CouponHelper::create_coupon( 'dummycoupon-1' ); + $post = get_post( $coupon->get_id() ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/coupons/' . $coupon->get_id() ) ); + $data = $response->get_data(); + $this->assertEquals( 'This is a dummy coupon', $data['description'] ); + $this->assertEquals( 'fixed_cart', $data['discount_type'] ); + $this->assertEquals( '1.00', $data['amount'] ); + + $request = new WP_REST_Request( 'PUT', '/wc/v3/coupons/' . $coupon->get_id() ); + $request->set_body_params( + array( + 'amount' => '10.00', + 'description' => 'New description', + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( '10.00', $data['amount'] ); + $this->assertEquals( 'New description', $data['description'] ); + $this->assertEquals( 'fixed_cart', $data['discount_type'] ); + } + + /** + * Test updating a single coupon with an invalid ID. + * @since 3.5.0 + */ + public function test_update_coupon_invalid_id() { + wp_set_current_user( $this->user ); + + $request = new WP_REST_Request( 'PUT', '/wc/v3/coupons/0' ); + $request->set_body_params( + array( + 'code' => 'tester', + 'amount' => '10.00', + 'description' => 'New description', + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 400, $response->get_status() ); + } + + /** + * Test updating a single coupon without valid permissions. + * @since 3.5.0 + */ + public function test_update_coupon_without_permission() { + wp_set_current_user( 0 ); + $coupon = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\CouponHelper::create_coupon( 'dummycoupon-1' ); + $post = get_post( $coupon->get_id() ); + + $request = new WP_REST_Request( 'PUT', '/wc/v3/coupons/' . $coupon->get_id() ); + $request->set_body_params( + array( + 'amount' => '10.00', + 'description' => 'New description', + ) + ); + $response = $this->server->dispatch( $request ); + + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test deleting a single coupon. + * @since 3.5.0 + */ + public function test_delete_coupon() { + wp_set_current_user( $this->user ); + $coupon = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\CouponHelper::create_coupon( 'dummycoupon-1' ); + $request = new WP_REST_Request( 'DELETE', '/wc/v3/coupons/' . $coupon->get_id() ); + $request->set_param( 'force', true ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 200, $response->get_status() ); + } + + /** + * Test deleting a single coupon with an invalid ID. + * @since 3.5.0 + */ + public function test_delete_coupon_invalid_id() { + wp_set_current_user( $this->user ); + $request = new WP_REST_Request( 'DELETE', '/wc/v3/coupons/0' ); + $request->set_param( 'force', true ); + $response = $this->server->dispatch( $request ); + + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * Test deleting a single coupon without valid permissions. + * @since 3.5.0 + */ + public function test_delete_coupon_without_permission() { + wp_set_current_user( 0 ); + $coupon = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\CouponHelper::create_coupon( 'dummycoupon-1' ); + $request = new WP_REST_Request( 'DELETE', '/wc/v3/coupons/' . $coupon->get_id() ); + $response = $this->server->dispatch( $request ); + + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test batch operations on coupons. + * @since 3.5.0 + */ + public function test_batch_coupon() { + wp_set_current_user( $this->user ); + + $coupon_1 = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\CouponHelper::create_coupon( 'dummycoupon-1' ); + $coupon_2 = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\CouponHelper::create_coupon( 'dummycoupon-2' ); + $coupon_3 = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\CouponHelper::create_coupon( 'dummycoupon-3' ); + $coupon_4 = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\CouponHelper::create_coupon( 'dummycoupon-4' ); + + $request = new WP_REST_Request( 'POST', '/wc/v3/coupons/batch' ); + $request->set_body_params( + array( + 'update' => array( + array( + 'id' => $coupon_1->get_id(), + 'amount' => '5.15', + ), + ), + 'delete' => array( + $coupon_2->get_id(), + $coupon_3->get_id(), + ), + 'create' => array( + array( + 'code' => 'new-coupon', + 'amount' => '11.00', + ), + ), + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( '5.15', $data['update'][0]['amount'] ); + $this->assertEquals( '11.00', $data['create'][0]['amount'] ); + $this->assertEquals( 'new-coupon', $data['create'][0]['code'] ); + $this->assertEquals( $coupon_2->get_id(), $data['delete'][0]['id'] ); + $this->assertEquals( $coupon_3->get_id(), $data['delete'][1]['id'] ); + + $request = new WP_REST_Request( 'GET', '/wc/v3/coupons' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 3, count( $data ) ); + } + + /** + * Test coupon schema. + * @since 3.5.0 + */ + public function test_coupon_schema() { + wp_set_current_user( $this->user ); + $request = new WP_REST_Request( 'OPTIONS', '/wc/v3/coupons' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $properties = $data['schema']['properties']; + + $this->assertEquals( 27, count( $properties ) ); + $this->assertArrayHasKey( 'id', $properties ); + $this->assertArrayHasKey( 'code', $properties ); + $this->assertArrayHasKey( 'date_created', $properties ); + $this->assertArrayHasKey( 'date_created_gmt', $properties ); + $this->assertArrayHasKey( 'date_modified', $properties ); + $this->assertArrayHasKey( 'date_modified_gmt', $properties ); + $this->assertArrayHasKey( 'description', $properties ); + $this->assertArrayHasKey( 'discount_type', $properties ); + $this->assertArrayHasKey( 'amount', $properties ); + $this->assertArrayHasKey( 'date_expires', $properties ); + $this->assertArrayHasKey( 'date_expires_gmt', $properties ); + $this->assertArrayHasKey( 'usage_count', $properties ); + $this->assertArrayHasKey( 'individual_use', $properties ); + $this->assertArrayHasKey( 'product_ids', $properties ); + $this->assertArrayHasKey( 'excluded_product_ids', $properties ); + $this->assertArrayHasKey( 'usage_limit', $properties ); + $this->assertArrayHasKey( 'usage_limit_per_user', $properties ); + $this->assertArrayHasKey( 'limit_usage_to_x_items', $properties ); + $this->assertArrayHasKey( 'free_shipping', $properties ); + $this->assertArrayHasKey( 'product_categories', $properties ); + $this->assertArrayHasKey( 'excluded_product_categories', $properties ); + $this->assertArrayHasKey( 'exclude_sale_items', $properties ); + $this->assertArrayHasKey( 'minimum_amount', $properties ); + $this->assertArrayHasKey( 'maximum_amount', $properties ); + $this->assertArrayHasKey( 'email_restrictions', $properties ); + $this->assertArrayHasKey( 'used_by', $properties ); + } +} diff --git a/tests/legacy/unit-tests/rest-api/Tests/Version3/customers.php b/tests/legacy/unit-tests/rest-api/Tests/Version3/customers.php new file mode 100644 index 00000000000..8d4508bade4 --- /dev/null +++ b/tests/legacy/unit-tests/rest-api/Tests/Version3/customers.php @@ -0,0 +1,634 @@ +endpoint = new WC_REST_Customers_Controller(); + } + + /** + * Test route registration. + * + * @since 3.5.0 + */ + public function test_register_routes() { + $routes = $this->server->get_routes(); + + $this->assertArrayHasKey( '/wc/v3/customers', $routes ); + $this->assertArrayHasKey( '/wc/v3/customers/(?P[\d]+)', $routes ); + $this->assertArrayHasKey( '/wc/v3/customers/batch', $routes ); + } + + /** + * Test getting customers. + * + * @since 3.5.0 + */ + public function test_get_customers() { + wp_set_current_user( 1 ); + + $customer_1 = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\CustomerHelper::create_customer(); + \Automattic\WooCommerce\RestApi\UnitTests\Helpers\CustomerHelper::create_customer( 'test2', 'test2', 'test2@woo.local' ); + + $request = new WP_REST_Request( 'GET', '/wc/v3/customers' ); + $request->set_query_params( + array( + 'orderby' => 'id', + ) + ); + $response = $this->server->dispatch( $request ); + $customers = $response->get_data(); + $date_created = get_date_from_gmt( date( 'Y-m-d H:i:s', strtotime( $customer_1->get_date_created() ) ) ); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 2, count( $customers ) ); + + $this->assertContains( + array( + 'id' => $customer_1->get_id(), + 'date_created' => wc_rest_prepare_date_response( $date_created, false ), + 'date_created_gmt' => wc_rest_prepare_date_response( $date_created ), + 'date_modified' => wc_rest_prepare_date_response( $customer_1->get_date_modified(), false ), + 'date_modified_gmt' => wc_rest_prepare_date_response( $customer_1->get_date_modified() ), + 'email' => 'test@woo.local', + 'first_name' => 'Justin', + 'last_name' => '', + 'role' => 'customer', + 'username' => 'testcustomer', + 'billing' => array( + 'first_name' => '', + 'last_name' => '', + 'company' => '', + 'address_1' => '123 South Street', + 'address_2' => 'Apt 1', + 'city' => 'Philadelphia', + 'state' => 'PA', + 'postcode' => '19123', + 'country' => 'US', + 'email' => '', + 'phone' => '', + ), + 'shipping' => array( + 'first_name' => '', + 'last_name' => '', + 'company' => '', + 'address_1' => '123 South Street', + 'address_2' => 'Apt 1', + 'city' => 'Philadelphia', + 'state' => 'PA', + 'postcode' => '19123', + 'country' => 'US', + ), + 'is_paying_customer' => false, + 'avatar_url' => $customer_1->get_avatar_url(), + 'meta_data' => array(), + '_links' => array( + 'self' => array( + array( + 'href' => rest_url( '/wc/v3/customers/' . $customer_1->get_id() . '' ), + ), + ), + 'collection' => array( + array( + 'href' => rest_url( '/wc/v3/customers' ), + ), + ), + ), + ), + $customers + ); + + update_option( 'timezone_tring', 'America/New York' ); + $customer_3 = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\CustomerHelper::create_customer( 'timezonetest', 'timezonetest', 'timezonetest@woo.local' ); + + $request = new WP_REST_Request( 'GET', '/wc/v3/customers' ); + $request->set_query_params( + array( + 'orderby' => 'id', + ) + ); + $response = $this->server->dispatch( $request ); + $customers = $response->get_data(); + $date_created = get_date_from_gmt( date( 'Y-m-d H:i:s', strtotime( $customer_3->get_date_created() ) ) ); + + $this->assertEquals( 200, $response->get_status() ); + + $this->assertContains( + array( + 'id' => $customer_3->get_id(), + 'date_created' => wc_rest_prepare_date_response( $date_created, false ), + 'date_created_gmt' => wc_rest_prepare_date_response( $date_created ), + 'date_modified' => wc_rest_prepare_date_response( $customer_3->get_date_modified(), false ), + 'date_modified_gmt' => wc_rest_prepare_date_response( $customer_3->get_date_modified() ), + 'email' => 'timezonetest@woo.local', + 'first_name' => 'Justin', + 'last_name' => '', + 'role' => 'customer', + 'username' => 'timezonetest', + 'billing' => array( + 'first_name' => '', + 'last_name' => '', + 'company' => '', + 'address_1' => '123 South Street', + 'address_2' => 'Apt 1', + 'city' => 'Philadelphia', + 'state' => 'PA', + 'postcode' => '19123', + 'country' => 'US', + 'email' => '', + 'phone' => '', + ), + 'shipping' => array( + 'first_name' => '', + 'last_name' => '', + 'company' => '', + 'address_1' => '123 South Street', + 'address_2' => 'Apt 1', + 'city' => 'Philadelphia', + 'state' => 'PA', + 'postcode' => '19123', + 'country' => 'US', + ), + 'is_paying_customer' => false, + 'avatar_url' => $customer_3->get_avatar_url(), + 'meta_data' => array(), + '_links' => array( + 'self' => array( + array( + 'href' => rest_url( '/wc/v3/customers/' . $customer_3->get_id() . '' ), + ), + ), + 'collection' => array( + array( + 'href' => rest_url( '/wc/v3/customers' ), + ), + ), + ), + ), + $customers + ); + + } + + /** + * Test getting customers without valid permissions. + * + * @since 3.5.0 + */ + public function test_get_customers_without_permission() { + wp_set_current_user( 0 ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/customers' ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test creating a new customer. + * + * @since 3.5.0 + */ + public function test_create_customer() { + wp_set_current_user( 1 ); + + // Test just the basics first.. + $request = new WP_REST_Request( 'POST', '/wc/v3/customers' ); + $request->set_body_params( + array( + 'username' => 'create_customer_test', + 'password' => 'test123', + 'email' => 'create_customer_test@woo.local', + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 201, $response->get_status() ); + $this->assertEquals( + array( + 'id' => $data['id'], + 'date_created' => $data['date_created'], + 'date_created_gmt' => $data['date_created_gmt'], + 'date_modified' => $data['date_modified'], + 'date_modified_gmt' => $data['date_modified_gmt'], + 'email' => 'create_customer_test@woo.local', + 'first_name' => '', + 'last_name' => '', + 'role' => 'customer', + 'username' => 'create_customer_test', + 'billing' => array( + 'first_name' => '', + 'last_name' => '', + 'company' => '', + 'address_1' => '', + 'address_2' => '', + 'city' => '', + 'state' => '', + 'postcode' => '', + 'country' => '', + 'email' => '', + 'phone' => '', + ), + 'shipping' => array( + 'first_name' => '', + 'last_name' => '', + 'company' => '', + 'address_1' => '', + 'address_2' => '', + 'city' => '', + 'state' => '', + 'postcode' => '', + 'country' => '', + ), + 'is_paying_customer' => false, + 'meta_data' => array(), + 'avatar_url' => $data['avatar_url'], + ), + $data + ); + + // Test extra data. + $request = new WP_REST_Request( 'POST', '/wc/v3/customers' ); + $request->set_body_params( + array( + 'username' => 'create_customer_test2', + 'password' => 'test123', + 'email' => 'create_customer_test2@woo.local', + 'first_name' => 'Test', + 'last_name' => 'McTestFace', + 'billing' => array( + 'country' => 'US', + 'state' => 'WA', + ), + 'shipping' => array( + 'state' => 'CA', + 'country' => 'US', + ), + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 201, $response->get_status() ); + $this->assertEquals( + array( + 'id' => $data['id'], + 'date_created' => $data['date_created'], + 'date_created_gmt' => $data['date_created_gmt'], + 'date_modified' => $data['date_modified'], + 'date_modified_gmt' => $data['date_modified_gmt'], + 'email' => 'create_customer_test2@woo.local', + 'first_name' => 'Test', + 'last_name' => 'McTestFace', + 'role' => 'customer', + 'username' => 'create_customer_test2', + 'billing' => array( + 'first_name' => '', + 'last_name' => '', + 'company' => '', + 'address_1' => '', + 'address_2' => '', + 'city' => '', + 'state' => 'WA', + 'postcode' => '', + 'country' => 'US', + 'email' => '', + 'phone' => '', + ), + 'shipping' => array( + 'first_name' => '', + 'last_name' => '', + 'company' => '', + 'address_1' => '', + 'address_2' => '', + 'city' => '', + 'state' => 'CA', + 'postcode' => '', + 'country' => 'US', + ), + 'is_paying_customer' => false, + 'meta_data' => array(), + 'avatar_url' => $data['avatar_url'], + ), + $data + ); + + // Test without required field. + $request = new WP_REST_Request( 'POST', '/wc/v3/customers' ); + $request->set_body_params( + array( + 'username' => 'create_customer_test3', + 'first_name' => 'Test', + 'last_name' => 'McTestFace', + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 400, $response->get_status() ); + } + + /** + * Test creating customers without valid permissions. + * + * @since 3.5.0 + */ + public function test_create_customer_without_permission() { + wp_set_current_user( 0 ); + $request = new WP_REST_Request( 'POST', '/wc/v3/customers' ); + $request->set_body_params( + array( + 'username' => 'create_customer_test_without_permission', + 'password' => 'test123', + 'email' => 'create_customer_test_without_permission@woo.local', + ) + ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test getting a single customer. + * + * @since 3.5.0 + */ + public function test_get_customer() { + wp_set_current_user( 1 ); + $customer = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\CustomerHelper::create_customer( 'get_customer_test', 'test123', 'get_customer_test@woo.local' ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/customers/' . $customer->get_id() ) ); + $data = $response->get_data(); + + $this->assertEquals( + array( + 'id' => $data['id'], + 'date_created' => $data['date_created'], + 'date_created_gmt' => $data['date_created_gmt'], + 'date_modified' => $data['date_modified'], + 'date_modified_gmt' => $data['date_modified_gmt'], + 'email' => 'get_customer_test@woo.local', + 'first_name' => 'Justin', + 'billing' => array( + 'first_name' => '', + 'last_name' => '', + 'company' => '', + 'address_1' => '123 South Street', + 'address_2' => 'Apt 1', + 'city' => 'Philadelphia', + 'state' => 'PA', + 'postcode' => '19123', + 'country' => 'US', + 'email' => '', + 'phone' => '', + ), + 'shipping' => array( + 'first_name' => '', + 'last_name' => '', + 'company' => '', + 'address_1' => '123 South Street', + 'address_2' => 'Apt 1', + 'city' => 'Philadelphia', + 'state' => 'PA', + 'postcode' => '19123', + 'country' => 'US', + ), + 'is_paying_customer' => false, + 'meta_data' => array(), + 'last_name' => '', + 'role' => 'customer', + 'username' => 'get_customer_test', + 'avatar_url' => $data['avatar_url'], + ), + $data + ); + } + + /** + * Test getting a single customer without valid permissions. + * + * @since 3.5.0 + */ + public function test_get_customer_without_permission() { + wp_set_current_user( 0 ); + $customer = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\CustomerHelper::create_customer( 'get_customer_test_without_permission', 'test123', 'get_customer_test_without_permission@woo.local' ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/customers/' . $customer->get_id() ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test getting a single customer with an invalid ID. + * + * @since 3.5.0 + */ + public function test_get_customer_invalid_id() { + wp_set_current_user( 1 ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/customers/0' ) ); + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * Test updating a customer. + * + * @since 3.5.0 + */ + public function test_update_customer() { + wp_set_current_user( 1 ); + $customer = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\CustomerHelper::create_customer( 'update_customer_test', 'test123', 'update_customer_test@woo.local' ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/customers/' . $customer->get_id() ) ); + $data = $response->get_data(); + $this->assertEquals( 'update_customer_test', $data['username'] ); + $this->assertEquals( 'update_customer_test@woo.local', $data['email'] ); + + $request = new WP_REST_Request( 'PUT', '/wc/v3/customers/' . $customer->get_id() ); + $request->set_body_params( + array( + 'email' => 'updated_email@woo.local', + 'first_name' => 'UpdatedTest', + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 'updated_email@woo.local', $data['email'] ); + $this->assertEquals( 'UpdatedTest', $data['first_name'] ); + } + + /** + * Test updating a customer without valid permissions. + * + * @since 3.5.0 + */ + public function test_update_customer_without_permission() { + wp_set_current_user( 0 ); + $customer = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\CustomerHelper::create_customer( 'update_customer_test_without_permission', 'test123', 'update_customer_test_without_permission@woo.local' ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/customers/' . $customer->get_id() ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test updating a customer with an invalid ID. + * + * @since 3.5.0 + */ + public function test_update_customer_invalid_id() { + wp_set_current_user( 1 ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/customers/0' ) ); + $this->assertEquals( 404, $response->get_status() ); + } + + + /** + * Test deleting a customer. + * + * @since 3.5.0 + */ + public function test_delete_customer() { + wp_set_current_user( 1 ); + $customer = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\CustomerHelper::create_customer( 'delete_customer_test', 'test123', 'delete_customer_test@woo.local' ); + $request = new WP_REST_Request( 'DELETE', '/wc/v3/customers/' . $customer->get_id() ); + $request->set_param( 'force', true ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 200, $response->get_status() ); + } + + /** + * Test deleting a customer with an invalid ID. + * + * @since 3.5.0 + */ + public function test_delete_customer_invalid_id() { + wp_set_current_user( 1 ); + $request = new WP_REST_Request( 'DELETE', '/wc/v3/customers/0' ); + $request->set_param( 'force', true ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 400, $response->get_status() ); + } + + /** + * Test deleting a customer without valid permissions. + * + * @since 3.5.0 + */ + public function test_delete_customer_without_permission() { + wp_set_current_user( 0 ); + $customer = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\CustomerHelper::create_customer( 'delete_customer_test_without_permission', 'test123', 'delete_customer_test_without_permission@woo.local' ); + $request = new WP_REST_Request( 'DELETE', '/wc/v3/customers/' . $customer->get_id() ); + $request->set_param( 'force', true ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test customer batch endpoint. + * + * @since 3.5.0 + */ + public function test_batch_customer() { + wp_set_current_user( 1 ); + + $customer_1 = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\CustomerHelper::create_customer( 'test_batch_customer', 'test123', 'test_batch_customer@woo.local' ); + $customer_2 = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\CustomerHelper::create_customer( 'test_batch_customer2', 'test123', 'test_batch_customer2@woo.local' ); + $customer_3 = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\CustomerHelper::create_customer( 'test_batch_customer3', 'test123', 'test_batch_customer3@woo.local' ); + $customer_4 = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\CustomerHelper::create_customer( 'test_batch_customer4', 'test123', 'test_batch_customer4@woo.local' ); + + $request = new WP_REST_Request( 'POST', '/wc/v3/customers/batch' ); + $request->set_body_params( + array( + 'update' => array( + array( + 'id' => $customer_1->get_id(), + 'last_name' => 'McTest', + ), + ), + 'delete' => array( + $customer_2->get_id(), + $customer_3->get_id(), + ), + 'create' => array( + array( + 'username' => 'newuser', + 'password' => 'test123', + 'email' => 'newuser@woo.local', + ), + ), + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 'McTest', $data['update'][0]['last_name'] ); + $this->assertEquals( 'newuser', $data['create'][0]['username'] ); + $this->assertEmpty( $data['create'][0]['last_name'] ); + $this->assertEquals( $customer_2->get_id(), $data['delete'][0]['id'] ); + $this->assertEquals( $customer_3->get_id(), $data['delete'][1]['id'] ); + + $request = new WP_REST_Request( 'GET', '/wc/v3/customers' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 3, count( $data ) ); + } + + /** + * Test customer schema. + * + * @since 3.5.0 + */ + public function test_customer_schema() { + wp_set_current_user( 1 ); + $request = new WP_REST_Request( 'OPTIONS', '/wc/v3/customers' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $properties = $data['schema']['properties']; + + $this->assertEquals( 16, count( $properties ) ); + $this->assertArrayHasKey( 'id', $properties ); + $this->assertArrayHasKey( 'date_created', $properties ); + $this->assertArrayHasKey( 'date_created_gmt', $properties ); + $this->assertArrayHasKey( 'date_modified', $properties ); + $this->assertArrayHasKey( 'date_modified_gmt', $properties ); + $this->assertArrayHasKey( 'email', $properties ); + $this->assertArrayHasKey( 'first_name', $properties ); + $this->assertArrayHasKey( 'last_name', $properties ); + $this->assertArrayHasKey( 'role', $properties ); + $this->assertArrayHasKey( 'username', $properties ); + $this->assertArrayHasKey( 'password', $properties ); + $this->assertArrayHasKey( 'avatar_url', $properties ); + $this->assertArrayHasKey( 'billing', $properties ); + $this->assertArrayHasKey( 'first_name', $properties['billing']['properties'] ); + $this->assertArrayHasKey( 'last_name', $properties['billing']['properties'] ); + $this->assertArrayHasKey( 'company', $properties['billing']['properties'] ); + $this->assertArrayHasKey( 'address_1', $properties['billing']['properties'] ); + $this->assertArrayHasKey( 'address_2', $properties['billing']['properties'] ); + $this->assertArrayHasKey( 'city', $properties['billing']['properties'] ); + $this->assertArrayHasKey( 'state', $properties['billing']['properties'] ); + $this->assertArrayHasKey( 'postcode', $properties['billing']['properties'] ); + $this->assertArrayHasKey( 'country', $properties['billing']['properties'] ); + $this->assertArrayHasKey( 'email', $properties['billing']['properties'] ); + $this->assertArrayHasKey( 'phone', $properties['billing']['properties'] ); + $this->assertArrayHasKey( 'shipping', $properties ); + $this->assertArrayHasKey( 'first_name', $properties['shipping']['properties'] ); + $this->assertArrayHasKey( 'last_name', $properties['shipping']['properties'] ); + $this->assertArrayHasKey( 'company', $properties['shipping']['properties'] ); + $this->assertArrayHasKey( 'address_1', $properties['shipping']['properties'] ); + $this->assertArrayHasKey( 'address_2', $properties['shipping']['properties'] ); + $this->assertArrayHasKey( 'city', $properties['shipping']['properties'] ); + $this->assertArrayHasKey( 'state', $properties['shipping']['properties'] ); + $this->assertArrayHasKey( 'postcode', $properties['shipping']['properties'] ); + $this->assertArrayHasKey( 'country', $properties['shipping']['properties'] ); + } +} diff --git a/tests/legacy/unit-tests/rest-api/Tests/Version3/orders.php b/tests/legacy/unit-tests/rest-api/Tests/Version3/orders.php new file mode 100644 index 00000000000..6ebb7477ae0 --- /dev/null +++ b/tests/legacy/unit-tests/rest-api/Tests/Version3/orders.php @@ -0,0 +1,778 @@ +endpoint = new WC_REST_Orders_Controller(); + $this->user = $this->factory->user->create( + array( + 'role' => 'administrator', + ) + ); + } + + /** + * Test route registration. + * @since 3.5.0 + */ + public function test_register_routes() { + $routes = $this->server->get_routes(); + $this->assertArrayHasKey( '/wc/v3/orders', $routes ); + $this->assertArrayHasKey( '/wc/v3/orders/batch', $routes ); + $this->assertArrayHasKey( '/wc/v3/orders/(?P[\d]+)', $routes ); + } + + /** + * Test getting all orders. + * @since 3.5.0 + */ + public function test_get_items() { + wp_set_current_user( $this->user ); + + // Create 10 orders. + for ( $i = 0; $i < 10; $i++ ) { + $this->orders[] = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order( $this->user ); + } + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/orders' ) ); + $orders = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 10, count( $orders ) ); + } + + /** + * Test getting all orders sorted by modified date. + */ + public function test_get_items_ordered_by_modified() { + wp_set_current_user( $this->user ); + + $order1 = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order( $this->user ); + $order2 = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order( $this->user ); + + $order1->set_status( 'completed' ); + $order1->save(); + sleep( 1 ); + $order2->set_status( 'completed' ); + $order2->save(); + + $request = new WP_REST_Request( 'GET', '/wc/v3/orders' ); + $request->set_query_params( array( 'orderby' => 'modified', 'order' => 'asc' ) ); + $response = $this->server->dispatch( $request ); + $orders = $response->get_data(); + $this->assertEquals( $order1->get_id(), $orders[0]['id'] ); + + $request->set_query_params( array( 'orderby' => 'modified', 'order' => 'desc' ) ); + $response = $this->server->dispatch( $request ); + $orders = $response->get_data(); + $this->assertEquals( $order2->get_id(), $orders[0]['id'] ); + } + + /** + * Tests to make sure orders cannot be viewed without valid permissions. + * + * @since 3.5.0 + */ + public function test_get_items_without_permission() { + wp_set_current_user( 0 ); + $this->orders[] = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order(); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/orders' ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Tests getting a single order. + * @since 3.5.0 + */ + public function test_get_item() { + wp_set_current_user( $this->user ); + $order = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order(); + $order->add_meta_data( 'key', 'value' ); + $order->add_meta_data( 'key2', 'value2' ); + $order->save(); + $this->orders[] = $order; + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/orders/' . $order->get_id() ) ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( $order->get_id(), $data['id'] ); + + // Test meta data is set. + $this->assertEquals( 'key', $data['meta_data'][0]->key ); + $this->assertEquals( 'value', $data['meta_data'][0]->value ); + $this->assertEquals( 'key2', $data['meta_data'][1]->key ); + $this->assertEquals( 'value2', $data['meta_data'][1]->value ); + } + + /** + * Tests getting a single order without the correct permissions. + * @since 3.5.0 + */ + public function test_get_item_without_permission() { + wp_set_current_user( 0 ); + $order = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order(); + $this->orders[] = $order; + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/orders/' . $order->get_id() ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Tests getting an order with an invalid ID. + * @since 3.5.0 + */ + public function test_get_item_invalid_id() { + wp_set_current_user( $this->user ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/orders/99999999' ) ); + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * Tests getting an order with an invalid ID. + * @since 3.5.0 + */ + public function test_get_item_refund_id() { + wp_set_current_user( $this->user ); + $order = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order(); + $refund = wc_create_refund( + array( + 'order_id' => $order->get_id(), + ) + ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/orders/' . $refund->get_id() ) ); + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * Tests creating an order. + * @since 3.5.0 + */ + public function test_create_order() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $request = new WP_REST_Request( 'POST', '/wc/v3/orders' ); + $request->set_body_params( + array( + 'payment_method' => 'bacs', + 'payment_method_title' => 'Direct Bank Transfer', + 'set_paid' => true, + 'billing' => array( + 'first_name' => 'John', + 'last_name' => 'Doe', + 'address_1' => '969 Market', + 'address_2' => '', + 'city' => 'San Francisco', + 'state' => 'CA', + 'postcode' => '94103', + 'country' => 'US', + 'email' => 'john.doe@example.com', + 'phone' => '(555) 555-5555', + ), + 'shipping' => array( + 'first_name' => 'John', + 'last_name' => 'Doe', + 'address_1' => '969 Market', + 'address_2' => '', + 'city' => 'San Francisco', + 'state' => 'CA', + 'postcode' => '94103', + 'country' => 'US', + ), + 'line_items' => array( + array( + 'product_id' => $product->get_id(), + 'quantity' => 2, + ), + ), + 'shipping_lines' => array( + array( + 'method_id' => 'flat_rate', + 'method_title' => 'Flat rate', + 'total' => '10.00', + 'instance_id' => '1', + ), + ), + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $order = wc_get_order( $data['id'] ); + $this->assertEquals( 201, $response->get_status() ); + $this->assertEquals( $order->get_payment_method(), $data['payment_method'] ); + $this->assertEquals( $order->get_payment_method_title(), $data['payment_method_title'] ); + $this->assertEquals( $order->get_billing_first_name(), $data['billing']['first_name'] ); + $this->assertEquals( $order->get_billing_last_name(), $data['billing']['last_name'] ); + $this->assertEquals( '', $data['billing']['company'] ); + $this->assertEquals( $order->get_billing_address_1(), $data['billing']['address_1'] ); + $this->assertEquals( $order->get_billing_address_2(), $data['billing']['address_2'] ); + $this->assertEquals( $order->get_billing_city(), $data['billing']['city'] ); + $this->assertEquals( $order->get_billing_state(), $data['billing']['state'] ); + $this->assertEquals( $order->get_billing_postcode(), $data['billing']['postcode'] ); + $this->assertEquals( $order->get_billing_country(), $data['billing']['country'] ); + $this->assertEquals( $order->get_billing_email(), $data['billing']['email'] ); + $this->assertEquals( $order->get_billing_phone(), $data['billing']['phone'] ); + $this->assertEquals( $order->get_shipping_first_name(), $data['shipping']['first_name'] ); + $this->assertEquals( $order->get_shipping_last_name(), $data['shipping']['last_name'] ); + $this->assertEquals( '', $data['shipping']['company'] ); + $this->assertEquals( $order->get_shipping_address_1(), $data['shipping']['address_1'] ); + $this->assertEquals( $order->get_shipping_address_2(), $data['shipping']['address_2'] ); + $this->assertEquals( $order->get_shipping_city(), $data['shipping']['city'] ); + $this->assertEquals( $order->get_shipping_state(), $data['shipping']['state'] ); + $this->assertEquals( $order->get_shipping_postcode(), $data['shipping']['postcode'] ); + $this->assertEquals( $order->get_shipping_country(), $data['shipping']['country'] ); + $this->assertEquals( 1, count( $data['line_items'] ) ); + $this->assertEquals( 1, count( $data['shipping_lines'] ) ); + $shipping = current( $order->get_items( 'shipping' ) ); + $expected = array( + 'id' => $shipping->get_id(), + 'method_title' => $shipping->get_method_title(), + 'method_id' => $shipping->get_method_id(), + 'instance_id' => $shipping->get_instance_id(), + 'total' => wc_format_decimal( $shipping->get_total(), '' ), + 'total_tax' => wc_format_decimal( $shipping->get_total_tax(), '' ), + 'taxes' => array(), + 'meta_data' => $shipping->get_meta_data(), + ); + $this->assertEquals( $expected, $data['shipping_lines'][0] ); + } + + /** + * Test the sanitization of the payment_method_title field through the API. + * + * @since 3.5.2 + */ + public function test_create_update_order_payment_method_title_sanitize() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + + // Test when creating order. + $request = new WP_REST_Request( 'POST', '/wc/v3/orders' ); + $request->set_body_params( + array( + 'payment_method' => 'bacs', + 'payment_method_title' => '

Sanitize this

', + 'set_paid' => true, + 'billing' => array( + 'first_name' => 'John', + 'last_name' => 'Doe', + 'address_1' => '969 Market', + 'address_2' => '', + 'city' => 'San Francisco', + 'state' => 'CA', + 'postcode' => '94103', + 'country' => 'US', + 'email' => 'john.doe@example.com', + 'phone' => '(555) 555-5555', + ), + 'shipping' => array( + 'first_name' => 'John', + 'last_name' => 'Doe', + 'address_1' => '969 Market', + 'address_2' => '', + 'city' => 'San Francisco', + 'state' => 'CA', + 'postcode' => '94103', + 'country' => 'US', + ), + 'line_items' => array( + array( + 'product_id' => $product->get_id(), + 'quantity' => 2, + ), + ), + 'shipping_lines' => array( + array( + 'method_id' => 'flat_rate', + 'method_title' => 'Flat rate', + 'total' => '10', + ), + ), + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $order = wc_get_order( $data['id'] ); + $this->assertEquals( 201, $response->get_status() ); + $this->assertEquals( $order->get_payment_method(), $data['payment_method'] ); + $this->assertEquals( $order->get_payment_method_title(), 'Sanitize this' ); + + // Test when updating order. + $request = new WP_REST_Request( 'PUT', '/wc/v3/orders/' . $data['id'] ); + $request->set_body_params( + array( + 'payment_method' => 'bacs', + 'payment_method_title' => '

Sanitize this too

', + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $order = wc_get_order( $data['id'] ); + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( $order->get_payment_method(), $data['payment_method'] ); + $this->assertEquals( $order->get_payment_method_title(), 'Sanitize this too' ); + } + + /** + * Tests creating an order without required fields. + * @since 3.5.0 + */ + public function test_create_order_invalid_fields() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + + // Non-existent customer. + $request = new WP_REST_Request( 'POST', '/wc/v3/orders' ); + $request->set_body_params( + array( + 'payment_method' => 'bacs', + 'payment_method_title' => 'Direct Bank Transfer', + 'set_paid' => true, + 'customer_id' => 99999, + 'billing' => array( + 'first_name' => 'John', + 'last_name' => 'Doe', + 'address_1' => '969 Market', + 'address_2' => '', + 'city' => 'San Francisco', + 'state' => 'CA', + 'postcode' => '94103', + 'country' => 'US', + 'email' => 'john.doe@example.com', + 'phone' => '(555) 555-5555', + ), + 'shipping' => array( + 'first_name' => 'John', + 'last_name' => 'Doe', + 'address_1' => '969 Market', + 'address_2' => '', + 'city' => 'San Francisco', + 'state' => 'CA', + 'postcode' => '94103', + 'country' => 'US', + ), + 'line_items' => array( + array( + 'product_id' => $product->get_id(), + 'quantity' => 2, + ), + ), + 'shipping_lines' => array( + array( + 'method_id' => 'flat_rate', + 'method_title' => 'Flat rate', + 'total' => 10, + ), + ), + ) + ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 400, $response->get_status() ); + } + + /** + * Tests create an order with an invalid product. + * + * @since 3.9.0 + */ + public function test_create_order_with_invalid_product() { + wp_set_current_user( $this->user ); + + $request = new WP_REST_Request( 'POST', '/wc/v3/orders' ); + $request->set_body_params( + array( + 'line_items' => array( + array( + 'quantity' => 2, + ), + ), + ) + ); + + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $this->assertEquals( 'woocommerce_rest_required_product_reference', $data['code'] ); + $this->assertEquals( 400, $response->get_status() ); + } + + /** + * Tests updating an order. + * + * @since 3.5.0 + */ + public function test_update_order() { + wp_set_current_user( $this->user ); + $order = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order(); + $request = new WP_REST_Request( 'PUT', '/wc/v3/orders/' . $order->get_id() ); + $request->set_body_params( + array( + 'payment_method' => 'test-update', + 'billing' => array( + 'first_name' => 'Fish', + 'last_name' => 'Face', + ), + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 'test-update', $data['payment_method'] ); + $this->assertEquals( 'Fish', $data['billing']['first_name'] ); + $this->assertEquals( 'Face', $data['billing']['last_name'] ); + } + + /** + * Tests updating an order and removing items. + * + * @since 3.5.0 + */ + public function test_update_order_remove_items() { + wp_set_current_user( $this->user ); + $order = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order(); + $fee = new WC_Order_Item_Fee(); + $fee->set_props( + array( + 'name' => 'Some Fee', + 'tax_status' => 'taxable', + 'total' => '100', + 'tax_class' => '', + ) + ); + $order->add_item( $fee ); + $order->save(); + + $request = new WP_REST_Request( 'PUT', '/wc/v3/orders/' . $order->get_id() ); + $fee_data = current( $order->get_items( 'fee' ) ); + + $request->set_body_params( + array( + 'fee_lines' => array( + array( + 'id' => $fee_data->get_id(), + 'name' => null, + ), + ), + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertTrue( empty( $data['fee_lines'] ) ); + } + + /** + * Tests updating an order after deleting a product. + * + * @since 3.9.0 + */ + public function test_update_order_after_delete_product() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $order = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order( 1, $product ); + $product->delete( true ); + + $request = new WP_REST_Request( 'PUT', '/wc/v3/orders/' . $order->get_id() ); + $line_items = $order->get_items( 'line_item' ); + $item = current( $line_items ); + + $request->set_body_params( + array( + 'line_items' => array( + array( + 'id' => $item->get_id(), + 'quantity' => 10, + ), + ), + ) + ); + + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $expected = array( + 'id' => $item->get_id(), + 'name' => 'Dummy Product', + 'product_id' => 0, + 'variation_id' => 0, + 'quantity' => 10, + 'tax_class' => '', + 'subtotal' => '40.00', + 'subtotal_tax' => '0.00', + 'total' => '40.00', + 'total_tax' => '0.00', + 'taxes' => array(), + 'meta_data' => array(), + 'sku' => null, + 'price' => 4, + ); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( $expected, $data['line_items'][0] ); + } + + /** + * Tests updating an order and adding a coupon. + * + * @since 3.5.0 + */ + public function test_update_order_add_coupons() { + wp_set_current_user( $this->user ); + + $order = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order(); + $order_item = current( $order->get_items() ); + $coupon = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\CouponHelper::create_coupon( 'fake-coupon' ); + $coupon->set_amount( 5 ); + $coupon->save(); + + $request = new WP_REST_Request( 'PUT', '/wc/v3/orders/' . $order->get_id() ); + $request->set_body_params( + array( + 'coupon_lines' => array( + array( + 'code' => 'fake-coupon', + ), + ), + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertCount( 1, $data['coupon_lines'] ); + $this->assertEquals( '45.00', $data['total'] ); + } + + /** + * Tests updating an order and removing a coupon. + * + * @since 3.5.0 + */ + public function test_update_order_remove_coupons() { + wp_set_current_user( $this->user ); + $order = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order(); + $order_item = current( $order->get_items() ); + $coupon = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\CouponHelper::create_coupon( 'fake-coupon' ); + $coupon->set_amount( 5 ); + $coupon->save(); + + $order->apply_coupon( $coupon ); + $order->save(); + + // Check that the coupon is applied. + $this->assertEquals( '45.00', $order->get_total() ); + + $request = new WP_REST_Request( 'PUT', '/wc/v3/orders/' . $order->get_id() ); + $coupon_data = current( $order->get_items( 'coupon' ) ); + + $request->set_body_params( + array( + 'coupon_lines' => array( + array( + 'id' => $coupon_data->get_id(), + 'code' => null, + ), + ), + 'line_items' => array( + array( + 'id' => $order_item->get_id(), + 'product_id' => $order_item->get_product_id(), + 'total' => '40.00', + ), + ), + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertTrue( empty( $data['coupon_lines'] ) ); + $this->assertEquals( '50.00', $data['total'] ); + } + + /** + * Tests updating an order with an invalid coupon. + * + * @since 3.5.0 + */ + public function test_invalid_coupon() { + wp_set_current_user( $this->user ); + $order = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order(); + $request = new WP_REST_Request( 'PUT', '/wc/v3/orders/' . $order->get_id() ); + + $request->set_body_params( + array( + 'coupon_lines' => array( + array( + 'code' => 'NON_EXISTING_COUPON', + ), + ), + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 400, $response->get_status() ); + $this->assertEquals( 'woocommerce_rest_invalid_coupon', $data['code'] ); + $this->assertEquals( 'Coupon "non_existing_coupon" does not exist!', $data['message'] ); + } + + /** + * Tests updating an order without the correct permissions. + * + * @since 3.5.0 + */ + public function test_update_order_without_permission() { + wp_set_current_user( 0 ); + $order = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order(); + $request = new WP_REST_Request( 'PUT', '/wc/v3/orders/' . $order->get_id() ); + $request->set_body_params( + array( + 'payment_method' => 'test-update', + 'billing' => array( + 'first_name' => 'Fish', + 'last_name' => 'Face', + ), + ) + ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Tests that updating an order with an invalid id fails. + * + * @since 3.5.0 + */ + public function test_update_order_invalid_id() { + wp_set_current_user( $this->user ); + $request = new WP_REST_Request( 'POST', '/wc/v3/orders/999999' ); + $request->set_body_params( + array( + 'payment_method' => 'test-update', + 'billing' => array( + 'first_name' => 'Fish', + 'last_name' => 'Face', + ), + ) + ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 400, $response->get_status() ); + } + + /** + * Test deleting an order. + * + * @since 3.5.0 + */ + public function test_delete_order() { + wp_set_current_user( $this->user ); + $order = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order(); + $request = new WP_REST_Request( 'DELETE', '/wc/v3/orders/' . $order->get_id() ); + $request->set_param( 'force', true ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( null, get_post( $order->get_id() ) ); + } + + /** + * Test deleting an order without permission/creds. + * + * @since 3.5.0 + */ + public function test_delete_order_without_permission() { + wp_set_current_user( 0 ); + $order = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order(); + $request = new WP_REST_Request( 'DELETE', '/wc/v3/orders/' . $order->get_id() ); + $request->set_param( 'force', true ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test deleting an order with an invalid id. + * + * @since 3.5.0 + */ + public function test_delete_order_invalid_id() { + wp_set_current_user( $this->user ); + $request = new WP_REST_Request( 'DELETE', '/wc/v3/orders/9999999' ); + $request->set_param( 'force', true ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * Test batch managing product reviews. + * + * @since 3.5.0 + */ + public function test_orders_batch() { + wp_set_current_user( $this->user ); + + $order1 = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order(); + $order2 = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order(); + $order3 = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order(); + + $request = new WP_REST_Request( 'POST', '/wc/v3/orders/batch' ); + $request->set_body_params( + array( + 'update' => array( + array( + 'id' => $order1->get_id(), + 'payment_method' => 'updated', + ), + ), + 'delete' => array( + $order2->get_id(), + $order3->get_id(), + ), + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 'updated', $data['update'][0]['payment_method'] ); + $this->assertEquals( $order2->get_id(), $data['delete'][0]['id'] ); + $this->assertEquals( $order3->get_id(), $data['delete'][1]['id'] ); + + $request = new WP_REST_Request( 'GET', '/wc/v3/orders' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $this->assertEquals( 1, count( $data ) ); + } + + /** + * Test the order schema. + * + * @since 3.5.0 + */ + public function test_order_schema() { + wp_set_current_user( $this->user ); + $order = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper::create_order(); + $request = new WP_REST_Request( 'OPTIONS', '/wc/v3/orders/' . $order->get_id() ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $properties = $data['schema']['properties']; + + $this->assertEquals( 42, count( $properties ) ); + $this->assertArrayHasKey( 'id', $properties ); + } +} diff --git a/tests/legacy/unit-tests/rest-api/Tests/Version3/payment-gateways.php b/tests/legacy/unit-tests/rest-api/Tests/Version3/payment-gateways.php new file mode 100644 index 00000000000..f9a43b64e5f --- /dev/null +++ b/tests/legacy/unit-tests/rest-api/Tests/Version3/payment-gateways.php @@ -0,0 +1,345 @@ +endpoint = new WC_REST_Payment_Gateways_Controller(); + $this->user = $this->factory->user->create( + array( + 'role' => 'administrator', + ) + ); + } + + /** + * Test route registration. + * + * @since 3.5.0 + */ + public function test_register_routes() { + $routes = $this->server->get_routes(); + $this->assertArrayHasKey( '/wc/v3/payment_gateways', $routes ); + $this->assertArrayHasKey( '/wc/v3/payment_gateways/(?P[\w-]+)', $routes ); + } + + /** + * Test getting all payment gateways. + * + * @since 3.5.0 + */ + public function test_get_payment_gateways() { + wp_set_current_user( $this->user ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/payment_gateways' ) ); + $gateways = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertContains( + array( + 'id' => 'cheque', + 'title' => 'Check payments', + 'description' => 'Please send a check to Store Name, Store Street, Store Town, Store State / County, Store Postcode.', + 'order' => '', + 'enabled' => false, + 'method_title' => 'Check payments', + 'method_description' => 'Take payments in person via checks. This offline gateway can also be useful to test purchases.', + 'method_supports' => array( + 'products', + ), + 'settings' => array_diff_key( + $this->get_settings( 'WC_Gateway_Cheque' ), + array( + 'enabled' => false, + 'description' => false, + ) + ), + '_links' => array( + 'self' => array( + array( + 'href' => rest_url( '/wc/v3/payment_gateways/cheque' ), + ), + ), + 'collection' => array( + array( + 'href' => rest_url( '/wc/v3/payment_gateways' ), + ), + ), + ), + ), + $gateways + ); + } + + /** + * Tests to make sure payment gateways cannot viewed without valid permissions. + * + * @since 3.5.0 + */ + public function test_get_payment_gateways_without_permission() { + wp_set_current_user( 0 ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/payment_gateways' ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test getting a single payment gateway. + * + * @since 3.5.0 + */ + public function test_get_payment_gateway() { + wp_set_current_user( $this->user ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/payment_gateways/paypal' ) ); + $paypal = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( + array( + 'id' => 'paypal', + 'title' => 'PayPal', + 'description' => "Pay via PayPal; you can pay with your credit card if you don't have a PayPal account.", + 'order' => '', + 'enabled' => false, + 'method_title' => 'PayPal', + 'method_description' => 'PayPal Standard redirects customers to PayPal to enter their payment information.', + 'method_supports' => array( + 'products', + 'refunds', + ), + 'settings' => array_diff_key( + $this->get_settings( 'WC_Gateway_Paypal' ), + array( + 'enabled' => false, + 'description' => false, + ) + ), + ), + $paypal + ); + } + + /** + * Test getting a payment gateway without valid permissions. + * + * @since 3.5.0 + */ + public function test_get_payment_gateway_without_permission() { + wp_set_current_user( 0 ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/payment_gateways/paypal' ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test getting a payment gateway with an invalid id. + * + * @since 3.5.0 + */ + public function test_get_payment_gateway_invalid_id() { + wp_set_current_user( $this->user ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/payment_gateways/totally_fake_method' ) ); + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * Test updating a single payment gateway. + * + * @since 3.5.0 + */ + public function test_update_payment_gateway() { + wp_set_current_user( $this->user ); + + // Test defaults + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/payment_gateways/paypal' ) ); + $paypal = $response->get_data(); + + $this->assertEquals( 'PayPal', $paypal['settings']['title']['value'] ); + $this->assertEquals( 'admin@example.org', $paypal['settings']['email']['value'] ); + $this->assertEquals( 'no', $paypal['settings']['testmode']['value'] ); + + // test updating single setting + $request = new WP_REST_Request( 'POST', '/wc/v3/payment_gateways/paypal' ); + $request->set_body_params( + array( + 'settings' => array( + 'email' => 'woo@woo.local', + ), + ) + ); + $response = $this->server->dispatch( $request ); + $paypal = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 'PayPal', $paypal['settings']['title']['value'] ); + $this->assertEquals( 'woo@woo.local', $paypal['settings']['email']['value'] ); + $this->assertEquals( 'no', $paypal['settings']['testmode']['value'] ); + + // test updating multiple settings + $request = new WP_REST_Request( 'POST', '/wc/v3/payment_gateways/paypal' ); + $request->set_body_params( + array( + 'settings' => array( + 'testmode' => 'yes', + 'title' => 'PayPal - New Title', + ), + ) + ); + $response = $this->server->dispatch( $request ); + $paypal = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 'PayPal - New Title', $paypal['settings']['title']['value'] ); + $this->assertEquals( 'woo@woo.local', $paypal['settings']['email']['value'] ); + $this->assertEquals( 'yes', $paypal['settings']['testmode']['value'] ); + + // Test other parameters, and recheck settings + $request = new WP_REST_Request( 'POST', '/wc/v3/payment_gateways/paypal' ); + $request->set_body_params( + array( + 'enabled' => false, + 'order' => 2, + ) + ); + $response = $this->server->dispatch( $request ); + $paypal = $response->get_data(); + + $this->assertFalse( $paypal['enabled'] ); + $this->assertEquals( 2, $paypal['order'] ); + $this->assertEquals( 'PayPal - New Title', $paypal['settings']['title']['value'] ); + $this->assertEquals( 'woo@woo.local', $paypal['settings']['email']['value'] ); + $this->assertEquals( 'yes', $paypal['settings']['testmode']['value'] ); + + // test bogus + $request = new WP_REST_Request( 'POST', '/wc/v3/payment_gateways/paypal' ); + $request->set_body_params( + array( + 'settings' => array( + 'paymentaction' => 'afasfasf', + ), + ) + ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 400, $response->get_status() ); + + $request = new WP_REST_Request( 'POST', '/wc/v3/payment_gateways/paypal' ); + $request->set_body_params( + array( + 'settings' => array( + 'paymentaction' => 'authorization', + ), + ) + ); + $response = $this->server->dispatch( $request ); + $paypal = $response->get_data(); + $this->assertEquals( 'authorization', $paypal['settings']['paymentaction']['value'] ); + } + + /** + * Test updating a payment gateway without valid permissions. + * + * @since 3.5.0 + */ + public function test_update_payment_gateway_without_permission() { + wp_set_current_user( 0 ); + $request = new WP_REST_Request( 'POST', '/wc/v3/payment_gateways/paypal' ); + $request->set_body_params( + array( + 'settings' => array( + 'testmode' => 'yes', + 'title' => 'PayPal - New Title', + ), + ) + ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test updating a payment gateway with an invalid id. + * + * @since 3.5.0 + */ + public function test_update_payment_gateway_invalid_id() { + wp_set_current_user( $this->user ); + $request = new WP_REST_Request( 'POST', '/wc/v3/payment_gateways/totally_fake_method' ); + $request->set_body_params( + array( + 'enabled' => true, + ) + ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * Test the payment gateway schema. + * + * @since 3.5.0 + */ + public function test_payment_gateway_schema() { + wp_set_current_user( $this->user ); + + $request = new WP_REST_Request( 'OPTIONS', '/wc/v3/payment_gateways' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $properties = $data['schema']['properties']; + + $this->assertEquals( 9, count( $properties ) ); + $this->assertArrayHasKey( 'id', $properties ); + $this->assertArrayHasKey( 'title', $properties ); + $this->assertArrayHasKey( 'description', $properties ); + $this->assertArrayHasKey( 'order', $properties ); + $this->assertArrayHasKey( 'enabled', $properties ); + $this->assertArrayHasKey( 'method_title', $properties ); + $this->assertArrayHasKey( 'method_description', $properties ); + $this->assertArrayHasKey( 'method_supports', $properties ); + $this->assertArrayHasKey( 'settings', $properties ); + } + + /** + * Loads a particular gateway's settings so we can correctly test API output. + * + * @since 3.5.0 + * @param string $gateway_class Name of WC_Payment_Gateway class. + */ + private function get_settings( $gateway_class ) { + $gateway = new $gateway_class(); + $settings = array(); + $gateway->init_form_fields(); + foreach ( $gateway->form_fields as $id => $field ) { + // Make sure we at least have a title and type + if ( empty( $field['title'] ) || empty( $field['type'] ) ) { + continue; + } + // Ignore 'enabled' and 'description', to be in line with \WC_REST_Payment_Gateways_Controller::get_settings. + if ( in_array( $id, array( 'enabled', 'description' ), true ) ) { + continue; + } + $data = array( + 'id' => $id, + 'label' => empty( $field['label'] ) ? $field['title'] : $field['label'], + 'description' => empty( $field['description'] ) ? '' : $field['description'], + 'type' => $field['type'], + 'value' => $gateway->settings[ $id ], + 'default' => empty( $field['default'] ) ? '' : $field['default'], + 'tip' => empty( $field['description'] ) ? '' : $field['description'], + 'placeholder' => empty( $field['placeholder'] ) ? '' : $field['placeholder'], + ); + if ( ! empty( $field['options'] ) ) { + $data['options'] = $field['options']; + } + $settings[ $id ] = $data; + } + return $settings; + } + +} diff --git a/tests/legacy/unit-tests/rest-api/Tests/Version3/product-reviews.php b/tests/legacy/unit-tests/rest-api/Tests/Version3/product-reviews.php new file mode 100644 index 00000000000..9a69f06a863 --- /dev/null +++ b/tests/legacy/unit-tests/rest-api/Tests/Version3/product-reviews.php @@ -0,0 +1,470 @@ +user = $this->factory->user->create( + array( + 'role' => 'administrator', + ) + ); + } + + /** + * Test route registration. + * + * @since 3.5.0 + */ + public function test_register_routes() { + $routes = $this->server->get_routes(); + $this->assertArrayHasKey( '/wc/v3/products/reviews', $routes ); + $this->assertArrayHasKey( '/wc/v3/products/reviews/(?P[\d]+)', $routes ); + $this->assertArrayHasKey( '/wc/v3/products/reviews/batch', $routes ); + } + + /** + * Test getting all product reviews. + * + * @since 3.5.0 + */ + public function test_get_product_reviews() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + // Create 10 products reviews for the product + for ( $i = 0; $i < 10; $i++ ) { + $review_id = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_product_review( $product->get_id() ); + } + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/products/reviews' ) ); + $product_reviews = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 10, count( $product_reviews ) ); + $this->assertContains( + array( + 'id' => $review_id, + 'date_created' => $product_reviews[0]['date_created'], + 'date_created_gmt' => $product_reviews[0]['date_created_gmt'], + 'product_id' => $product->get_id(), + 'status' => 'approved', + 'reviewer' => 'admin', + 'reviewer_email' => 'woo@woo.local', + 'review' => "

Review content here

\n", + 'rating' => 0, + 'verified' => false, + 'reviewer_avatar_urls' => $product_reviews[0]['reviewer_avatar_urls'], + '_links' => array( + 'self' => array( + array( + 'href' => rest_url( '/wc/v3/products/reviews/' . $review_id ), + ), + ), + 'collection' => array( + array( + 'href' => rest_url( '/wc/v3/products/reviews' ), + ), + ), + 'up' => array( + array( + 'href' => rest_url( '/wc/v3/products/' . $product->get_id() ), + ), + ), + ), + ), + $product_reviews + ); + } + + /** + * Tests to make sure product reviews cannot be viewed without valid permissions. + * + * @since 3.5.0 + */ + public function test_get_product_reviews_without_permission() { + wp_set_current_user( 0 ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/products/reviews' ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Tests to make sure an error is returned when an invalid product is loaded. + * + * @since 3.5.0 + */ + public function test_get_product_reviews_invalid_product() { + wp_set_current_user( $this->user ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/products/0/reviews' ) ); + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * Tests getting a single product review. + * + * @since 3.5.0 + */ + public function test_get_product_review() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $product_review_id = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_product_review( $product->get_id() ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/products/reviews/' . $product_review_id ) ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( + array( + 'id' => $product_review_id, + 'date_created' => $data['date_created'], + 'date_created_gmt' => $data['date_created_gmt'], + 'product_id' => $product->get_id(), + 'status' => 'approved', + 'reviewer' => 'admin', + 'reviewer_email' => 'woo@woo.local', + 'review' => "

Review content here

\n", + 'rating' => 0, + 'verified' => false, + 'reviewer_avatar_urls' => $data['reviewer_avatar_urls'], + ), + $data + ); + } + + /** + * Tests getting a single product review without the correct permissions. + * + * @since 3.5.0 + */ + public function test_get_product_review_without_permission() { + wp_set_current_user( 0 ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $product_review_id = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_product_review( $product->get_id() ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/products/reviews/' . $product_review_id ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Tests getting a product review with an invalid ID. + * + * @since 3.5.0 + */ + public function test_get_product_review_invalid_id() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/products/reviews/0' ) ); + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * Tests creating a product review. + * + * @since 3.5.0 + */ + public function test_create_product_review() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $request = new WP_REST_Request( 'POST', '/wc/v3/products/reviews' ); + $request->set_body_params( + array( + 'review' => 'Hello world.', + 'reviewer' => 'Admin', + 'reviewer_email' => 'woo@woo.local', + 'rating' => '5', + 'product_id' => $product->get_id(), + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 201, $response->get_status() ); + $this->assertEquals( + array( + 'id' => $data['id'], + 'date_created' => $data['date_created'], + 'date_created_gmt' => $data['date_created_gmt'], + 'product_id' => $product->get_id(), + 'status' => 'approved', + 'reviewer' => 'Admin', + 'reviewer_email' => 'woo@woo.local', + 'review' => 'Hello world.', + 'rating' => 5, + 'verified' => false, + 'reviewer_avatar_urls' => $data['reviewer_avatar_urls'], + ), + $data + ); + } + + /** + * Tests creating a product review without required fields. + * + * @since 3.5.0 + */ + public function test_create_product_review_invalid_fields() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + + // missing review + $request = new WP_REST_Request( 'POST', '/wc/v3/products/reviews' ); + $request->set_body_params( + array( + 'reviewer' => 'Admin', + 'reviewer_email' => 'woo@woo.local', + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 400, $response->get_status() ); + + // Missing reviewer. + $request = new WP_REST_Request( 'POST', '/wc/v3/products/reviews' ); + $request->set_body_params( + array( + 'review' => 'Hello world.', + 'reviewer_email' => 'woo@woo.local', + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 400, $response->get_status() ); + + // missing reviewer_email + $request = new WP_REST_Request( 'POST', '/wc/v3/products/reviews' ); + $request->set_body_params( + array( + 'review' => 'Hello world.', + 'reviewer' => 'Admin', + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 400, $response->get_status() ); + } + + /** + * Tests updating a product review. + * + * @since 3.5.0 + */ + public function test_update_product_review() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $product_review_id = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_product_review( $product->get_id() ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/products/reviews/' . $product_review_id ) ); + $data = $response->get_data(); + $this->assertEquals( "

Review content here

\n", $data['review'] ); + $this->assertEquals( 'admin', $data['reviewer'] ); + $this->assertEquals( 'woo@woo.local', $data['reviewer_email'] ); + $this->assertEquals( 0, $data['rating'] ); + + $request = new WP_REST_Request( 'PUT', '/wc/v3/products/reviews/' . $product_review_id ); + $request->set_body_params( + array( + 'review' => 'Hello world - updated.', + 'reviewer' => 'Justin', + 'reviewer_email' => 'woo2@woo.local', + 'rating' => 3, + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $this->assertEquals( 'Hello world - updated.', $data['review'] ); + $this->assertEquals( 'Justin', $data['reviewer'] ); + $this->assertEquals( 'woo2@woo.local', $data['reviewer_email'] ); + $this->assertEquals( 3, $data['rating'] ); + } + + /** + * Tests updating a product review without the correct permissions. + * + * @since 3.5.0 + */ + public function test_update_product_review_without_permission() { + wp_set_current_user( 0 ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $product_review_id = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_product_review( $product->get_id() ); + + $request = new WP_REST_Request( 'PUT', '/wc/v3/products/reviews/' . $product_review_id ); + $request->set_body_params( + array( + 'review' => 'Hello world.', + 'reviewer' => 'Admin', + 'reviewer_email' => 'woo@woo.dev', + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Tests that updating a product review with an invalid id fails. + * + * @since 3.5.0 + */ + public function test_update_product_review_invalid_id() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + + $request = new WP_REST_Request( 'PUT', '/wc/v3/products/reviews/0' ); + $request->set_body_params( + array( + 'review' => 'Hello world.', + 'reviewer' => 'Admin', + 'reviewer_email' => 'woo@woo.dev', + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * Test deleting a product review. + * + * @since 3.5.0 + */ + public function test_delete_product_review() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $product_review_id = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_product_review( $product->get_id() ); + + $request = new WP_REST_Request( 'DELETE', '/wc/v3/products/reviews/' . $product_review_id ); + $request->set_param( 'force', true ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 200, $response->get_status() ); + } + + /** + * Test deleting a product review without permission/creds. + * + * @since 3.5.0 + */ + public function test_delete_product_without_permission() { + wp_set_current_user( 0 ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $product_review_id = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_product_review( $product->get_id() ); + + $request = new WP_REST_Request( 'DELETE', '/wc/v3/products/reviews/' . $product_review_id ); + $response = $this->server->dispatch( $request ); + + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test deleting a product review with an invalid id. + * + * @since 3.5.0 + */ + public function test_delete_product_review_invalid_id() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $product_review_id = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_product_review( $product->get_id() ); + + $request = new WP_REST_Request( 'DELETE', '/wc/v3/products/reviews/0' ); + $request->set_param( 'force', true ); + $response = $this->server->dispatch( $request ); + + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * Test batch managing product reviews. + * + * @since 3.5.0 + */ + public function test_product_reviews_batch() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + + $review_1_id = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_product_review( $product->get_id() ); + $review_2_id = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_product_review( $product->get_id() ); + $review_3_id = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_product_review( $product->get_id() ); + $review_4_id = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_product_review( $product->get_id() ); + + $request = new WP_REST_Request( 'POST', '/wc/v3/products/reviews/batch' ); + $request->set_body_params( + array( + 'update' => array( + array( + 'id' => $review_1_id, + 'review' => 'Updated review.', + ), + ), + 'delete' => array( + $review_2_id, + $review_3_id, + ), + 'create' => array( + array( + 'review' => 'New review.', + 'reviewer' => 'Justin', + 'reviewer_email' => 'woo3@woo.local', + 'product_id' => $product->get_id(), + ), + ), + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 'Updated review.', $data['update'][0]['review'] ); + $this->assertEquals( 'New review.', $data['create'][0]['review'] ); + $this->assertEquals( $review_2_id, $data['delete'][0]['previous']['id'] ); + $this->assertEquals( $review_3_id, $data['delete'][1]['previous']['id'] ); + + $request = new WP_REST_Request( 'GET', '/wc/v3/products/reviews' ); + $request->set_param( 'product', $product->get_id() ); + + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 3, count( $data ) ); + } + + /** + * Test the product review schema. + * + * @since 3.5.0 + */ + public function test_product_review_schema() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $request = new WP_REST_Request( 'OPTIONS', '/wc/v3/products/reviews' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $properties = $data['schema']['properties']; + + $this->assertEquals( 11, count( $properties ) ); + $this->assertArrayHasKey( 'id', $properties ); + $this->assertArrayHasKey( 'date_created', $properties ); + $this->assertArrayHasKey( 'date_created_gmt', $properties ); + $this->assertArrayHasKey( 'product_id', $properties ); + $this->assertArrayHasKey( 'status', $properties ); + $this->assertArrayHasKey( 'reviewer', $properties ); + $this->assertArrayHasKey( 'reviewer_email', $properties ); + $this->assertArrayHasKey( 'review', $properties ); + $this->assertArrayHasKey( 'rating', $properties ); + $this->assertArrayHasKey( 'verified', $properties ); + + if ( get_option( 'show_avatars' ) ) { + $this->assertArrayHasKey( 'reviewer_avatar_urls', $properties ); + } + } +} diff --git a/tests/legacy/unit-tests/rest-api/Tests/Version3/product-variations.php b/tests/legacy/unit-tests/rest-api/Tests/Version3/product-variations.php new file mode 100644 index 00000000000..98bfda23c6b --- /dev/null +++ b/tests/legacy/unit-tests/rest-api/Tests/Version3/product-variations.php @@ -0,0 +1,496 @@ +endpoint = new WC_REST_Product_Variations_Controller(); + $this->user = $this->factory->user->create( + array( + 'role' => 'administrator', + ) + ); + } + + /** + * Test route registration. + * + * @since 3.5.0 + */ + public function test_register_routes() { + $routes = $this->server->get_routes(); + $this->assertArrayHasKey( '/wc/v3/products/(?P[\d]+)/variations', $routes ); + $this->assertArrayHasKey( '/wc/v3/products/(?P[\d]+)/variations/(?P[\d]+)', $routes ); + $this->assertArrayHasKey( '/wc/v3/products/(?P[\d]+)/variations/batch', $routes ); + } + + /** + * Test getting variations. + * + * @since 3.5.0 + */ + public function test_get_variations() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_variation_product(); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/products/' . $product->get_id() . '/variations' ) ); + $variations = $response->get_data(); + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 2, count( $variations ) ); + $this->assertEquals( 'DUMMY SKU VARIABLE LARGE', $variations[0]['sku'] ); + $this->assertEquals( 'size', $variations[0]['attributes'][0]['name'] ); + } + + /** + * Test getting variations with an orderby clause. + * + * @since 3.9.0 + */ + public function test_get_variations_with_orderby() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_variation_product(); + $request = new WP_REST_Request( 'GET', '/wc/v3/products/' . $product->get_id() . '/variations' ); + $request->set_query_params( array( 'orderby' => 'menu_order' ) ); + $response = $this->server->dispatch( $request ); + $variations = $response->get_data(); + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 2, count( $variations ) ); + $this->assertEquals( 'DUMMY SKU VARIABLE SMALL', $variations[0]['sku'] ); + $this->assertEquals( 'size', $variations[0]['attributes'][0]['name'] ); + } + + /** + * Test getting variations without permission. + * + * @since 3.5.0 + */ + public function test_get_variations_without_permission() { + wp_set_current_user( 0 ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_variation_product(); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/products/' . $product->get_id() . '/variations' ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test getting a single variation. + * + * @since 3.5.0 + */ + public function test_get_variation() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_variation_product(); + $children = $product->get_children(); + $variation_id = $children[0]; + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/products/' . $product->get_id() . '/variations/' . $variation_id ) ); + $variation = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( $variation_id, $variation['id'] ); + $this->assertEquals( 'size', $variation['attributes'][0]['name'] ); + } + + /** + * Test getting single variation without permission. + * + * @since 3.5.0 + */ + public function test_get_variation_without_permission() { + wp_set_current_user( 0 ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_variation_product(); + $children = $product->get_children(); + $variation_id = $children[0]; + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/products/' . $product->get_id() . '/variations/' . $variation_id ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test deleting a single variation. + * + * @since 3.5.0 + */ + public function test_delete_variation() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_variation_product(); + $children = $product->get_children(); + $variation_id = $children[0]; + + $request = new WP_REST_Request( 'DELETE', '/wc/v3/products/' . $product->get_id() . '/variations/' . $variation_id ); + $request->set_param( 'force', true ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 200, $response->get_status() ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/products/' . $product->get_id() . '/variations' ) ); + $variations = $response->get_data(); + $this->assertEquals( 1, count( $variations ) ); + } + + /** + * Test deleting a single variation without permission. + * + * @since 3.5.0 + */ + public function test_delete_variation_without_permission() { + wp_set_current_user( 0 ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_variation_product(); + $children = $product->get_children(); + $variation_id = $children[0]; + + $request = new WP_REST_Request( 'DELETE', '/wc/v3/products/' . $product->get_id() . '/variations/' . $variation_id ); + $request->set_param( 'force', true ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test deleting a single variation with an invalid ID. + * + * @since 3.5.0 + */ + public function test_delete_variation_with_invalid_id() { + wp_set_current_user( 0 ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_variation_product(); + $request = new WP_REST_Request( 'DELETE', '/wc/v3/products/' . $product->get_id() . '/variations/0' ); + $request->set_param( 'force', true ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * Test editing a single variation. + * + * @since 3.5.0 + */ + public function test_update_variation() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_variation_product(); + $children = $product->get_children(); + $variation_id = $children[0]; + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/products/' . $product->get_id() . '/variations/' . $variation_id ) ); + $variation = $response->get_data(); + + $this->assertEquals( 'DUMMY SKU VARIABLE SMALL', $variation['sku'] ); + $this->assertEquals( 10, $variation['regular_price'] ); + $this->assertEmpty( $variation['sale_price'] ); + $this->assertEquals( 'small', $variation['attributes'][0]['option'] ); + + $request = new WP_REST_Request( 'PUT', '/wc/v3/products/' . $product->get_id() . '/variations/' . $variation_id ); + $request->set_body_params( + array( + 'sku' => 'FIXED-\'SKU', + 'sale_price' => '8', + 'description' => 'O_O', + 'image' => array( + 'position' => 0, + 'src' => 'http://cldup.com/Dr1Bczxq4q.png', + 'alt' => 'test upload image', + ), + 'attributes' => array( + array( + 'name' => 'pa_size', + 'option' => 'medium', + ), + ), + ) + ); + $response = $this->server->dispatch( $request ); + $variation = $response->get_data(); + + $this->assertTrue( isset( $variation['description'] ), print_r( $variation, true ) ); + $this->assertContains( 'O_O', $variation['description'], print_r( $variation, true ) ); + $this->assertEquals( '8', $variation['price'], print_r( $variation, true ) ); + $this->assertEquals( '8', $variation['sale_price'], print_r( $variation, true ) ); + $this->assertEquals( '10', $variation['regular_price'], print_r( $variation, true ) ); + $this->assertEquals( 'FIXED-\'SKU', $variation['sku'], print_r( $variation, true ) ); + $this->assertEquals( 'medium', $variation['attributes'][0]['option'], print_r( $variation, true ) ); + $this->assertContains( 'Dr1Bczxq4q', $variation['image']['src'], print_r( $variation, true ) ); + $this->assertContains( 'test upload image', $variation['image']['alt'], print_r( $variation, true ) ); + + wp_delete_attachment( $variation['image']['id'], true ); + } + + /** + * Test updating a single variation without permission. + * + * @since 3.5.0 + */ + public function test_update_variation_without_permission() { + wp_set_current_user( 0 ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_variation_product(); + $children = $product->get_children(); + $variation_id = $children[0]; + + $request = new WP_REST_Request( 'PUT', '/wc/v3/products/' . $product->get_id() . '/variations/' . $variation_id ); + $request->set_body_params( + array( + 'sku' => 'FIXED-SKU-NO-PERMISSION', + ) + ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test updating a single variation with an invalid ID. + * + * @since 3.5.0 + */ + public function test_update_variation_with_invalid_id() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_variation_product(); + $request = new WP_REST_Request( 'PUT', '/wc/v3/products/' . $product->get_id() . '/variations/0' ); + $request->set_body_params( + array( + 'sku' => 'FIXED-SKU-NO-PERMISSION', + ) + ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 400, $response->get_status() ); + } + + /** + * Test creating a single variation. + * + * @since 3.5.0 + */ + public function test_create_variation() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_variation_product(); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/products/' . $product->get_id() . '/variations' ) ); + $variations = $response->get_data(); + $this->assertEquals( 2, count( $variations ) ); + + $request = new WP_REST_Request( 'POST', '/wc/v3/products/' . $product->get_id() . '/variations' ); + $request->set_body_params( + array( + 'sku' => 'DUMMY SKU VARIABLE MEDIUM', + 'regular_price' => '12', + 'description' => 'A medium size.', + 'attributes' => array( + array( + 'name' => 'pa_size', + 'option' => 'medium', + ), + ), + ) + ); + $response = $this->server->dispatch( $request ); + $variation = $response->get_data(); + + $this->assertContains( 'A medium size.', $variation['description'] ); + $this->assertEquals( '12', $variation['price'] ); + $this->assertEquals( '12', $variation['regular_price'] ); + $this->assertTrue( $variation['purchasable'] ); + $this->assertEquals( 'DUMMY SKU VARIABLE MEDIUM', $variation['sku'] ); + $this->assertEquals( 'medium', $variation['attributes'][0]['option'] ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/products/' . $product->get_id() . '/variations' ) ); + $variations = $response->get_data(); + $this->assertEquals( 3, count( $variations ) ); + } + + /** + * Test creating a single variation without permission. + * + * @since 3.5.0 + */ + public function test_create_variation_without_permission() { + wp_set_current_user( 0 ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_variation_product(); + + $request = new WP_REST_Request( 'POST', '/wc/v3/products/' . $product->get_id() . '/variations' ); + $request->set_body_params( + array( + 'sku' => 'DUMMY SKU VARIABLE MEDIUM', + 'regular_price' => '12', + 'description' => 'A medium size.', + 'attributes' => array( + array( + 'name' => 'pa_size', + 'option' => 'medium', + ), + ), + ) + ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test batch managing product variations. + * + * @since 3.5.0 + */ + public function test_product_variations_batch() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_variation_product(); + $children = $product->get_children(); + $request = new WP_REST_Request( 'POST', '/wc/v3/products/' . $product->get_id() . '/variations/batch' ); + $request->set_body_params( + array( + 'update' => array( + array( + 'id' => $children[0], + 'description' => 'Updated description.', + 'image' => array( + 'position' => 0, + 'src' => 'http://cldup.com/Dr1Bczxq4q.png', + 'alt' => 'test upload image', + ), + ), + ), + 'delete' => array( + $children[1], + ), + 'create' => array( + array( + 'sku' => 'DUMMY SKU VARIABLE MEDIUM', + 'regular_price' => '12', + 'description' => 'A medium size.', + 'attributes' => array( + array( + 'name' => 'pa_size', + 'option' => 'medium', + ), + ), + ), + ), + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertContains( 'Updated description.', $data['update'][0]['description'] ); + $this->assertEquals( 'DUMMY SKU VARIABLE MEDIUM', $data['create'][0]['sku'] ); + $this->assertEquals( 'medium', $data['create'][0]['attributes'][0]['option'] ); + $this->assertEquals( $children[1], $data['delete'][0]['id'] ); + + $request = new WP_REST_Request( 'GET', '/wc/v3/products/' . $product->get_id() . '/variations' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 2, count( $data ) ); + + wp_delete_attachment( $data[1]['image']['id'], true ); + } + + /** + * Test variation schema. + * + * @since 3.5.0 + */ + public function test_variation_schema() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $request = new WP_REST_Request( 'OPTIONS', '/wc/v3/products/' . $product->get_id() . '/variations' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $properties = $data['schema']['properties']; + + $this->assertEquals( 37, count( $properties ) ); + $this->assertArrayHasKey( 'id', $properties ); + $this->assertArrayHasKey( 'date_created', $properties ); + $this->assertArrayHasKey( 'date_modified', $properties ); + $this->assertArrayHasKey( 'description', $properties ); + $this->assertArrayHasKey( 'permalink', $properties ); + $this->assertArrayHasKey( 'sku', $properties ); + $this->assertArrayHasKey( 'price', $properties ); + $this->assertArrayHasKey( 'regular_price', $properties ); + $this->assertArrayHasKey( 'sale_price', $properties ); + $this->assertArrayHasKey( 'date_on_sale_from', $properties ); + $this->assertArrayHasKey( 'date_on_sale_to', $properties ); + $this->assertArrayHasKey( 'on_sale', $properties ); + $this->assertArrayHasKey( 'purchasable', $properties ); + $this->assertArrayHasKey( 'virtual', $properties ); + $this->assertArrayHasKey( 'downloadable', $properties ); + $this->assertArrayHasKey( 'downloads', $properties ); + $this->assertArrayHasKey( 'download_limit', $properties ); + $this->assertArrayHasKey( 'download_expiry', $properties ); + $this->assertArrayHasKey( 'tax_status', $properties ); + $this->assertArrayHasKey( 'tax_class', $properties ); + $this->assertArrayHasKey( 'manage_stock', $properties ); + $this->assertArrayHasKey( 'stock_quantity', $properties ); + $this->assertArrayHasKey( 'stock_status', $properties ); + $this->assertArrayHasKey( 'backorders', $properties ); + $this->assertArrayHasKey( 'backorders_allowed', $properties ); + $this->assertArrayHasKey( 'backordered', $properties ); + $this->assertArrayHasKey( 'weight', $properties ); + $this->assertArrayHasKey( 'dimensions', $properties ); + $this->assertArrayHasKey( 'shipping_class', $properties ); + $this->assertArrayHasKey( 'shipping_class_id', $properties ); + $this->assertArrayHasKey( 'image', $properties ); + $this->assertArrayHasKey( 'attributes', $properties ); + $this->assertArrayHasKey( 'menu_order', $properties ); + $this->assertArrayHasKey( 'meta_data', $properties ); + } + + /** + * Test updating a variation stock. + * + * @since 3.5.0 + */ + public function test_update_variation_manage_stock() { + wp_set_current_user( $this->user ); + + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_variation_product(); + $product->set_manage_stock( false ); + $product->save(); + + $children = $product->get_children(); + $variation_id = $children[0]; + + // Set stock to true. + $request = new WP_REST_Request( 'PUT', '/wc/v3/products/' . $product->get_id() . '/variations/' . $variation_id ); + $request->set_body_params( + array( + 'manage_stock' => true, + ) + ); + + $response = $this->server->dispatch( $request ); + $variation = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( true, $variation['manage_stock'] ); + + // Set stock to false. + $request = new WP_REST_Request( 'PUT', '/wc/v3/products/' . $product->get_id() . '/variations/' . $variation_id ); + $request->set_body_params( + array( + 'manage_stock' => false, + ) + ); + + $response = $this->server->dispatch( $request ); + $variation = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( false, $variation['manage_stock'] ); + + // Set stock to false but parent is managing stock. + $product->set_manage_stock( true ); + $product->save(); + $request = new WP_REST_Request( 'PUT', '/wc/v3/products/' . $product->get_id() . '/variations/' . $variation_id ); + $request->set_body_params( + array( + 'manage_stock' => false, + ) + ); + + $response = $this->server->dispatch( $request ); + $variation = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 'parent', $variation['manage_stock'] ); + } +} diff --git a/tests/legacy/unit-tests/rest-api/Tests/Version3/products.php b/tests/legacy/unit-tests/rest-api/Tests/Version3/products.php new file mode 100644 index 00000000000..22351897134 --- /dev/null +++ b/tests/legacy/unit-tests/rest-api/Tests/Version3/products.php @@ -0,0 +1,861 @@ +endpoint = new WC_REST_Products_Controller(); + $this->user = $this->factory->user->create( + array( + 'role' => 'administrator', + ) + ); + } + + /** + * Test route registration. + * + * @since 3.5.0 + */ + public function test_register_routes() { + $routes = $this->server->get_routes(); + $this->assertArrayHasKey( '/wc/v3/products', $routes ); + $this->assertArrayHasKey( '/wc/v3/products/(?P[\d]+)', $routes ); + $this->assertArrayHasKey( '/wc/v3/products/batch', $routes ); + } + + /** + * Test getting products. + * + * @since 3.5.0 + */ + public function test_get_products() { + wp_set_current_user( $this->user ); + \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_external_product(); + sleep( 1 ); // So both products have different timestamps. + \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/products' ) ); + $products = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + + $this->assertEquals( 2, count( $products ) ); + $this->assertEquals( 'Dummy Product', $products[0]['name'] ); + $this->assertEquals( 'DUMMY SKU', $products[0]['sku'] ); + $this->assertEquals( 'Dummy External Product', $products[1]['name'] ); + $this->assertEquals( 'DUMMY EXTERNAL SKU', $products[1]['sku'] ); + } + + /** + * Test getting trashed products. + */ + public function test_get_trashed_products() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $data_store = WC_Data_Store::load( 'product' ); + $data_store->delete( $product ); + $request = new WP_REST_Request( 'GET', '/wc/v3/products' ); + $request->set_query_params( array( 'status' => 'trash' ) ); + $response = $this->server->dispatch( $request ); + $products = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 1, count( $products ) ); + $this->assertEquals( $product->get_name(), $products[0]['name'] ); + $this->assertEquals( $product->get_id(), $products[0]['id'] ); + } + + /** + * Trashed products should not be returned by default. + */ + public function test_get_trashed_products_not_returned_by_default() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $data_store = WC_Data_Store::load( 'product' ); + $data_store->delete( $product ); + + $response = $this->server->dispatch( + new WP_REST_Request( 'GET', '/wc/v3/products' ) + ); + $products = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 0, count( $products ) ); + } + + /** + * Trashed product can be fetched directly. + */ + public function test_get_trashed_products_returned_by_id() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $data_store = WC_Data_Store::load( 'product' ); + $data_store->delete( $product ); + + $response = $this->server->dispatch( + new WP_REST_Request( 'GET', '/wc/v3/products/' . $product->get_id() ) + ); + + $this->assertEquals( 200, $response->get_status() ); + } + + /** + * Test getting products without permission. + * + * @since 3.5.0 + */ + public function test_get_products_without_permission() { + wp_set_current_user( 0 ); + \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/products' ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test getting a single product. + * + * @since 3.5.0 + */ + public function test_get_product() { + wp_set_current_user( $this->user ); + $simple = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_external_product(); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/products/' . $simple->get_id() ) ); + $product = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertContains( + array( + 'id' => $simple->get_id(), + 'name' => 'Dummy External Product', + 'type' => 'simple', + 'status' => 'publish', + 'sku' => 'DUMMY EXTERNAL SKU', + 'regular_price' => 10, + ), + $product + ); + } + + /** + * Test getting single product without permission. + * + * @since 3.5.0 + */ + public function test_get_product_without_permission() { + wp_set_current_user( 0 ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/products/' . $product->get_id() ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test deleting a single product. + * + * @since 3.5.0 + */ + public function test_delete_product() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + + $request = new WP_REST_Request( 'DELETE', '/wc/v3/products/' . $product->get_id() ); + $request->set_param( 'force', true ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 200, $response->get_status() ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/products' ) ); + $variations = $response->get_data(); + $this->assertEquals( 0, count( $variations ) ); + } + + /** + * Test deleting a single product without permission. + * + * @since 3.5.0 + */ + public function test_delete_product_without_permission() { + wp_set_current_user( 0 ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $request = new WP_REST_Request( 'DELETE', '/wc/v3/products/' . $product->get_id() ); + $request->set_param( 'force', true ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test deleting a single product with an invalid ID. + * + * @since 3.5.0 + */ + public function test_delete_product_with_invalid_id() { + wp_set_current_user( 0 ); + $request = new WP_REST_Request( 'DELETE', '/wc/v3/products/0' ); + $request->set_param( 'force', true ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * Test editing a single product. Tests multiple product types. + * + * @since 3.5.0 + */ + public function test_update_product() { + wp_set_current_user( $this->user ); + + // test simple products. + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/products/' . $product->get_id() ) ); + $data = $response->get_data(); + $date_created = date( 'Y-m-d\TH:i:s', current_time( 'timestamp' ) ); + + $this->assertEquals( 'DUMMY SKU', $data['sku'] ); + $this->assertEquals( 10, $data['regular_price'] ); + $this->assertEmpty( $data['sale_price'] ); + + $request = new WP_REST_Request( 'PUT', '/wc/v3/products/' . $product->get_id() ); + $request->set_body_params( + array( + 'sku' => 'FIXED-SKU', + 'sale_price' => '8', + 'description' => 'Testing', + 'date_created' => $date_created, + 'images' => array( + array( + 'position' => 0, + 'src' => 'http://cldup.com/Dr1Bczxq4q.png', + 'alt' => 'test upload image', + ), + ), + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertContains( 'Testing', $data['description'] ); + $this->assertEquals( '8', $data['price'] ); + $this->assertEquals( '8', $data['sale_price'] ); + $this->assertEquals( '10', $data['regular_price'] ); + $this->assertEquals( 'FIXED-SKU', $data['sku'] ); + $this->assertEquals( $date_created, $data['date_created'] ); + $this->assertContains( 'Dr1Bczxq4q', $data['images'][0]['src'] ); + $this->assertContains( 'test upload image', $data['images'][0]['alt'] ); + $product->delete( true ); + wp_delete_attachment( $data['images'][0]['id'], true ); + + // test variable product (variations are tested in product-variations.php). + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_variation_product(); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/products/' . $product->get_id() ) ); + $data = $response->get_data(); + + foreach ( array( 'small', 'large' ) as $term_name ) { + $this->assertContains( $term_name, $data['attributes'][0]['options'] ); + } + + $request = new WP_REST_Request( 'PUT', '/wc/v3/products/' . $product->get_id() ); + $request->set_body_params( + array( + 'attributes' => array( + array( + 'id' => 0, + 'name' => 'pa_color', + 'options' => array( + 'red', + 'yellow', + ), + 'visible' => false, + 'variation' => 1, + ), + array( + 'id' => 0, + 'name' => 'pa_size', + 'options' => array( + 'small', + ), + 'visible' => false, + 'variation' => 1, + ), + ), + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( array( 'small' ), $data['attributes'][0]['options'] ); + + foreach ( array( 'red', 'yellow' ) as $term_name ) { + $this->assertContains( $term_name, $data['attributes'][1]['options'] ); + } + + $product->delete( true ); + + // test external product. + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_external_product(); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/products/' . $product->get_id() ) ); + $data = $response->get_data(); + + $this->assertEquals( 'Buy external product', $data['button_text'] ); + $this->assertEquals( 'http://woocommerce.com', $data['external_url'] ); + + $request = new WP_REST_Request( 'PUT', '/wc/v3/products/' . $product->get_id() ); + $request->set_body_params( + array( + 'button_text' => 'Test API Update', + 'external_url' => 'http://automattic.com', + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 'Test API Update', $data['button_text'] ); + $this->assertEquals( 'http://automattic.com', $data['external_url'] ); + } + + /** + * Test updating a single product without permission. + * + * @since 3.5.0 + */ + public function test_update_product_without_permission() { + wp_set_current_user( 0 ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $request = new WP_REST_Request( 'PUT', '/wc/v3/products/' . $product->get_id() ); + $request->set_body_params( + array( + 'sku' => 'FIXED-SKU-NO-PERMISSION', + ) + ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test updating a single product with an invalid ID. + * + * @since 3.5.0 + */ + public function test_update_product_with_invalid_id() { + wp_set_current_user( $this->user ); + $request = new WP_REST_Request( 'PUT', '/wc/v2/products/0' ); + $request->set_body_params( + array( + 'sku' => 'FIXED-SKU-INVALID-ID', + ) + ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 400, $response->get_status() ); + } + + /** + * Test creating a single product. + * + * @since 3.5.0 + */ + public function test_create_product() { + wp_set_current_user( $this->user ); + + $request = new WP_REST_Request( 'POST', '/wc/v3/products/shipping_classes' ); + $request->set_body_params( + array( + 'name' => 'Test', + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $shipping_class_id = $data['id']; + + // Create simple. + $request = new WP_REST_Request( 'POST', '/wc/v3/products' ); + $request->set_body_params( + array( + 'type' => 'simple', + 'name' => 'Test Simple Product', + 'sku' => 'DUMMY SKU SIMPLE API', + 'regular_price' => '10', + 'shipping_class' => 'test', + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( '10', $data['price'] ); + $this->assertEquals( '10', $data['regular_price'] ); + $this->assertTrue( $data['purchasable'] ); + $this->assertEquals( 'DUMMY SKU SIMPLE API', $data['sku'] ); + $this->assertEquals( 'Test Simple Product', $data['name'] ); + $this->assertEquals( 'simple', $data['type'] ); + $this->assertEquals( $shipping_class_id, $data['shipping_class_id'] ); + + // Create external. + $request = new WP_REST_Request( 'POST', '/wc/v3/products' ); + $request->set_body_params( + array( + 'type' => 'external', + 'name' => 'Test External Product', + 'sku' => 'DUMMY SKU EXTERNAL API', + 'regular_price' => '10', + 'button_text' => 'Test Button', + 'external_url' => 'https://wordpress.org', + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( '10', $data['price'] ); + $this->assertEquals( '10', $data['regular_price'] ); + $this->assertFalse( $data['purchasable'] ); + $this->assertEquals( 'DUMMY SKU EXTERNAL API', $data['sku'] ); + $this->assertEquals( 'Test External Product', $data['name'] ); + $this->assertEquals( 'external', $data['type'] ); + $this->assertEquals( 'Test Button', $data['button_text'] ); + $this->assertEquals( 'https://wordpress.org', $data['external_url'] ); + + // Create variable. + $request = new WP_REST_Request( 'POST', '/wc/v3/products' ); + $request->set_body_params( + array( + 'type' => 'variable', + 'name' => 'Test Variable Product', + 'sku' => 'DUMMY SKU VARIABLE API', + 'attributes' => array( + array( + 'id' => 0, + 'name' => 'pa_size', + 'options' => array( + 'small', + 'medium', + ), + 'visible' => false, + 'variation' => 1, + ), + ), + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 'DUMMY SKU VARIABLE API', $data['sku'] ); + $this->assertEquals( 'Test Variable Product', $data['name'] ); + $this->assertEquals( 'variable', $data['type'] ); + $this->assertEquals( array( 'small', 'medium' ), $data['attributes'][0]['options'] ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/products' ) ); + $products = $response->get_data(); + $this->assertEquals( 3, count( $products ) ); + } + + /** + * Test creating a single product without permission. + * + * @since 3.5.0 + */ + public function test_create_product_without_permission() { + wp_set_current_user( 0 ); + + $request = new WP_REST_Request( 'POST', '/wc/v3/products' ); + $request->set_body_params( + array( + 'name' => 'Test Product', + 'regular_price' => '12', + ) + ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test batch managing products. + * + * @since 3.5.0 + */ + public function test_products_batch() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $product_2 = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $request = new WP_REST_Request( 'POST', '/wc/v3/products/batch' ); + $request->set_body_params( + array( + 'update' => array( + array( + 'id' => $product->get_id(), + 'description' => 'Updated description.', + ), + ), + 'delete' => array( + $product_2->get_id(), + ), + 'create' => array( + array( + 'sku' => 'DUMMY SKU BATCH TEST 1', + 'regular_price' => '10', + 'name' => 'Test Batch Create 1', + 'type' => 'external', + 'button_text' => 'Test Button', + ), + array( + 'sku' => 'DUMMY SKU BATCH TEST 2', + 'regular_price' => '20', + 'name' => 'Test Batch Create 2', + 'type' => 'simple', + ), + ), + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertContains( 'Updated description.', $data['update'][0]['description'] ); + $this->assertEquals( 'DUMMY SKU BATCH TEST 1', $data['create'][0]['sku'] ); + $this->assertEquals( 'DUMMY SKU BATCH TEST 2', $data['create'][1]['sku'] ); + $this->assertEquals( 'Test Button', $data['create'][0]['button_text'] ); + $this->assertEquals( 'external', $data['create'][0]['type'] ); + $this->assertEquals( 'simple', $data['create'][1]['type'] ); + $this->assertEquals( $product_2->get_id(), $data['delete'][0]['id'] ); + + $request = new WP_REST_Request( 'GET', '/wc/v3/products' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 3, count( $data ) ); + } + + /** + * Tests to make sure you can filter products post statuses by both + * the status query arg and WP_Query. + * + * @since 3.5.0 + */ + public function test_products_filter_post_status() { + wp_set_current_user( $this->user ); + for ( $i = 0; $i < 8; $i++ ) { + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + if ( 0 === $i % 2 ) { + wp_update_post( + array( + 'ID' => $product->get_id(), + 'post_status' => 'draft', + ) + ); + } + } + + // Test filtering with status=publish. + $request = new WP_REST_Request( 'GET', '/wc/v3/products' ); + $request->set_param( 'status', 'publish' ); + $response = $this->server->dispatch( $request ); + $products = $response->get_data(); + + $this->assertEquals( 4, count( $products ) ); + foreach ( $products as $product ) { + $this->assertEquals( 'publish', $product['status'] ); + } + + // Test filtering with status=draft. + $request = new WP_REST_Request( 'GET', '/wc/v3/products' ); + $request->set_param( 'status', 'draft' ); + $response = $this->server->dispatch( $request ); + $products = $response->get_data(); + + $this->assertEquals( 4, count( $products ) ); + foreach ( $products as $product ) { + $this->assertEquals( 'draft', $product['status'] ); + } + + // Test filtering with no filters - which should return 'any' (all 8). + $request = new WP_REST_Request( 'GET', '/wc/v3/products' ); + $response = $this->server->dispatch( $request ); + $products = $response->get_data(); + + $this->assertEquals( 8, count( $products ) ); + } + + /** + * Test product schema. + * + * @since 3.5.0 + */ + public function test_product_schema() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $request = new WP_REST_Request( 'OPTIONS', '/wc/v3/products/' . $product->get_id() ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $properties = $data['schema']['properties']; + $this->assertEquals( 65, count( $properties ) ); + } + + /** + * Test product category. + * + * @since 3.5.0 + */ + public function test_get_products_by_category() { + wp_set_current_user( $this->user ); + + // Create one product with a category. + $category = wp_insert_term( 'Some Category', 'product_cat' ); + + $product = new WC_Product_Simple(); + $product->set_category_ids( array( $category['term_id'] ) ); + $product->save(); + + // Create one product without category, i.e. Uncategorized. + $product_2 = new WC_Product_Simple(); + $product_2->save(); + + // Test product assigned to a single category. + $query_params = array( + 'category' => (string) $category['term_id'], + ); + $request = new WP_REST_Request( 'GET', '/wc/v2/products' ); + $request->set_query_params( $query_params ); + $response = $this->server->dispatch( $request ); + $response_products = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + foreach ( $response_products as $response_product ) { + $this->assertEquals( $product->get_id(), $response_product['id'] ); + $this->assertEquals( $product->get_category_ids(), wp_list_pluck( $response_product['categories'], 'id' ) ); + } + + // Test product without categories. + $request = new WP_REST_Request( 'GET', '/wc/v2/products/' . $product_2->get_id() ); + $response = $this->server->dispatch( $request ); + $response_product = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertCount( 1, $response_product['categories'], print_r( $response_product, true ) ); + $this->assertEquals( 'uncategorized', $response_product['categories'][0]['slug'] ); + + } + + /** + * Test getting products by product type. + * + * @since 3.5.0 + */ + public function test_get_products_by_type() { + wp_set_current_user( $this->user ); + + $simple = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $external = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_external_product(); + $grouped = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_grouped_product(); + $variable = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_variation_product(); + + $product_ids_for_type = array( + 'simple' => array( $simple->get_id() ), + 'external' => array( $external->get_id() ), + 'grouped' => array( $grouped->get_id() ), + 'variable' => array( $variable->get_id() ), + ); + + foreach ( $grouped->get_children() as $additional_product ) { + $product_ids_for_type['simple'][] = $additional_product; + } + + foreach ( $product_ids_for_type as $product_type => $product_ids ) { + $query_params = array( + 'type' => $product_type, + ); + $request = new WP_REST_Request( 'GET', '/wc/v2/products' ); + $request->set_query_params( $query_params ); + $response = $this->server->dispatch( $request ); + $response_products = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( count( $product_ids ), count( $response_products ) ); + foreach ( $response_products as $response_product ) { + $this->assertContains( $response_product['id'], $product_ids_for_type[ $product_type ], 'REST API: ' . $product_type . ' not found correctly' ); + } + } + } + + /** + * Test getting products by featured property. + * + * @since 3.5.0 + */ + public function test_get_featured_products() { + wp_set_current_user( $this->user ); + + // Create a featured product. + $feat_product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $feat_product->set_featured( true ); + $feat_product->save(); + + // Create a non-featured product. + $nonfeat_product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $nonfeat_product->save(); + + $query_params = array( + 'featured' => 'true', + ); + $request = new WP_REST_Request( 'GET', '/wc/v2/products' ); + $request->set_query_params( $query_params ); + $response = $this->server->dispatch( $request ); + $response_products = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + foreach ( $response_products as $response_product ) { + $this->assertEquals( $feat_product->get_id(), $response_product['id'], 'REST API: Featured product not found correctly' ); + } + + $query_params = array( + 'featured' => 'false', + ); + $request = new WP_REST_Request( 'GET', '/wc/v2/products' ); + $request->set_query_params( $query_params ); + $response = $this->server->dispatch( $request ); + $response_products = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + foreach ( $response_products as $response_product ) { + $this->assertEquals( $nonfeat_product->get_id(), $response_product['id'], 'REST API: Featured product not found correctly' ); + } + } + + /** + * Test getting products by shipping class property. + * + * @since 3.5.0 + */ + public function test_get_products_by_shipping_class() { + wp_set_current_user( $this->user ); + + $shipping_class_1 = wp_insert_term( 'Bulky', 'product_shipping_class' ); + + $product_1 = new WC_Product_Simple(); + $product_1->set_shipping_class_id( $shipping_class_1['term_id'] ); + $product_1->save(); + + $query_params = array( + 'shipping_class' => (string) $shipping_class_1['term_id'], + ); + $request = new WP_REST_Request( 'GET', '/wc/v2/products' ); + $request->set_query_params( $query_params ); + $response = $this->server->dispatch( $request ); + $response_products = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + foreach ( $response_products as $response_product ) { + $this->assertEquals( $product_1->get_id(), $response_product['id'] ); + } + } + + /** + * Test getting products by tag. + * + * @since 3.5.0 + */ + public function test_get_products_by_tag() { + wp_set_current_user( $this->user ); + + $test_tag_1 = wp_insert_term( 'Tag 1', 'product_tag' ); + + // Product with a tag. + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $product->set_tag_ids( array( $test_tag_1['term_id'] ) ); + $product->save(); + + // Product without a tag. + $product_2 = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + + $query_params = array( + 'tag' => (string) $test_tag_1['term_id'], + ); + $request = new WP_REST_Request( 'GET', '/wc/v2/products' ); + $request->set_query_params( $query_params ); + $response = $this->server->dispatch( $request ); + $response_products = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + foreach ( $response_products as $response_product ) { + $this->assertEquals( $product->get_id(), $response_product['id'] ); + } + } + + /** + * Test getting products by global attribute. + * + * @since 3.5.0 + */ + public function test_get_products_by_attribute() { + global $wpdb; + wp_set_current_user( $this->user ); + + // Variable product with 2 different variations. + $variable_product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_variation_product(); + + // Terms created by variable product. + $term_large = get_term_by( 'slug', 'large', 'pa_size' ); + $term_small = get_term_by( 'slug', 'small', 'pa_size' ); + + // Simple product without attribute. + $product_1 = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + + // Simple product with attribute size = large. + $product_2 = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $product_2->set_attributes( array( 'pa_size' => 'large' ) ); + $product_2->save(); + + // Link the product to the term. + $wpdb->insert( + $wpdb->prefix . 'term_relationships', + array( + 'object_id' => $product_2->get_id(), + 'term_taxonomy_id' => $term_large->term_id, + 'term_order' => 0, + ) + ); + + // Products with attribute size == large. + $expected_product_ids = array( + $variable_product->get_id(), + $product_2->get_id(), + ); + $query_params = array( + 'attribute' => 'pa_size', + 'attribute_term' => (string) $term_large->term_id, + ); + $request = new WP_REST_Request( 'GET', '/wc/v2/products' ); + $request->set_query_params( $query_params ); + $response = $this->server->dispatch( $request ); + $response_products = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( count( $expected_product_ids ), count( $response_products ) ); + foreach ( $response_products as $response_product ) { + $this->assertContains( $response_product['id'], $expected_product_ids ); + } + + // Products with attribute size == small. + $expected_product_ids = array( + $variable_product->get_id(), + ); + $query_params = array( + 'attribute' => 'pa_size', + 'attribute_term' => (string) $term_small->term_id, + ); + $request = new WP_REST_Request( 'GET', '/wc/v2/products' ); + $request->set_query_params( $query_params ); + $response = $this->server->dispatch( $request ); + $response_products = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( count( $expected_product_ids ), count( $response_products ) ); + foreach ( $response_products as $response_product ) { + $this->assertContains( $response_product['id'], $expected_product_ids ); + } + } +} diff --git a/tests/legacy/unit-tests/rest-api/Tests/Version3/reports-coupons-totals.php b/tests/legacy/unit-tests/rest-api/Tests/Version3/reports-coupons-totals.php new file mode 100644 index 00000000000..162b8af7ef7 --- /dev/null +++ b/tests/legacy/unit-tests/rest-api/Tests/Version3/reports-coupons-totals.php @@ -0,0 +1,103 @@ +user = $this->factory->user->create( + array( + 'role' => 'administrator', + ) + ); + } + + /** + * Test route registration. + * + * @since 3.5.0 + */ + public function test_register_routes() { + $routes = $this->server->get_routes(); + $this->assertArrayHasKey( '/wc/v3/reports/coupons/totals', $routes ); + } + + /** + * Test getting all product reviews. + * + * @since 3.5.0 + */ + public function test_get_reports() { + global $wpdb; + wp_set_current_user( $this->user ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/reports/coupons/totals' ) ); + $report = $response->get_data(); + $types = wc_get_coupon_types(); + $data = array(); + + foreach ( $types as $slug => $name ) { + $results = $wpdb->get_results( + $wpdb->prepare( + " + SELECT count(meta_id) AS total + FROM $wpdb->postmeta + WHERE meta_key = 'discount_type' + AND meta_value = %s + ", + $slug + ) + ); + + $total = isset( $results[0] ) ? (int) $results[0]->total : 0; + + $data[] = array( + 'slug' => $slug, + 'name' => $name, + 'total' => $total, + ); + } + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( count( $types ), count( $report ) ); + $this->assertEquals( $data, $report ); + } + + /** + * Tests to make sure product reviews cannot be viewed without valid permissions. + * + * @since 3.5.0 + */ + public function test_get_reports_without_permission() { + wp_set_current_user( 0 ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/reports/coupons/totals' ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test the product review schema. + * + * @since 3.5.0 + */ + public function test_product_review_schema() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $request = new WP_REST_Request( 'OPTIONS', '/wc/v3/reports/coupons/totals' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $properties = $data['schema']['properties']; + + $this->assertEquals( 3, count( $properties ) ); + $this->assertArrayHasKey( 'slug', $properties ); + $this->assertArrayHasKey( 'name', $properties ); + $this->assertArrayHasKey( 'total', $properties ); + } +} diff --git a/tests/legacy/unit-tests/rest-api/Tests/Version3/reports-customers-totals.php b/tests/legacy/unit-tests/rest-api/Tests/Version3/reports-customers-totals.php new file mode 100644 index 00000000000..cd0e58f886f --- /dev/null +++ b/tests/legacy/unit-tests/rest-api/Tests/Version3/reports-customers-totals.php @@ -0,0 +1,119 @@ +user = $this->factory->user->create( + array( + 'role' => 'administrator', + ) + ); + } + + /** + * Test route registration. + * + * @since 3.5.0 + */ + public function test_register_routes() { + $routes = $this->server->get_routes(); + $this->assertArrayHasKey( '/wc/v3/reports/customers/totals', $routes ); + } + + /** + * Test getting all product reviews. + * + * @since 3.5.0 + */ + public function test_get_reports() { + wp_set_current_user( $this->user ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/reports/customers/totals' ) ); + $report = $response->get_data(); + $users_count = count_users(); + $total_customers = 0; + + foreach ( $users_count['avail_roles'] as $role => $total ) { + if ( in_array( $role, array( 'administrator', 'shop_manager' ), true ) ) { + continue; + } + + $total_customers += (int) $total; + } + + $customers_query = new WP_User_Query( + array( + 'role__not_in' => array( 'administrator', 'shop_manager' ), + 'number' => 0, + 'fields' => 'ID', + 'count_total' => true, + 'meta_query' => array( // WPCS: slow query ok. + array( + 'key' => 'paying_customer', + 'value' => 1, + 'compare' => '=', + ), + ), + ) + ); + + $total_paying = (int) $customers_query->get_total(); + + $data = array( + array( + 'slug' => 'paying', + 'name' => __( 'Paying customer', 'woocommerce' ), + 'total' => $total_paying, + ), + array( + 'slug' => 'non_paying', + 'name' => __( 'Non-paying customer', 'woocommerce' ), + 'total' => $total_customers - $total_paying, + ), + ); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 2, count( $report ) ); + $this->assertEquals( $data, $report ); + } + + /** + * Tests to make sure product reviews cannot be viewed without valid permissions. + * + * @since 3.5.0 + */ + public function test_get_reports_without_permission() { + wp_set_current_user( 0 ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/reports/customers/totals' ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test the product review schema. + * + * @since 3.5.0 + */ + public function test_product_review_schema() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $request = new WP_REST_Request( 'OPTIONS', '/wc/v3/reports/customers/totals' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $properties = $data['schema']['properties']; + + $this->assertEquals( 3, count( $properties ) ); + $this->assertArrayHasKey( 'slug', $properties ); + $this->assertArrayHasKey( 'name', $properties ); + $this->assertArrayHasKey( 'total', $properties ); + } +} diff --git a/tests/legacy/unit-tests/rest-api/Tests/Version3/reports-orders-totals.php b/tests/legacy/unit-tests/rest-api/Tests/Version3/reports-orders-totals.php new file mode 100644 index 00000000000..93a21a87634 --- /dev/null +++ b/tests/legacy/unit-tests/rest-api/Tests/Version3/reports-orders-totals.php @@ -0,0 +1,92 @@ +user = $this->factory->user->create( + array( + 'role' => 'administrator', + ) + ); + } + + /** + * Test route registration. + * + * @since 3.5.0 + */ + public function test_register_routes() { + $routes = $this->server->get_routes(); + $this->assertArrayHasKey( '/wc/v3/reports/orders/totals', $routes ); + } + + /** + * Test getting all product reviews. + * + * @since 3.5.0 + */ + public function test_get_reports() { + wp_set_current_user( $this->user ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/reports/orders/totals' ) ); + $report = $response->get_data(); + $totals = wp_count_posts( 'shop_order' ); + $data = array(); + + foreach ( wc_get_order_statuses() as $slug => $name ) { + if ( ! isset( $totals->$slug ) ) { + continue; + } + + $data[] = array( + 'slug' => str_replace( 'wc-', '', $slug ), + 'name' => $name, + 'total' => (int) $totals->$slug, + ); + } + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( count( wc_get_order_statuses() ), count( $report ) ); + $this->assertEquals( $data, $report ); + } + + /** + * Tests to make sure product reviews cannot be viewed without valid permissions. + * + * @since 3.5.0 + */ + public function test_get_reports_without_permission() { + wp_set_current_user( 0 ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/reports/orders/totals' ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test the product review schema. + * + * @since 3.5.0 + */ + public function test_product_review_schema() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $request = new WP_REST_Request( 'OPTIONS', '/wc/v3/reports/orders/totals' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $properties = $data['schema']['properties']; + + $this->assertEquals( 3, count( $properties ) ); + $this->assertArrayHasKey( 'slug', $properties ); + $this->assertArrayHasKey( 'name', $properties ); + $this->assertArrayHasKey( 'total', $properties ); + } +} diff --git a/tests/legacy/unit-tests/rest-api/Tests/Version3/reports-products-totals.php b/tests/legacy/unit-tests/rest-api/Tests/Version3/reports-products-totals.php new file mode 100644 index 00000000000..6243786ffad --- /dev/null +++ b/tests/legacy/unit-tests/rest-api/Tests/Version3/reports-products-totals.php @@ -0,0 +1,99 @@ +user = $this->factory->user->create( + array( + 'role' => 'administrator', + ) + ); + } + + /** + * Test route registration. + * + * @since 3.5.0 + */ + public function test_register_routes() { + $routes = $this->server->get_routes(); + $this->assertArrayHasKey( '/wc/v3/reports/products/totals', $routes ); + } + + /** + * Test getting all product reviews. + * + * @since 3.5.0 + */ + public function test_get_reports() { + wp_set_current_user( $this->user ); + WC_Install::create_terms(); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/reports/products/totals' ) ); + $report = $response->get_data(); + $types = wc_get_product_types(); + $terms = get_terms( + array( + 'taxonomy' => 'product_type', + 'hide_empty' => false, + ) + ); + $data = array(); + + foreach ( $terms as $product_type ) { + if ( ! isset( $types[ $product_type->name ] ) ) { + continue; + } + + $data[] = array( + 'slug' => $product_type->name, + 'name' => $types[ $product_type->name ], + 'total' => (int) $product_type->count, + ); + } + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( count( $types ), count( $report ) ); + $this->assertEquals( $data, $report ); + } + + /** + * Tests to make sure product reviews cannot be viewed without valid permissions. + * + * @since 3.5.0 + */ + public function test_get_reports_without_permission() { + wp_set_current_user( 0 ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/reports/products/totals' ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test the product review schema. + * + * @since 3.5.0 + */ + public function test_product_review_schema() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $request = new WP_REST_Request( 'OPTIONS', '/wc/v3/reports/products/totals' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $properties = $data['schema']['properties']; + + $this->assertEquals( 3, count( $properties ) ); + $this->assertArrayHasKey( 'slug', $properties ); + $this->assertArrayHasKey( 'name', $properties ); + $this->assertArrayHasKey( 'total', $properties ); + } +} diff --git a/tests/legacy/unit-tests/rest-api/Tests/Version3/reports-reviews-totals.php b/tests/legacy/unit-tests/rest-api/Tests/Version3/reports-reviews-totals.php new file mode 100644 index 00000000000..3a2dc1622ff --- /dev/null +++ b/tests/legacy/unit-tests/rest-api/Tests/Version3/reports-reviews-totals.php @@ -0,0 +1,98 @@ +user = $this->factory->user->create( + array( + 'role' => 'administrator', + ) + ); + } + + /** + * Test route registration. + * + * @since 3.5.0 + */ + public function test_register_routes() { + $routes = $this->server->get_routes(); + $this->assertArrayHasKey( '/wc/v3/reports/reviews/totals', $routes ); + } + + /** + * Test getting all product reviews. + * + * @since 3.5.0 + */ + public function test_get_reports() { + global $wpdb; + wp_set_current_user( $this->user ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/reports/reviews/totals' ) ); + $report = $response->get_data(); + $data = array(); + + $query_data = array( + 'count' => true, + 'post_type' => 'product', + 'meta_key' => 'rating', // WPCS: slow query ok. + 'meta_value' => '', // WPCS: slow query ok. + ); + + for ( $i = 1; $i <= 5; $i++ ) { + $query_data['meta_value'] = $i; + + $data[] = array( + 'slug' => 'rated_' . $i . '_out_of_5', + /* translators: %s: average rating */ + 'name' => sprintf( __( 'Rated %s out of 5', 'woocommerce' ), $i ), + 'total' => (int) get_comments( $query_data ), + ); + } + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( count( $data ), count( $report ) ); + $this->assertEquals( $data, $report ); + } + + /** + * Tests to make sure product reviews cannot be viewed without valid permissions. + * + * @since 3.5.0 + */ + public function test_get_reports_without_permission() { + wp_set_current_user( 0 ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/reports/reviews/totals' ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test the product review schema. + * + * @since 3.5.0 + */ + public function test_product_review_schema() { + wp_set_current_user( $this->user ); + $product = \Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper::create_simple_product(); + $request = new WP_REST_Request( 'OPTIONS', '/wc/v3/reports/reviews/totals' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $properties = $data['schema']['properties']; + + $this->assertEquals( 3, count( $properties ) ); + $this->assertArrayHasKey( 'slug', $properties ); + $this->assertArrayHasKey( 'name', $properties ); + $this->assertArrayHasKey( 'total', $properties ); + } +} diff --git a/tests/legacy/unit-tests/rest-api/Tests/Version3/settings.php b/tests/legacy/unit-tests/rest-api/Tests/Version3/settings.php new file mode 100644 index 00000000000..daf907a7d8a --- /dev/null +++ b/tests/legacy/unit-tests/rest-api/Tests/Version3/settings.php @@ -0,0 +1,906 @@ +endpoint = new WC_REST_Setting_Options_Controller(); + \Automattic\WooCommerce\RestApi\UnitTests\Helpers\SettingsHelper::register(); + $this->user = $this->factory->user->create( + array( + 'role' => 'administrator', + ) + ); + } + + /** + * Test route registration. + * + * @since 3.5.0 + */ + public function test_register_routes() { + $routes = $this->server->get_routes(); + $this->assertArrayHasKey( '/wc/v3/settings', $routes ); + $this->assertArrayHasKey( '/wc/v3/settings/(?P[\w-]+)', $routes ); + $this->assertArrayHasKey( '/wc/v3/settings/(?P[\w-]+)/(?P[\w-]+)', $routes ); + } + + /** + * Test getting all groups. + * + * @since 3.5.0 + */ + public function test_get_groups() { + wp_set_current_user( $this->user ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/settings' ) ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + + $this->assertContains( + array( + 'id' => 'test', + 'label' => 'Test extension', + 'parent_id' => '', + 'description' => 'My awesome test settings.', + 'sub_groups' => array( 'sub-test' ), + '_links' => array( + 'options' => array( + array( + 'href' => rest_url( '/wc/v3/settings/test' ), + ), + ), + ), + ), + $data + ); + + $this->assertContains( + array( + 'id' => 'sub-test', + 'label' => 'Sub test', + 'parent_id' => 'test', + 'description' => '', + 'sub_groups' => array(), + '_links' => array( + 'options' => array( + array( + 'href' => rest_url( '/wc/v3/settings/sub-test' ), + ), + ), + ), + ), + $data + ); + } + + /** + * Test /settings without valid permissions/creds. + * + * @since 3.5.0 + */ + public function test_get_groups_without_permission() { + wp_set_current_user( 0 ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/settings' ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test /settings without valid permissions/creds. + * + * @since 3.5.0 + * @covers WC_Rest_Settings_Controller::get_items + */ + public function test_get_groups_none_registered() { + wp_set_current_user( $this->user ); + + remove_all_filters( 'woocommerce_settings_groups' ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/settings' ) ); + $this->assertEquals( 500, $response->get_status() ); + + \Automattic\WooCommerce\RestApi\UnitTests\Helpers\SettingsHelper::register(); + } + + /** + * Test groups schema. + * + * @since 3.5.0 + */ + public function test_get_group_schema() { + $request = new WP_REST_Request( 'OPTIONS', '/wc/v3/settings' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $properties = $data['schema']['properties']; + $this->assertEquals( 5, count( $properties ) ); + $this->assertArrayHasKey( 'id', $properties ); + $this->assertArrayHasKey( 'parent_id', $properties ); + $this->assertArrayHasKey( 'label', $properties ); + $this->assertArrayHasKey( 'description', $properties ); + $this->assertArrayHasKey( 'sub_groups', $properties ); + } + + /** + * Test settings schema. + * + * @since 3.5.0 + */ + public function test_get_setting_schema() { + $request = new WP_REST_Request( 'OPTIONS', '/wc/v3/settings/test/woocommerce_shop_page_display' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $properties = $data['schema']['properties']; + $this->assertEquals( 10, count( $properties ) ); + $this->assertArrayHasKey( 'id', $properties ); + $this->assertArrayHasKey( 'label', $properties ); + $this->assertArrayHasKey( 'description', $properties ); + $this->assertArrayHasKey( 'value', $properties ); + $this->assertArrayHasKey( 'default', $properties ); + $this->assertArrayHasKey( 'tip', $properties ); + $this->assertArrayHasKey( 'placeholder', $properties ); + $this->assertArrayHasKey( 'type', $properties ); + $this->assertArrayHasKey( 'options', $properties ); + $this->assertArrayHasKey( 'group_id', $properties ); + } + + /** + * Test getting a single group. + * + * @since 3.5.0 + */ + public function test_get_group() { + wp_set_current_user( $this->user ); + + // test route callback receiving an empty group id. + $result = $this->endpoint->get_group_settings( '' ); + $this->assertWPError( $result ); + + // test getting a group that does not exist. + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/settings/not-real' ) ); + $this->assertEquals( 404, $response->get_status() ); + + // test getting the 'invalid' group. + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/settings/invalid' ) ); + $this->assertEquals( 404, $response->get_status() ); + + // test getting a valid group with settings attached to it. + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/settings/test' ) ); + $data = $response->get_data(); + $this->assertEquals( 1, count( $data ) ); + $this->assertEquals( 'woocommerce_shop_page_display', $data[0]['id'] ); + $this->assertEmpty( $data[0]['value'] ); + } + + /** + * Test getting a single group without permission. + * + * @since 3.5.0 + */ + public function test_get_group_without_permission() { + wp_set_current_user( 0 ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/settings/coupon-data' ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test updating a single setting. + * + * @since 3.5.0 + */ + public function test_update_setting() { + wp_set_current_user( $this->user ); + + // test defaults first. + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/settings/test/woocommerce_shop_page_display' ) ); + $data = $response->get_data(); + $this->assertEquals( '', $data['value'] ); + + // test updating shop display setting. + $request = new WP_REST_Request( 'PUT', sprintf( '/wc/v3/settings/%s/%s', 'test', 'woocommerce_shop_page_display' ) ); + $request->set_body_params( + array( + 'value' => 'both', + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 'both', $data['value'] ); + $this->assertEquals( 'both', get_option( 'woocommerce_shop_page_display' ) ); + + $request = new WP_REST_Request( 'PUT', sprintf( '/wc/v3/settings/%s/%s', 'test', 'woocommerce_shop_page_display' ) ); + $request->set_body_params( + array( + 'value' => 'subcategories', + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 'subcategories', $data['value'] ); + $this->assertEquals( 'subcategories', get_option( 'woocommerce_shop_page_display' ) ); + + $request = new WP_REST_Request( 'PUT', sprintf( '/wc/v3/settings/%s/%s', 'test', 'woocommerce_shop_page_display' ) ); + $request->set_body_params( + array( + 'value' => '', + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( '', $data['value'] ); + $this->assertEquals( '', get_option( 'woocommerce_shop_page_display' ) ); + } + + /** + * Test updating multiple settings at once. + * + * @since 3.5.0 + */ + public function test_update_settings() { + wp_set_current_user( $this->user ); + + // test defaults first. + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/settings/test' ) ); + $data = $response->get_data(); + $this->assertEquals( '', $data[0]['value'] ); + + // test setting both at once. + $request = new WP_REST_Request( 'POST', '/wc/v3/settings/test/batch' ); + $request->set_body_params( + array( + 'update' => array( + array( + 'id' => 'woocommerce_shop_page_display', + 'value' => 'both', + ), + ), + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 'both', $data['update'][0]['value'] ); + $this->assertEquals( 'both', get_option( 'woocommerce_shop_page_display' ) ); + + // test updating one, but making sure the other value stays the same. + $request = new WP_REST_Request( 'POST', '/wc/v3/settings/test/batch' ); + $request->set_body_params( + array( + 'update' => array( + array( + 'id' => 'woocommerce_shop_page_display', + 'value' => 'subcategories', + ), + ), + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $this->assertEquals( 'subcategories', $data['update'][0]['value'] ); + $this->assertEquals( 'subcategories', get_option( 'woocommerce_shop_page_display' ) ); + } + + /** + * Test getting a single setting. + * + * @since 3.5.0 + */ + public function test_get_setting() { + wp_set_current_user( $this->user ); + + // test getting an invalid setting from a group that does not exist. + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/settings/not-real/woocommerce_shop_page_display' ) ); + $data = $response->get_data(); + $this->assertEquals( 404, $response->get_status() ); + + // test getting an invalid setting from a group that does exist. + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/settings/invalid/invalid' ) ); + $data = $response->get_data(); + $this->assertEquals( 404, $response->get_status() ); + + // test getting a valid setting. + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/settings/test/woocommerce_shop_page_display' ) ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + + $this->assertEquals( 'woocommerce_shop_page_display', $data['id'] ); + $this->assertEquals( 'Shop page display', $data['label'] ); + $this->assertEquals( '', $data['default'] ); + $this->assertEquals( 'select', $data['type'] ); + $this->assertEquals( '', $data['value'] ); + } + + /** + * Test getting a single setting without valid user permissions. + * + * @since 3.5.0 + */ + public function test_get_setting_without_permission() { + wp_set_current_user( 0 ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/settings/test/woocommerce_shop_page_display' ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Tests the GET single setting route handler receiving an empty setting ID. + * + * @since 3.5.0 + */ + public function test_get_setting_empty_setting_id() { + $result = $this->endpoint->get_setting( 'test', '' ); + + $this->assertWPError( $result ); + } + + /** + * Tests the GET single setting route handler receiving an invalid setting ID. + * + * @since 3.5.0 + */ + public function test_get_setting_invalid_setting_id() { + $result = $this->endpoint->get_setting( 'test', 'invalid' ); + + $this->assertWPError( $result ); + } + + /** + * Tests the GET single setting route handler encountering an invalid setting type. + * + * @since 3.5.0 + */ + public function test_get_setting_invalid_setting_type() { + // $controller = $this->getMock( 'WC_Rest_Setting_Options_Controller', array( 'get_group_settings', 'is_setting_type_valid' ) ); + $controller = $this->getMockBuilder( 'WC_Rest_Setting_Options_Controller' )->setMethods( array( 'get_group_settings', 'is_setting_type_valid' ) )->getMock(); + + $controller + ->expects( $this->any() ) + ->method( 'get_group_settings' ) + ->will( $this->returnValue( \Automattic\WooCommerce\RestApi\UnitTests\Helpers\SettingsHelper::register_test_settings( array() ) ) ); + + $controller + ->expects( $this->any() ) + ->method( 'is_setting_type_valid' ) + ->will( $this->returnValue( false ) ); + + $result = $controller->get_setting( 'test', 'woocommerce_shop_page_display' ); + + $this->assertWPError( $result ); + } + + /** + * Test updating a single setting without valid user permissions. + * + * @since 3.5.0 + */ + public function test_update_setting_without_permission() { + wp_set_current_user( 0 ); + + $request = new WP_REST_Request( 'PUT', sprintf( '/wc/v3/settings/%s/%s', 'test', 'woocommerce_shop_page_display' ) ); + $request->set_body_params( + array( + 'value' => 'subcategories', + ) + ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 401, $response->get_status() ); + } + + + /** + * Test updating multiple settings without valid user permissions. + * + * @since 3.5.0 + */ + public function test_update_settings_without_permission() { + wp_set_current_user( 0 ); + + $request = new WP_REST_Request( 'POST', '/wc/v3/settings/test/batch' ); + $request->set_body_params( + array( + 'update' => array( + array( + 'id' => 'woocommerce_shop_page_display', + 'value' => 'subcategories', + ), + ), + ) + ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test updating a bad setting ID. + * + * @since 3.5.0 + * @covers WC_Rest_Setting_Options_Controller::update_item + */ + public function test_update_setting_bad_setting_id() { + wp_set_current_user( $this->user ); + + $request = new WP_REST_Request( 'PUT', '/wc/v3/settings/test/invalid' ); + $request->set_body_params( + array( + 'value' => 'test', + ) + ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * Tests our classic setting registration to make sure settings added for WP-Admin are available over the API. + * + * @since 3.5.0 + */ + public function test_classic_settings() { + wp_set_current_user( $this->user ); + + // Make sure the group is properly registered. + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/settings/products' ) ); + $data = $response->get_data(); + $this->assertTrue( is_array( $data ) ); + $this->assertContains( + array( + 'id' => 'woocommerce_downloads_require_login', + 'label' => 'Access restriction', + 'description' => 'Downloads require login', + 'type' => 'checkbox', + 'default' => 'no', + 'tip' => 'This setting does not apply to guest purchases.', + 'value' => 'no', + '_links' => array( + 'self' => array( + array( + 'href' => rest_url( '/wc/v3/settings/products/woocommerce_downloads_require_login' ), + ), + ), + 'collection' => array( + array( + 'href' => rest_url( '/wc/v3/settings/products' ), + ), + ), + ), + ), + $data + ); + + // test get single. + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/settings/products/woocommerce_dimension_unit' ) ); + $data = $response->get_data(); + + $this->assertEquals( 'cm', $data['default'] ); + + // test update. + $request = new WP_REST_Request( 'PUT', sprintf( '/wc/v3/settings/%s/%s', 'products', 'woocommerce_dimension_unit' ) ); + $request->set_body_params( + array( + 'value' => 'yd', + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 'yd', $data['value'] ); + $this->assertEquals( 'yd', get_option( 'woocommerce_dimension_unit' ) ); + } + + /** + * Tests our email etting registration to make sure settings added for WP-Admin are available over the API. + * + * @since 3.5.0 + */ + public function test_email_settings() { + wp_set_current_user( $this->user ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/settings/email_new_order' ) ); + $settings = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + + $this->assertContains( + array( + 'id' => 'recipient', + 'label' => 'Recipient(s)', + 'description' => 'Enter recipients (comma separated) for this email. Defaults to admin@example.org.', + 'type' => 'text', + 'default' => '', + 'tip' => 'Enter recipients (comma separated) for this email. Defaults to admin@example.org.', + 'value' => '', + '_links' => array( + 'self' => array( + array( + 'href' => rest_url( '/wc/v3/settings/email_new_order/recipient' ), + ), + ), + 'collection' => array( + array( + 'href' => rest_url( '/wc/v3/settings/email_new_order' ), + ), + ), + ), + ), + $settings + ); + + // test get single. + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/settings/email_new_order/subject' ) ); + $setting = $response->get_data(); + + $this->assertEquals( + array( + 'id' => 'subject', + 'label' => 'Subject', + 'description' => 'Available placeholders: {site_title}, {site_address}, {site_url}, {order_date}, {order_number}', + 'type' => 'text', + 'default' => '', + 'tip' => 'Available placeholders: {site_title}, {site_address}, {site_url}, {order_date}, {order_number}', + 'value' => '', + 'group_id' => 'email_new_order', + ), + $setting + ); + + // test update. + $request = new WP_REST_Request( 'PUT', sprintf( '/wc/v3/settings/%s/%s', 'email_new_order', 'subject' ) ); + $request->set_body_params( + array( + 'value' => 'This is my subject', + ) + ); + $response = $this->server->dispatch( $request ); + $setting = $response->get_data(); + + $this->assertEquals( + array( + 'id' => 'subject', + 'label' => 'Subject', + 'description' => 'Available placeholders: {site_title}, {site_address}, {site_url}, {order_date}, {order_number}', + 'type' => 'text', + 'default' => '', + 'tip' => 'Available placeholders: {site_title}, {site_address}, {site_url}, {order_date}, {order_number}', + 'value' => 'This is my subject', + 'group_id' => 'email_new_order', + ), + $setting + ); + + // test updating another subject and making sure it works with a "similar" id. + $request = new WP_REST_Request( 'GET', sprintf( '/wc/v3/settings/%s/%s', 'email_customer_new_account', 'subject' ) ); + $response = $this->server->dispatch( $request ); + $setting = $response->get_data(); + + $this->assertEmpty( $setting['value'] ); + + // test update. + $request = new WP_REST_Request( 'PUT', sprintf( '/wc/v3/settings/%s/%s', 'email_customer_new_account', 'subject' ) ); + $request->set_body_params( + array( + 'value' => 'This is my new subject', + ) + ); + $response = $this->server->dispatch( $request ); + $setting = $response->get_data(); + + $this->assertEquals( 'This is my new subject', $setting['value'] ); + + // make sure the other is what we left it. + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/settings/email_new_order/subject' ) ); + $setting = $response->get_data(); + + $this->assertEquals( 'This is my subject', $setting['value'] ); + } + + /** + * Test validation of checkbox settings. + * + * @since 3.5.0 + */ + public function test_validation_checkbox() { + wp_set_current_user( $this->user ); + + // test bogus value. + $request = new WP_REST_Request( 'PUT', sprintf( '/wc/v3/settings/%s/%s', 'email_cancelled_order', 'enabled' ) ); + $request->set_body_params( + array( + 'value' => 'not_yes_or_no', + ) + ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 400, $response->get_status() ); + + // test yes. + $request = new WP_REST_Request( 'PUT', sprintf( '/wc/v3/settings/%s/%s', 'email_cancelled_order', 'enabled' ) ); + $request->set_body_params( + array( + 'value' => 'yes', + ) + ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 200, $response->get_status() ); + + // test no. + $request = new WP_REST_Request( 'PUT', sprintf( '/wc/v3/settings/%s/%s', 'email_cancelled_order', 'enabled' ) ); + $request->set_body_params( + array( + 'value' => 'no', + ) + ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 200, $response->get_status() ); + } + + /** + * Test validation of radio settings. + * + * @since 3.5.0 + */ + public function test_validation_radio() { + wp_set_current_user( $this->user ); + + // not a valid option. + $request = new WP_REST_Request( 'PUT', sprintf( '/wc/v3/settings/%s/%s', 'shipping', 'woocommerce_ship_to_destination' ) ); + $request->set_body_params( + array( + 'value' => 'billing2', + ) + ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 400, $response->get_status() ); + + // valid. + $request = new WP_REST_Request( 'PUT', sprintf( '/wc/v3/settings/%s/%s', 'shipping', 'woocommerce_ship_to_destination' ) ); + $request->set_body_params( + array( + 'value' => 'billing', + ) + ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 200, $response->get_status() ); + } + + /** + * Test validation of multiselect. + * + * @since 3.5.0 + */ + public function test_validation_multiselect() { + wp_set_current_user( $this->user ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', sprintf( '/wc/v3/settings/%s/%s', 'general', 'woocommerce_specific_allowed_countries' ) ) ); + $setting = $response->get_data(); + $this->assertEmpty( $setting['value'] ); + + $request = new WP_REST_Request( 'PUT', sprintf( '/wc/v3/settings/%s/%s', 'general', 'woocommerce_specific_allowed_countries' ) ); + $request->set_body_params( + array( + 'value' => array( 'AX', 'DZ', 'MMM' ), + ) + ); + $response = $this->server->dispatch( $request ); + $setting = $response->get_data(); + $this->assertEquals( array( 'AX', 'DZ' ), $setting['value'] ); + } + + /** + * Test validation of select. + * + * @since 3.5.0 + */ + public function test_validation_select() { + wp_set_current_user( $this->user ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', sprintf( '/wc/v3/settings/%s/%s', 'products', 'woocommerce_weight_unit' ) ) ); + $setting = $response->get_data(); + $this->assertEquals( 'kg', $setting['value'] ); + + // invalid. + $request = new WP_REST_Request( 'PUT', sprintf( '/wc/v3/settings/%s/%s', 'products', 'woocommerce_weight_unit' ) ); + $request->set_body_params( + array( + 'value' => 'pounds', // invalid, should be lbs. + ) + ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 400, $response->get_status() ); + + // valid. + $request = new WP_REST_Request( 'PUT', sprintf( '/wc/v3/settings/%s/%s', 'products', 'woocommerce_weight_unit' ) ); + $request->set_body_params( + array( + 'value' => 'lbs', // invalid, should be lbs. + ) + ); + $response = $this->server->dispatch( $request ); + $setting = $response->get_data(); + $this->assertEquals( 'lbs', $setting['value'] ); + } + + /** + * Test to make sure the 'base location' setting is present in the response. + * That it is returned as 'select' and not 'single_select_country', + * and that both state and country options are returned. + * + * @since 3.5.0 + */ + public function test_woocommerce_default_country() { + wp_set_current_user( $this->user ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/settings/general/woocommerce_default_country' ) ); + $setting = $response->get_data(); + + $this->assertEquals( 'select', $setting['type'] ); + $this->assertArrayHasKey( 'GB', $setting['options'] ); + $this->assertArrayHasKey( 'US:OR', $setting['options'] ); + } + + /** + * Test to make sure the store address setting can be fetched and updated. + * + * @since 3.5.0 + */ + public function test_woocommerce_store_address() { + wp_set_current_user( $this->user ); + update_option( 'woocommerce_store_address', rand( 1000, 9999 ) ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/settings/general/woocommerce_store_address' ) ); + $setting = $response->get_data(); + $this->assertEquals( 'text', $setting['type'] ); + + // Repalce the old value with something uniquely new. + $old_value = $setting['value']; + $new_value = $old_value . ' ' . rand( 1000, 9999 ); + $request = new WP_REST_Request( 'PUT', '/wc/v3/settings/general/woocommerce_store_address' ); + $request->set_body_params( + array( + 'value' => $new_value, + ) + ); + $response = $this->server->dispatch( $request ); + $setting = $response->get_data(); + $this->assertEquals( $new_value, $setting['value'] ); + + // Put the original value back. + $request = new WP_REST_Request( 'PUT', '/wc/v3/settings/general/woocommerce_store_address' ); + $request->set_body_params( + array( + 'value' => $old_value, + ) + ); + $response = $this->server->dispatch( $request ); + $setting = $response->get_data(); + $this->assertEquals( $old_value, $setting['value'] ); + } + + /** + * Test to make sure the store address 2 (line 2) setting can be fetched and updated. + * + * @since 3.5.0 + */ + public function test_woocommerce_store_address_2() { + wp_set_current_user( $this->user ); + update_option( 'woocommerce_store_address_2', rand( 1000, 9999 ) ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/settings/general/woocommerce_store_address_2' ) ); + $setting = $response->get_data(); + $this->assertEquals( 'text', $setting['type'] ); + + // Repalce the old value with something uniquely new. + $old_value = $setting['value']; + $new_value = $old_value . ' ' . rand( 1000, 9999 ); + $request = new WP_REST_Request( 'PUT', '/wc/v3/settings/general/woocommerce_store_address_2' ); + $request->set_body_params( + array( + 'value' => $new_value, + ) + ); + $response = $this->server->dispatch( $request ); + $setting = $response->get_data(); + $this->assertEquals( $new_value, $setting['value'] ); + + // Put the original value back. + $request = new WP_REST_Request( 'PUT', '/wc/v3/settings/general/woocommerce_store_address_2' ); + $request->set_body_params( + array( + 'value' => $old_value, + ) + ); + $response = $this->server->dispatch( $request ); + $setting = $response->get_data(); + $this->assertEquals( $old_value, $setting['value'] ); + } + + /** + * Test to make sure the store city setting can be fetched and updated. + * + * @since 3.5.0 + */ + public function test_woocommerce_store_city() { + wp_set_current_user( $this->user ); + update_option( 'woocommerce_store_city', rand( 1000, 9999 ) ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/settings/general/woocommerce_store_city' ) ); + $setting = $response->get_data(); + $this->assertEquals( 'text', $setting['type'] ); + + // Repalce the old value with something uniquely new. + $old_value = $setting['value']; + $new_value = $old_value . ' ' . rand( 1000, 9999 ); + $request = new WP_REST_Request( 'PUT', '/wc/v3/settings/general/woocommerce_store_city' ); + $request->set_body_params( + array( + 'value' => $new_value, + ) + ); + $response = $this->server->dispatch( $request ); + $setting = $response->get_data(); + $this->assertEquals( $new_value, $setting['value'] ); + + // Put the original value back. + $request = new WP_REST_Request( 'PUT', '/wc/v3/settings/general/woocommerce_store_city' ); + $request->set_body_params( + array( + 'value' => $old_value, + ) + ); + $response = $this->server->dispatch( $request ); + $setting = $response->get_data(); + $this->assertEquals( $old_value, $setting['value'] ); + } + + /** + * Test to make sure the store postcode setting can be fetched and updated. + * + * @since 3.5.0 + */ + public function test_woocommerce_store_postcode() { + wp_set_current_user( $this->user ); + update_option( 'woocommerce_store_postcode', rand( 1000, 9999 ) ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/settings/general/woocommerce_store_postcode' ) ); + $setting = $response->get_data(); + $this->assertEquals( 'text', $setting['type'] ); + + // Repalce the old value with something uniquely new. + $old_value = $setting['value']; + $new_value = $old_value . ' ' . rand( 1000, 9999 ); + $request = new WP_REST_Request( 'PUT', '/wc/v3/settings/general/woocommerce_store_postcode' ); + $request->set_body_params( + array( + 'value' => $new_value, + ) + ); + $response = $this->server->dispatch( $request ); + $setting = $response->get_data(); + $this->assertEquals( $new_value, $setting['value'] ); + + // Put the original value back. + $request = new WP_REST_Request( 'PUT', '/wc/v3/settings/general/woocommerce_store_postcode' ); + $request->set_body_params( + array( + 'value' => $old_value, + ) + ); + $response = $this->server->dispatch( $request ); + $setting = $response->get_data(); + $this->assertEquals( $old_value, $setting['value'] ); + } +} diff --git a/tests/legacy/unit-tests/rest-api/Tests/Version3/shipping-methods.php b/tests/legacy/unit-tests/rest-api/Tests/Version3/shipping-methods.php new file mode 100644 index 00000000000..ad139c427af --- /dev/null +++ b/tests/legacy/unit-tests/rest-api/Tests/Version3/shipping-methods.php @@ -0,0 +1,143 @@ +endpoint = new WC_REST_Shipping_Methods_Controller(); + $this->user = $this->factory->user->create( + array( + 'role' => 'administrator', + ) + ); + } + + /** + * Test route registration. + * + * @since 3.5.0 + */ + public function test_register_routes() { + $routes = $this->server->get_routes(); + $this->assertArrayHasKey( '/wc/v3/shipping_methods', $routes ); + $this->assertArrayHasKey( '/wc/v3/shipping_methods/(?P[\w-]+)', $routes ); + } + + /** + * Test getting all shipping methods. + * + * @since 3.5.0 + */ + public function test_get_shipping_methods() { + wp_set_current_user( $this->user ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/shipping_methods' ) ); + $methods = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertContains( + array( + 'id' => 'free_shipping', + 'title' => 'Free shipping', + 'description' => 'Free shipping is a special method which can be triggered with coupons and minimum spends.', + '_links' => array( + 'self' => array( + array( + 'href' => rest_url( '/wc/v3/shipping_methods/free_shipping' ), + ), + ), + 'collection' => array( + array( + 'href' => rest_url( '/wc/v3/shipping_methods' ), + ), + ), + ), + ), + $methods + ); + } + + /** + * Tests to make sure shipping methods cannot viewed without valid permissions. + * + * @since 3.5.0 + */ + public function test_get_shipping_methods_without_permission() { + wp_set_current_user( 0 ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/shipping_methods' ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Tests getting a single shipping method. + * + * @since 3.5.0 + */ + public function test_get_shipping_method() { + wp_set_current_user( $this->user ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/shipping_methods/local_pickup' ) ); + $method = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( + array( + 'id' => 'local_pickup', + 'title' => 'Local pickup', + 'description' => 'Allow customers to pick up orders themselves. By default, when using local pickup store base taxes will apply regardless of customer address.', + ), + $method + ); + } + + /** + * Tests getting a single shipping method without the correct permissions. + * + * @since 3.5.0 + */ + public function test_get_shipping_method_without_permission() { + wp_set_current_user( 0 ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/shipping_methods/local_pickup' ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Tests getting a shipping method with an invalid ID. + * + * @since 3.5.0 + */ + public function test_get_shipping_method_invalid_id() { + wp_set_current_user( $this->user ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/shipping_methods/fake_method' ) ); + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * Test the shipping method schema. + * + * @since 3.5.0 + */ + public function test_shipping_method_schema() { + wp_set_current_user( $this->user ); + + $request = new WP_REST_Request( 'OPTIONS', '/wc/v3/shipping_methods' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $properties = $data['schema']['properties']; + + $this->assertEquals( 3, count( $properties ) ); + $this->assertArrayHasKey( 'id', $properties ); + $this->assertArrayHasKey( 'title', $properties ); + $this->assertArrayHasKey( 'description', $properties ); + } +} diff --git a/tests/legacy/unit-tests/rest-api/Tests/Version3/shipping-zones.php b/tests/legacy/unit-tests/rest-api/Tests/Version3/shipping-zones.php new file mode 100644 index 00000000000..a735357b41f --- /dev/null +++ b/tests/legacy/unit-tests/rest-api/Tests/Version3/shipping-zones.php @@ -0,0 +1,825 @@ +endpoint = new WC_REST_Shipping_Zones_Controller(); + $this->user = $this->factory->user->create( + array( + 'role' => 'administrator', + ) + ); + $this->zones = array(); + } + + /** + * Helper method to create a Shipping Zone. + * + * @param string $name Zone name. + * @param int $order Optional. Zone sort order. + * @return WC_Shipping_Zone + */ + protected function create_shipping_zone( $name, $order = 0, $locations = array() ) { + $zone = new WC_Shipping_Zone( null ); + $zone->set_zone_name( $name ); + $zone->set_zone_order( $order ); + $zone->set_locations( $locations ); + $zone->save(); + + $this->zones[] = $zone; + + return $zone; + } + + /** + * Test route registration. + * + * @since 3.5.0 + */ + public function test_register_routes() { + $routes = $this->server->get_routes(); + $this->assertArrayHasKey( '/wc/v3/shipping/zones', $routes ); + $this->assertArrayHasKey( '/wc/v3/shipping/zones/(?P[\d]+)', $routes ); + $this->assertArrayHasKey( '/wc/v3/shipping/zones/(?P[\d]+)/locations', $routes ); + $this->assertArrayHasKey( '/wc/v3/shipping/zones/(?P[\d]+)/methods', $routes ); + $this->assertArrayHasKey( '/wc/v3/shipping/zones/(?P[\d]+)/methods/(?P[\d]+)', $routes ); + } + + /** + * Test getting all Shipping Zones. + * + * @since 3.5.0 + */ + public function test_get_zones() { + wp_set_current_user( $this->user ); + + // "Rest of the World" zone exists by default + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/shipping/zones' ) ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( count( $data ), 1 ); + $this->assertContains( + array( + 'id' => $data[0]['id'], + 'name' => 'Locations not covered by your other zones', + 'order' => 0, + '_links' => array( + 'self' => array( + array( + 'href' => rest_url( '/wc/v3/shipping/zones/' . $data[0]['id'] ), + ), + ), + 'collection' => array( + array( + 'href' => rest_url( '/wc/v3/shipping/zones' ), + ), + ), + 'describedby' => array( + array( + 'href' => rest_url( '/wc/v3/shipping/zones/' . $data[0]['id'] . '/locations' ), + ), + ), + ), + ), + $data + ); + + // Create a zone and make sure it's in the response + $this->create_shipping_zone( 'Zone 1' ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/shipping/zones' ) ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( count( $data ), 2 ); + $this->assertContains( + array( + 'id' => $data[1]['id'], + 'name' => 'Zone 1', + 'order' => 0, + '_links' => array( + 'self' => array( + array( + 'href' => rest_url( '/wc/v3/shipping/zones/' . $data[1]['id'] ), + ), + ), + 'collection' => array( + array( + 'href' => rest_url( '/wc/v3/shipping/zones' ), + ), + ), + 'describedby' => array( + array( + 'href' => rest_url( '/wc/v3/shipping/zones/' . $data[1]['id'] . '/locations' ), + ), + ), + ), + ), + $data + ); + } + + /** + * Test /shipping/zones without valid permissions/creds. + * + * @since 3.5.0 + */ + public function test_get_shipping_zones_without_permission() { + wp_set_current_user( 0 ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/shipping/zones' ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test /shipping/zones while Shipping is disabled in WooCommerce. + * + * @since 3.5.0 + */ + public function test_get_shipping_zones_disabled_shipping() { + wp_set_current_user( $this->user ); + + add_filter( 'wc_shipping_enabled', '__return_false' ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/shipping/zones' ) ); + $this->assertEquals( 404, $response->get_status() ); + + remove_filter( 'wc_shipping_enabled', '__return_false' ); + } + + /** + * Test Shipping Zone schema. + * + * @since 3.5.0 + */ + public function test_get_shipping_zone_schema() { + $request = new WP_REST_Request( 'OPTIONS', '/wc/v3/shipping/zones' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $properties = $data['schema']['properties']; + $this->assertEquals( 3, count( $properties ) ); + $this->assertArrayHasKey( 'id', $properties ); + $this->assertTrue( $properties['id']['readonly'] ); + $this->assertArrayHasKey( 'name', $properties ); + $this->assertArrayHasKey( 'order', $properties ); + } + + /** + * Test Shipping Zone create endpoint. + * + * @since 3.5.0 + */ + public function test_create_shipping_zone() { + wp_set_current_user( $this->user ); + + $request = new WP_REST_Request( 'POST', '/wc/v3/shipping/zones' ); + $request->set_body_params( + array( + 'name' => 'Test Zone', + 'order' => 1, + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 201, $response->get_status() ); + $this->assertEquals( + array( + 'id' => $data['id'], + 'name' => 'Test Zone', + 'order' => 1, + '_links' => array( + 'self' => array( + array( + 'href' => rest_url( '/wc/v3/shipping/zones/' . $data['id'] ), + ), + ), + 'collection' => array( + array( + 'href' => rest_url( '/wc/v3/shipping/zones' ), + ), + ), + 'describedby' => array( + array( + 'href' => rest_url( '/wc/v3/shipping/zones/' . $data['id'] . '/locations' ), + ), + ), + ), + ), + $data + ); + } + + /** + * Test Shipping Zone create endpoint. + * + * @since 3.5.0 + */ + public function test_create_shipping_zone_without_permission() { + wp_set_current_user( 0 ); + + $request = new WP_REST_Request( 'POST', '/wc/v3/shipping/zones' ); + $request->set_body_params( + array( + 'name' => 'Test Zone', + 'order' => 1, + ) + ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test Shipping Zone update endpoint. + * + * @since 3.5.0 + */ + public function test_update_shipping_zone() { + wp_set_current_user( $this->user ); + + $zone = $this->create_shipping_zone( 'Test Zone' ); + + $request = new WP_REST_Request( 'PUT', '/wc/v3/shipping/zones/' . $zone->get_id() ); + $request->set_body_params( + array( + 'name' => 'Zone Test', + 'order' => 2, + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( + array( + 'id' => $zone->get_id(), + 'name' => 'Zone Test', + 'order' => 2, + '_links' => array( + 'self' => array( + array( + 'href' => rest_url( '/wc/v3/shipping/zones/' . $zone->get_id() ), + ), + ), + 'collection' => array( + array( + 'href' => rest_url( '/wc/v3/shipping/zones' ), + ), + ), + 'describedby' => array( + array( + 'href' => rest_url( '/wc/v3/shipping/zones/' . $zone->get_id() . '/locations' ), + ), + ), + ), + ), + $data + ); + } + + /** + * Test Shipping Zone update endpoint with a bad zone ID. + * + * @since 3.5.0 + */ + public function test_update_shipping_zone_invalid_id() { + wp_set_current_user( $this->user ); + + $request = new WP_REST_Request( 'PUT', '/wc/v3/shipping/zones/555555' ); + $request->set_body_params( + array( + 'name' => 'Zone Test', + 'order' => 2, + ) + ); + $response = $this->server->dispatch( $request ); + + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * Test Shipping Zone delete endpoint. + * + * @since 3.5.0 + */ + public function test_delete_shipping_zone() { + wp_set_current_user( $this->user ); + $zone = $this->create_shipping_zone( 'Zone 1' ); + + $request = new WP_REST_Request( 'DELETE', '/wc/v3/shipping/zones/' . $zone->get_id() ); + $request->set_param( 'force', true ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + } + + /** + * Test Shipping Zone delete endpoint without permissions. + * + * @since 3.5.0 + */ + public function test_delete_shipping_zone_without_permission() { + wp_set_current_user( 0 ); + $zone = $this->create_shipping_zone( 'Zone 1' ); + + $request = new WP_REST_Request( 'DELETE', '/wc/v3/shipping/zones/' . $zone->get_id() ); + $request->set_param( 'force', true ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test Shipping Zone delete endpoint with a bad zone ID. + * + * @since 3.5.0 + */ + public function test_delete_shipping_zone_invalid_id() { + wp_set_current_user( $this->user ); + $request = new WP_REST_Request( 'DELETE', '/wc/v3/shipping/zones/555555' ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * Test getting a single Shipping Zone. + * + * @since 3.5.0 + */ + public function test_get_single_shipping_zone() { + wp_set_current_user( $this->user ); + + $zone = $this->create_shipping_zone( 'Test Zone' ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/shipping/zones/' . $zone->get_id() ) ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( + array( + 'id' => $zone->get_id(), + 'name' => 'Test Zone', + 'order' => 0, + '_links' => array( + 'self' => array( + array( + 'href' => rest_url( '/wc/v3/shipping/zones/' . $zone->get_id() ), + ), + ), + 'collection' => array( + array( + 'href' => rest_url( '/wc/v3/shipping/zones' ), + ), + ), + 'describedby' => array( + array( + 'href' => rest_url( '/wc/v3/shipping/zones/' . $zone->get_id() . '/locations' ), + ), + ), + ), + ), + $data + ); + } + + /** + * Test getting a single Shipping Zone with a bad zone ID. + * + * @since 3.5.0 + */ + public function test_get_single_shipping_zone_invalid_id() { + wp_set_current_user( $this->user ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/shipping/zones/1' ) ); + + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * Test getting Shipping Zone Locations. + * + * @since 3.5.0 + */ + public function test_get_locations() { + wp_set_current_user( $this->user ); + + // Create a zone + $zone = $this->create_shipping_zone( + 'Zone 1', + 0, + array( + array( + 'code' => 'US', + 'type' => 'country', + ), + ) + ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/shipping/zones/' . $zone->get_id() . '/locations' ) ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( count( $data ), 1 ); + $this->assertEquals( + array( + array( + 'code' => 'US', + 'type' => 'country', + '_links' => array( + 'collection' => array( + array( + 'href' => rest_url( '/wc/v3/shipping/zones/' . $zone->get_id() . '/locations' ), + ), + ), + 'describes' => array( + array( + 'href' => rest_url( '/wc/v3/shipping/zones/' . $zone->get_id() ), + ), + ), + ), + ), + ), + $data + ); + } + + /** + * Test getting Shipping Zone Locations with a bad zone ID. + * + * @since 3.5.0 + */ + public function test_get_locations_invalid_id() { + wp_set_current_user( $this->user ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/shipping/zones/1/locations' ) ); + + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * Test Shipping Zone Locations update endpoint. + * + * @since 3.5.0 + */ + public function test_update_locations() { + wp_set_current_user( $this->user ); + + $zone = $this->create_shipping_zone( 'Test Zone' ); + + $request = new WP_REST_Request( 'PUT', '/wc/v3/shipping/zones/' . $zone->get_id() . '/locations' ); + $request->add_header( 'Content-Type', 'application/json' ); + $request->set_body( + json_encode( + array( + array( + 'code' => 'UK', + 'type' => 'country', + ), + array( + 'code' => 'US', // test that locations missing "type" treated as country. + ), + array( + 'code' => 'SW1A0AA', + 'type' => 'postcode', + ), + array( + 'type' => 'continent', // test that locations missing "code" aren't saved + ), + ) + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 3, count( $data ) ); + $this->assertEquals( + array( + array( + 'code' => 'UK', + 'type' => 'country', + '_links' => array( + 'collection' => array( + array( + 'href' => rest_url( '/wc/v3/shipping/zones/' . $zone->get_id() . '/locations' ), + ), + ), + 'describes' => array( + array( + 'href' => rest_url( '/wc/v3/shipping/zones/' . $zone->get_id() ), + ), + ), + ), + ), + array( + 'code' => 'US', + 'type' => 'country', + '_links' => array( + 'collection' => array( + array( + 'href' => rest_url( '/wc/v3/shipping/zones/' . $zone->get_id() . '/locations' ), + ), + ), + 'describes' => array( + array( + 'href' => rest_url( '/wc/v3/shipping/zones/' . $zone->get_id() ), + ), + ), + ), + ), + array( + 'code' => 'SW1A0AA', + 'type' => 'postcode', + '_links' => array( + 'collection' => array( + array( + 'href' => rest_url( '/wc/v3/shipping/zones/' . $zone->get_id() . '/locations' ), + ), + ), + 'describes' => array( + array( + 'href' => rest_url( '/wc/v3/shipping/zones/' . $zone->get_id() ), + ), + ), + ), + ), + ), + $data + ); + } + + /** + * Test updating Shipping Zone Locations with a bad zone ID. + * + * @since 3.5.0 + */ + public function test_update_locations_invalid_id() { + wp_set_current_user( $this->user ); + + $response = $this->server->dispatch( new WP_REST_Request( 'PUT', '/wc/v3/shipping/zones/1/locations' ) ); + + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * Test getting all Shipping Zone Methods and getting a single Shipping Zone Method. + * + * @since 3.5.0 + */ + public function test_get_methods() { + wp_set_current_user( $this->user ); + + // Create a shipping method and make sure it's in the response + $zone = $this->create_shipping_zone( 'Zone 1' ); + $instance_id = $zone->add_shipping_method( 'flat_rate' ); + $methods = $zone->get_shipping_methods(); + $method = $methods[ $instance_id ]; + + $settings = array(); + $method->init_instance_settings(); + foreach ( $method->get_instance_form_fields() as $id => $field ) { + $data = array( + 'id' => $id, + 'label' => $field['title'], + 'description' => ( empty( $field['description'] ) ? '' : $field['description'] ), + 'type' => $field['type'], + 'value' => $method->instance_settings[ $id ], + 'default' => ( empty( $field['default'] ) ? '' : $field['default'] ), + 'tip' => ( empty( $field['description'] ) ? '' : $field['description'] ), + 'placeholder' => ( empty( $field['placeholder'] ) ? '' : $field['placeholder'] ), + ); + if ( ! empty( $field['options'] ) ) { + $data['options'] = $field['options']; + } + $settings[ $id ] = $data; + } + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/shipping/zones/' . $zone->get_id() . '/methods' ) ); + $data = $response->get_data(); + $expected = array( + 'id' => $instance_id, + 'instance_id' => $instance_id, + 'title' => $method->instance_settings['title'], + 'order' => $method->method_order, + 'enabled' => ( 'yes' === $method->enabled ), + 'method_id' => $method->id, + 'method_title' => $method->method_title, + 'method_description' => $method->method_description, + 'settings' => $settings, + '_links' => array( + 'self' => array( + array( + 'href' => rest_url( '/wc/v3/shipping/zones/' . $zone->get_id() . '/methods/' . $instance_id ), + ), + ), + 'collection' => array( + array( + 'href' => rest_url( '/wc/v3/shipping/zones/' . $zone->get_id() . '/methods' ), + ), + ), + 'describes' => array( + array( + 'href' => rest_url( '/wc/v3/shipping/zones/' . $zone->get_id() ), + ), + ), + ), + ); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( count( $data ), 1 ); + $this->assertContains( $expected, $data ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/shipping/zones/' . $zone->get_id() . '/methods/' . $instance_id ) ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( $expected, $data ); + } + + /** + * Test getting all Shipping Zone Methods with a bad zone ID. + * + * @since 3.5.0 + */ + public function test_get_methods_invalid_zone_id() { + wp_set_current_user( $this->user ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/shipping/zones/1/methods' ) ); + + $this->assertEquals( 404, $response->get_status() ); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/shipping/zones/1/methods/1' ) ); + + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * Test getting a single Shipping Zone Method with a bad ID. + * + * @since 3.5.0 + */ + public function test_get_methods_invalid_method_id() { + wp_set_current_user( $this->user ); + + $zone = $this->create_shipping_zone( 'Zone 1' ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/shipping/zones/' . $zone->get_id() . '/methods/1' ) ); + + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * Test updating a Shipping Zone Method. + * + * @since 3.5.0 + */ + public function test_update_methods() { + wp_set_current_user( $this->user ); + + $zone = $this->create_shipping_zone( 'Zone 1' ); + $instance_id = $zone->add_shipping_method( 'flat_rate' ); + $methods = $zone->get_shipping_methods(); + $method = $methods[ $instance_id ]; + + // Test defaults + $request = new WP_REST_Request( 'GET', '/wc/v3/shipping/zones/' . $zone->get_id() . '/methods/' . $instance_id ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertArrayHasKey( 'title', $data['settings'] ); + $this->assertEquals( 'Flat rate', $data['settings']['title']['value'] ); + $this->assertArrayHasKey( 'tax_status', $data['settings'] ); + $this->assertEquals( 'taxable', $data['settings']['tax_status']['value'] ); + $this->assertArrayHasKey( 'cost', $data['settings'] ); + $this->assertEquals( '0', $data['settings']['cost']['value'] ); + + // Update a single value + $request = new WP_REST_Request( 'POST', '/wc/v3/shipping/zones/' . $zone->get_id() . '/methods/' . $instance_id ); + $request->set_body_params( + array( + 'settings' => array( + 'cost' => 5, + ), + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertArrayHasKey( 'title', $data['settings'] ); + $this->assertEquals( 'Flat rate', $data['settings']['title']['value'] ); + $this->assertArrayHasKey( 'tax_status', $data['settings'] ); + $this->assertEquals( 'taxable', $data['settings']['tax_status']['value'] ); + $this->assertArrayHasKey( 'cost', $data['settings'] ); + $this->assertEquals( '5', $data['settings']['cost']['value'] ); + + // Test multiple settings + $request = new WP_REST_Request( 'POST', '/wc/v3/shipping/zones/' . $zone->get_id() . '/methods/' . $instance_id ); + $request->set_body_params( + array( + 'settings' => array( + 'cost' => 10, + 'tax_status' => 'none', + ), + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertArrayHasKey( 'title', $data['settings'] ); + $this->assertEquals( 'Flat rate', $data['settings']['title']['value'] ); + $this->assertArrayHasKey( 'tax_status', $data['settings'] ); + $this->assertEquals( 'none', $data['settings']['tax_status']['value'] ); + $this->assertArrayHasKey( 'cost', $data['settings'] ); + $this->assertEquals( '10', $data['settings']['cost']['value'] ); + + // Test bogus + $request = new WP_REST_Request( 'POST', '/wc/v3/shipping/zones/' . $zone->get_id() . '/methods/' . $instance_id ); + $request->set_body_params( + array( + 'settings' => array( + 'cost' => 10, + 'tax_status' => 'this_is_not_a_valid_option', + ), + ) + ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 400, $response->get_status() ); + + // Test other parameters + $this->assertTrue( $data['enabled'] ); + $this->assertEquals( 1, $data['order'] ); + + $request = new WP_REST_Request( 'POST', '/wc/v3/shipping/zones/' . $zone->get_id() . '/methods/' . $instance_id ); + $request->set_body_params( + array( + 'enabled' => false, + 'order' => 2, + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertFalse( $data['enabled'] ); + $this->assertEquals( 2, $data['order'] ); + $this->assertArrayHasKey( 'cost', $data['settings'] ); + $this->assertEquals( '10', $data['settings']['cost']['value'] ); + } + + /** + * Test creating a Shipping Zone Method. + * + * @since 3.5.0 + */ + public function test_create_method() { + wp_set_current_user( $this->user ); + $zone = $this->create_shipping_zone( 'Zone 1' ); + $request = new WP_REST_Request( 'POST', '/wc/v3/shipping/zones/' . $zone->get_id() . '/methods' ); + $request->set_body_params( + array( + 'method_id' => 'flat_rate', + 'enabled' => false, + 'order' => 2, + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertFalse( $data['enabled'] ); + $this->assertEquals( 2, $data['order'] ); + $this->assertArrayHasKey( 'cost', $data['settings'] ); + $this->assertEquals( '0', $data['settings']['cost']['value'] ); + } + + /** + * Test deleting a Shipping Zone Method. + * + * @since 3.5.0 + */ + public function test_delete_method() { + wp_set_current_user( $this->user ); + $zone = $this->create_shipping_zone( 'Zone 1' ); + $instance_id = $zone->add_shipping_method( 'flat_rate' ); + $methods = $zone->get_shipping_methods(); + $method = $methods[ $instance_id ]; + $request = new WP_REST_Request( 'DELETE', '/wc/v3/shipping/zones/' . $zone->get_id() . '/methods/' . $instance_id ); + $request->set_param( 'force', true ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 200, $response->get_status() ); + } +} diff --git a/tests/legacy/unit-tests/rest-api/Tests/Version3/system-status.php b/tests/legacy/unit-tests/rest-api/Tests/Version3/system-status.php new file mode 100644 index 00000000000..7aa77fbde02 --- /dev/null +++ b/tests/legacy/unit-tests/rest-api/Tests/Version3/system-status.php @@ -0,0 +1,491 @@ +user->create( + array( + 'role' => 'administrator', + ) + ); + } + + /** + * Setup our test server. + */ + public function setUp() { + parent::setUp(); + + wp_set_current_user( self::$user ); + + $this->endpoint = new WC_REST_System_Status_Controller(); + + // Callback used by WP_HTTP_TestCase to decide whether to perform HTTP requests or to provide a mocked response. + $this->http_responder = array( $this, 'mock_http_responses' ); + } + + /** + * Test route registration. + */ + public function test_register_routes() { + $routes = $this->server->get_routes(); + $this->assertArrayHasKey( '/wc/v3/system_status', $routes ); + $this->assertArrayHasKey( '/wc/v3/system_status/tools', $routes ); + $this->assertArrayHasKey( '/wc/v3/system_status/tools/(?P[\w-]+)', $routes ); + } + + /** + * Test to make sure system status cannot be accessed without valid creds + * + * @since 3.5.0 + */ + public function test_get_system_status_info_without_permission() { + wp_set_current_user( 0 ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/system_status' ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test to make sure root properties are present. + * (environment, theme, database, etc). + * + * @since 3.5.0 + */ + public function test_get_system_status_info_returns_root_properties() { + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/system_status' ) ); + $data = $response->get_data(); + + $this->assertArrayHasKey( 'environment', $data ); + $this->assertArrayHasKey( 'database', $data ); + $this->assertArrayHasKey( 'active_plugins', $data ); + $this->assertArrayHasKey( 'theme', $data ); + $this->assertArrayHasKey( 'settings', $data ); + $this->assertArrayHasKey( 'security', $data ); + $this->assertArrayHasKey( 'pages', $data ); + } + + /** + * Test to make sure environment response is correct. + * + * @since 3.5.0 + */ + public function test_get_system_status_info_environment() { + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/system_status' ) ); + $data = $response->get_data(); + $environment = (array) $data['environment']; + + // Make sure all expected data is present. + $this->assertEquals( 32, count( $environment ) ); + + // Test some responses to make sure they match up. + $this->assertEquals( get_option( 'home' ), $environment['home_url'] ); + $this->assertEquals( get_option( 'siteurl' ), $environment['site_url'] ); + $this->assertEquals( WC()->version, $environment['version'] ); + } + + /** + * Test to make sure that it is possible to filter + * the environment fields returned in the response. + */ + public function test_get_system_status_info_environment_filtered_by_field() { + if ( ! version_compare( get_bloginfo( 'version' ), '5.3', '>=' ) ) { + $this->markTestSkipped( 'Skipping because nested property support was introduced in 5.3.' ); + return; + } + $expected_data = array( + 'environment' => array( + 'version' => WC()->version + ) + ); + + $request = new WP_REST_Request( 'GET', '/wc/v3/system_status' ); + $request->set_query_params( array( '_fields' => 'environment.version' ) ); + + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( $expected_data, $data ); + } + + /** + * Test to make sure database response is correct. + * + * @since 3.5.0 + */ + public function test_get_system_status_info_database() { + global $wpdb; + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/system_status' ) ); + $data = $response->get_data(); + $database = (array) $data['database']; + + $this->assertEquals( get_option( 'woocommerce_db_version' ), $database['wc_database_version'] ); + $this->assertEquals( $wpdb->prefix, $database['database_prefix'] ); + $this->assertArrayHasKey( 'woocommerce', $database['database_tables'], wc_print_r( $database, true ) ); + $this->assertArrayHasKey( $wpdb->prefix . 'woocommerce_payment_tokens', $database['database_tables']['woocommerce'], wc_print_r( $database, true ) ); + } + + /** + * Test to make sure active plugins response is correct. + * + * @since 3.5.0 + */ + public function test_get_system_status_info_active_plugins() { + $actual_plugins = array( 'hello.php' ); + update_option( 'active_plugins', $actual_plugins ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/system_status' ) ); + update_option( 'active_plugins', array() ); + + $data = $response->get_data(); + $plugins = (array) $data['active_plugins']; + + $this->assertEquals( 1, count( $plugins ) ); + $this->assertEquals( 'Hello Dolly', $plugins[0]['name'] ); + } + + /** + * Test to make sure theme response is correct. + * + * @since 3.5.0 + */ + public function test_get_system_status_info_theme() { + $active_theme = wp_get_theme(); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/system_status' ) ); + $data = $response->get_data(); + $theme = (array) $data['theme']; + + $this->assertEquals( 13, count( $theme ) ); + $this->assertEquals( $active_theme->Name, $theme['name'] ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.NotSnakeCaseMemberVar + } + + /** + * Test to make sure settings response is correct. + * + * @since 3.5.0 + */ + public function test_get_system_status_info_settings() { + $term_response = array(); + $terms = get_terms( 'product_type', array( 'hide_empty' => 0 ) ); + foreach ( $terms as $term ) { + $term_response[ $term->slug ] = strtolower( $term->name ); + } + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/system_status' ) ); + $data = $response->get_data(); + $settings = (array) $data['settings']; + + $this->assertEquals( 12, count( $settings ) ); + $this->assertEquals( ( 'yes' === get_option( 'woocommerce_api_enabled' ) ), $settings['api_enabled'] ); + $this->assertEquals( get_woocommerce_currency(), $settings['currency'] ); + $this->assertEquals( $term_response, $settings['taxonomies'] ); + } + + /** + * Test to make sure security response is correct. + * + * @since 3.5.0 + */ + public function test_get_system_status_info_security() { + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/system_status' ) ); + $data = $response->get_data(); + $settings = (array) $data['security']; + + $this->assertEquals( 2, count( $settings ) ); + $this->assertEquals( 'https' === substr( wc_get_page_permalink( 'shop' ), 0, 5 ), $settings['secure_connection'] ); + $this->assertEquals( ! ( defined( 'WP_DEBUG' ) && defined( 'WP_DEBUG_DISPLAY' ) && WP_DEBUG && WP_DEBUG_DISPLAY ) || 0 === intval( ini_get( 'display_errors' ) ), $settings['hide_errors'] ); + } + + /** + * Test to make sure pages response is correct. + * + * @since 3.5.0 + */ + public function test_get_system_status_info_pages() { + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/system_status' ) ); + $data = $response->get_data(); + $pages = $data['pages']; + $this->assertEquals( 5, count( $pages ) ); + } + + /** + * Test system status schema. + * + * @since 3.5.0 + */ + public function test_system_status_schema() { + $request = new WP_REST_Request( 'OPTIONS', '/wc/v3/system_status' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $properties = $data['schema']['properties']; + $this->assertEquals( 10, count( $properties ) ); + $this->assertArrayHasKey( 'environment', $properties ); + $this->assertArrayHasKey( 'database', $properties ); + $this->assertArrayHasKey( 'active_plugins', $properties ); + $this->assertArrayHasKey( 'theme', $properties ); + $this->assertArrayHasKey( 'settings', $properties ); + $this->assertArrayHasKey( 'security', $properties ); + $this->assertArrayHasKey( 'pages', $properties ); + } + + /** + * Test to make sure get_items (all tools) response is correct. + * + * @since 3.5.0 + */ + public function test_get_system_tools() { + $tools_controller = new WC_REST_System_Status_Tools_Controller(); + $raw_tools = $tools_controller->get_tools(); + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/system_status/tools' ) ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( count( $raw_tools ), count( $data ) ); + $this->assertContains( + array( + 'id' => 'regenerate_thumbnails', + 'name' => 'Regenerate shop thumbnails', + 'action' => 'Regenerate', + 'description' => 'This will regenerate all shop thumbnails to match your theme and/or image settings.', + '_links' => array( + 'item' => array( + array( + 'href' => rest_url( '/wc/v3/system_status/tools/regenerate_thumbnails' ), + 'embeddable' => 1, + ), + ), + ), + ), + $data + ); + + $query_params = array( + '_fields' => 'id,name,nonexisting', + ); + $request = new WP_REST_Request( 'GET', '/wc/v3/system_status/tools' ); + $request->set_query_params( $query_params ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( count( $raw_tools ), count( $data ) ); + $this->assertContains( + array( + 'id' => 'regenerate_thumbnails', + 'name' => 'Regenerate shop thumbnails', + ), + $data + ); + foreach ( $data as $item ) { + // Fields that are not requested are not returned in response. + $this->assertArrayNotHasKey( 'action', $item ); + $this->assertArrayNotHasKey( 'description', $item ); + // Links are part of data in collections, so excluded if not explicitly requested. + $this->assertArrayNotHasKey( '_links', $item ); + // Non existing field is ignored. + $this->assertArrayNotHasKey( 'nonexisting', $item ); + } + + // Links are part of data, not links in collections. + $links = $response->get_links(); + $this->assertEquals( 0, count( $links ) ); + } + + /** + * Test to make sure system status tools cannot be accessed without valid creds + * + * @since 3.5.0 + */ + public function test_get_system_status_tools_without_permission() { + wp_set_current_user( 0 ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/system_status/tools' ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test to make sure we can load a single tool correctly. + * + * @since 3.5.0 + */ + public function test_get_system_tool() { + $tools_controller = new WC_REST_System_Status_Tools_Controller(); + $raw_tools = $tools_controller->get_tools(); + $raw_tool = $raw_tools['recount_terms']; + + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/system_status/tools/recount_terms' ) ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + + $this->assertEquals( 'recount_terms', $data['id'] ); + $this->assertEquals( 'Term counts', $data['name'] ); + $this->assertEquals( 'Recount terms', $data['action'] ); + $this->assertEquals( 'This tool will recount product terms - useful when changing your settings in a way which hides products from the catalog.', $data['description'] ); + + // Test for _fields query parameter. + $query_params = array( + '_fields' => 'id,name,nonexisting', + ); + $request = new WP_REST_Request( 'GET', '/wc/v3/system_status/tools/recount_terms' ); + $request->set_query_params( $query_params ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + + $this->assertEquals( 'recount_terms', $data['id'] ); + $this->assertEquals( 'Term counts', $data['name'] ); + $this->assertArrayNotHasKey( 'action', $data ); + $this->assertArrayNotHasKey( 'description', $data ); + // Links are part of links, not data in single items. + $this->assertArrayNotHasKey( '_links', $data ); + + // Links are part of links, not data in single item response. + $links = $response->get_links(); + $this->assertEquals( 1, count( $links ) ); + } + + /** + * Test to make sure a single system status toolscannot be accessed without valid creds. + * + * @since 3.5.0 + */ + public function test_get_system_status_tool_without_permission() { + wp_set_current_user( 0 ); + $response = $this->server->dispatch( new WP_REST_Request( 'GET', '/wc/v3/system_status/tools/recount_terms' ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test to make sure we can RUN a tool correctly. + * + * @since 3.5.0 + */ + public function test_execute_system_tool() { + $tools_controller = new WC_REST_System_Status_Tools_Controller(); + $raw_tools = $tools_controller->get_tools(); + $raw_tool = $raw_tools['recount_terms']; + + $response = $this->server->dispatch( new WP_REST_Request( 'POST', '/wc/v3/system_status/tools/recount_terms' ) ); + $data = $response->get_data(); + + $this->assertEquals( 'recount_terms', $data['id'] ); + $this->assertEquals( 'Term counts', $data['name'] ); + $this->assertEquals( 'Recount terms', $data['action'] ); + $this->assertEquals( 'This tool will recount product terms - useful when changing your settings in a way which hides products from the catalog.', $data['description'] ); + $this->assertTrue( $data['success'] ); + $this->assertEquals( 1, did_action( 'woocommerce_rest_insert_system_status_tool' ) ); + + $response = $this->server->dispatch( new WP_REST_Request( 'POST', '/wc/v3/system_status/tools/not_a_real_tool' ) ); + $this->assertEquals( 404, $response->get_status() ); + + // Test _fields for execute system tool request. + $query_params = array( + '_fields' => 'id,success,nonexisting', + ); + $request = new WP_REST_Request( 'PUT', '/wc/v3/system_status/tools/recount_terms' ); + $request->set_query_params( $query_params ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 'recount_terms', $data['id'] ); + $this->assertTrue( $data['success'] ); + + // Fields that are not requested are not returned in response. + $this->assertArrayNotHasKey( 'action', $data ); + $this->assertArrayNotHasKey( 'name', $data ); + $this->assertArrayNotHasKey( 'description', $data ); + // Links are part of links, not data in single item response. + $this->assertArrayNotHasKey( '_links', $data ); + // Non existing field is ignored. + $this->assertArrayNotHasKey( 'nonexisting', $data ); + + // Links are part of links, not data in single item response. + $links = $response->get_links(); + $this->assertEquals( 1, count( $links ) ); + } + + /** + * Test to make sure a tool cannot be run without valid creds. + * + * @since 3.5.0 + */ + public function test_execute_system_status_tool_without_permission() { + wp_set_current_user( 0 ); + $response = $this->server->dispatch( new WP_REST_Request( 'POST', '/wc/v3/system_status/tools/recount_terms' ) ); + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test system status schema. + * + * @since 3.5.0 + */ + public function test_system_status_tool_schema() { + $request = new WP_REST_Request( 'OPTIONS', '/wc/v3/system_status/tools' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $properties = $data['schema']['properties']; + + $this->assertEquals( 6, count( $properties ) ); + $this->assertArrayHasKey( 'id', $properties ); + $this->assertArrayHasKey( 'name', $properties ); + $this->assertArrayHasKey( 'action', $properties ); + $this->assertArrayHasKey( 'description', $properties ); + $this->assertArrayHasKey( 'success', $properties ); + $this->assertArrayHasKey( 'message', $properties ); + } + + /** + * Provides a mocked response for external requests performed by WC_REST_System_Status_Controller. + * This way it is not necessary to perform a regular request to an external server which would + * significantly slow down the tests. + * + * This function is called by WP_HTTP_TestCase::http_request_listner(). + * + * @param array $request Request arguments. + * @param string $url URL of the request. + * + * @return array|false mocked response or false to let WP perform a regular request. + */ + protected function mock_http_responses( $request, $url ) { + $mocked_response = false; + + if ( in_array( $url, array( 'https://www.paypal.com/cgi-bin/webscr', 'https://woocommerce.com/wc-api/product-key-api?request=ping&network=0' ), true ) ) { + $mocked_response = array( + 'response' => array( 'code' => 200 ), + ); + } elseif ( 'https://api.wordpress.org/themes/info/1.0/' === $url ) { + $mocked_response = array( + 'body' => 'O:8:"stdClass":12:{s:4:"name";s:7:"Default";s:4:"slug";s:7:"default";s:7:"version";s:5:"1.7.2";s:11:"preview_url";s:29:"https://wp-themes.com/default";s:6:"author";s:15:"wordpressdotorg";s:14:"screenshot_url";s:61:"//ts.w.org/wp-content/themes/default/screenshot.png?ver=1.7.2";s:6:"rating";d:100;s:11:"num_ratings";s:1:"3";s:10:"downloaded";i:296618;s:12:"last_updated";s:10:"2010-06-14";s:8:"homepage";s:37:"https://wordpress.org/themes/default/";s:13:"download_link";s:55:"https://downloads.wordpress.org/theme/default.1.7.2.zip";}', + 'response' => array( 'code' => 200 ), + ); + } + + return $mocked_response; + } +} diff --git a/tests/legacy/unit-tests/rest-api/data/Dr1Bczxq4q.png b/tests/legacy/unit-tests/rest-api/data/Dr1Bczxq4q.png new file mode 100644 index 00000000000..4a2bb205fe4 Binary files /dev/null and b/tests/legacy/unit-tests/rest-api/data/Dr1Bczxq4q.png differ diff --git a/tests/legacy/unit-tests/rest-api/data/file.txt b/tests/legacy/unit-tests/rest-api/data/file.txt new file mode 100644 index 00000000000..078595600bd --- /dev/null +++ b/tests/legacy/unit-tests/rest-api/data/file.txt @@ -0,0 +1 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi porta purus dolor, malesuada consequat sem blandit eu. Pellentesque tempor elementum maximus. Phasellus efficitur turpis ante, ac egestas nisl efficitur sit amet. Vestibulum tortor erat, efficitur id nulla eget, malesuada lobortis augue. Ut eleifend ante at pharetra gravida. Quisque suscipit efficitur mauris, quis bibendum massa efficitur id. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam vel luctus nisl, ac bibendum nulla. Sed faucibus nibh consectetur urna hendrerit sagittis. Sed bibendum felis in tortor elementum, a porttitor lacus volutpat. Etiam facilisis congue lacinia. Quisque vitae ultricies mauris, non ornare orci. diff --git a/tests/legacy/unit-tests/templates/functions.php b/tests/legacy/unit-tests/templates/functions.php index 5b1bbb49001..5ef1c017291 100644 --- a/tests/legacy/unit-tests/templates/functions.php +++ b/tests/legacy/unit-tests/templates/functions.php @@ -2,7 +2,7 @@ /** * Test template funcitons. * - * @package WooCommerce/Tests/Templates + * @package WooCommerce\Tests\Templates * @since 3.4.0 */ diff --git a/tests/legacy/unit-tests/widgets/class-wc-tests-widget.php b/tests/legacy/unit-tests/widgets/class-wc-tests-widget.php index 694fe754922..2f3bb9924d6 100644 --- a/tests/legacy/unit-tests/widgets/class-wc-tests-widget.php +++ b/tests/legacy/unit-tests/widgets/class-wc-tests-widget.php @@ -2,7 +2,7 @@ /** * Testing WC_Widget functionality. * - * @package WooCommerce/Tests/Widgets + * @package WooCommerce\Tests\Widgets */ /** diff --git a/tests/php/includes/class-wc-ajax-test.php b/tests/php/includes/class-wc-ajax-test.php index 93904242f0a..afce0cb159a 100644 --- a/tests/php/includes/class-wc-ajax-test.php +++ b/tests/php/includes/class-wc-ajax-test.php @@ -2,7 +2,7 @@ /** * Class WC_AJAX_Test file. * - * @package WooCommerce|Tests|WC_AJAX. + * @package WooCommerce\Tests\WC_AJAX. */ /** diff --git a/tests/php/includes/class-wc-post-data-test.php b/tests/php/includes/class-wc-post-data-test.php new file mode 100644 index 00000000000..3d3f7914325 --- /dev/null +++ b/tests/php/includes/class-wc-post-data-test.php @@ -0,0 +1,35 @@ +login_as_role( 'shop_manager' ); + $coupon = WC_Helper_Coupon::create_coupon( 'a&a' ); + $post_data = get_post( $coupon->get_id() ); + $this->assertEquals( 'a&a', $post_data->post_title ); + $coupon->delete( true ); + + $this->login_as_administrator(); + $coupon = WC_Helper_Coupon::create_coupon( 'b&b' ); + $post_data = get_post( $coupon->get_id() ); + $this->assertEquals( 'b&b', $post_data->post_title ); + $coupon->delete( true ); + + wp_set_current_user( 0 ); + $coupon = WC_Helper_Coupon::create_coupon( 'c&c' ); + $post_data = get_post( $coupon->get_id() ); + $this->assertEquals( 'c&c', $post_data->post_title ); + $coupon->delete( true ); + } +} diff --git a/tests/php/includes/class-wc-product-variable-test.php b/tests/php/includes/class-wc-product-variable-test.php index e80b5751a0f..28fc57529fe 100644 --- a/tests/php/includes/class-wc-product-variable-test.php +++ b/tests/php/includes/class-wc-product-variable-test.php @@ -12,7 +12,7 @@ class WC_Product_Variable_Test extends \WC_Unit_Test_Case { $variations = $product->get_available_variations(); - $this->assertIsArray( $variations[0] ); + $this->assertTrue( is_array( $variations[0] ) ); $this->assertEquals( 'DUMMY SKU VARIABLE SMALL', $variations[0]['sku'] ); } @@ -24,7 +24,7 @@ class WC_Product_Variable_Test extends \WC_Unit_Test_Case { $variations = $product->get_available_variations( 'array' ); - $this->assertIsArray( $variations[0] ); + $this->assertTrue( is_array( $variations[0] ) ); $this->assertEquals( 'DUMMY SKU VARIABLE SMALL', $variations[0]['sku'] ); } diff --git a/tests/php/includes/wc-formatting-functions-test.php b/tests/php/includes/wc-formatting-functions-test.php new file mode 100644 index 00000000000..93dfda3b314 --- /dev/null +++ b/tests/php/includes/wc-formatting-functions-test.php @@ -0,0 +1,20 @@ +assertEquals( 'DUMMYCOUPON', wc_sanitize_coupon_code( 'DUMMYCOUPON' ) ); + $this->assertEquals( 'a&a', wc_sanitize_coupon_code( 'a&a' ) ); + } +} diff --git a/tests/php/src/Internal/DependencyManagement/AbstractServiceProviderTest.php b/tests/php/src/Internal/DependencyManagement/AbstractServiceProviderTest.php index ebd72755b29..48c447efc1b 100644 --- a/tests/php/src/Internal/DependencyManagement/AbstractServiceProviderTest.php +++ b/tests/php/src/Internal/DependencyManagement/AbstractServiceProviderTest.php @@ -1,8 +1,6 @@