Merge branch 'master' into update/travis-git-depth

This commit is contained in:
Rodrigo Primo 2020-11-17 10:27:26 -03:00
commit e1856b961d
125 changed files with 7014 additions and 3779 deletions

View File

@ -1,35 +0,0 @@
engines:
phpcodesniffer:
enabled: true
config:
standard: "WordPress"
eslint:
enabled: true
scss-lint:
enabled: true
duplication:
enabled: true
config:
languages:
- php
- javascript
ratings:
paths:
- "includes/*"
exclude_paths:
- "tests/"
- "sample-data/"
- "i18n/"
- "includes/api/legacy/"
- "includes/libraries/"
- "includes/updates/"
- "includes/shipping/legacy-*"
- "includes/wc-deprecated-functions.php"
- "assets/js/accounting/"
- "assets/js/jquery-*"
- "assets/js/prettyPhoto/"
- "assets/js/round/"
- "assets/js/select2/"
- "assets/js/selectWoo/"
- "assets/js/stupidtable/"
- "assets/js/zeroclipboard/"

View File

@ -34,7 +34,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Update nightly tag
uses: richardsimko/github-tag-action@v1.0.4
uses: richardsimko/github-tag-action@v1.0.5
with:
tag_name: nightly
env:

2
.nvmrc
View File

@ -1 +1 @@
v10
v12

View File

@ -1,35 +0,0 @@
tools:
php_code_sniffer:
config:
standard: WordPress
sensiolabs_security_checker: true
external_code_coverage:
timeout: 2500
checks:
php:
avoid_closing_tag: false
avoid_superglobals: false
coding_standard:
name: WordPress
no_exit: false
no_global_keyword: false
one_class_per_file: false
psr2_class_declaration: false
psr2_control_structure_declaration: false
psr2_switch_declaration: false
variable_existence: false
verify_access_scope_valid: false
verify_argument_usable_as_reference: false
verify_property_names: false
filter:
excluded_paths:
- sample-data/
- i18n/
- includes/api/legacy/
- includes/legacy/
- includes/libraries/
- includes/shipping/legacy-*
- includes/updates/
- includes/vendor/
- includes/wc-deprecated-functions.php
- tests/

View File

@ -70,10 +70,9 @@ install:
fi
- composer install
- |
# Install WP Test suite, install PHPUnit globally:
# Install WP Test suite:
if [[ ! -z "$WP_VERSION" ]]; then
bash tests/bin/install.sh woocommerce_test root '' localhost $WP_VERSION
composer global require "phpunit/phpunit=6.5.*|7.5.*"
fi
script:
@ -90,10 +89,12 @@ branches:
- /^\d+\.\d+(\.\d+)?(-\S*)?$/
- /^release\//
# Composer 2.0.7 introduced a change that broke the jetpack autoloader in PHP 7.0 - 7.3.
before_install:
- composer self-update --1
- composer self-update 2.0.6
# Git clone depth
# By default Travis CI clones repositories to a depth of 50 commits. Using a depth of 1 makes this step a bit faster.
git:
depth: 1

View File

@ -6,7 +6,6 @@
<img src="https://img.shields.io/wordpress/plugin/dt/woocommerce.svg" alt="WordPress.org downloads">
<img src="https://img.shields.io/wordpress/plugin/r/woocommerce.svg" alt="WordPress.org rating">
<a href="https://travis-ci.com/woocommerce/woocommerce"><img src="https://travis-ci.com/woocommerce/woocommerce.svg?branch=master" alt="Build Status"></a>
<a href="https://scrutinizer-ci.com/g/woocommerce/woocommerce/?branch=master"><img src="https://scrutinizer-ci.com/g/woocommerce/woocommerce/badges/quality-score.png?b=master" alt="Scrutinizer Code Quality"></a>
<a href="https://codecov.io/gh/woocommerce/woocommerce"><img src="https://codecov.io/gh/woocommerce/woocommerce/branch/master/graph/badge.svg" alt="codecov"></a>
</p>

View File

@ -360,7 +360,7 @@
}
.addons-button-solid {
background-color: #955a89;
background-color:#674399;
color: #fff;
}
@ -379,6 +379,16 @@
opacity: 0.8;
}
.addons-button-outline-purple {
border: 1px solid #674399;
color: #674399;
}
.addons-button-outline-purple:hover {
color: #674399;
opacity: 0.8;
}
.addons-button-outline-white {
border: 1px solid #fff;
color: #fff;

View File

@ -1661,6 +1661,16 @@ a.reset_variations {
}
}
form[name="checkout"] {
display: table;
}
.blockUI.blockOverlay {
position: relative;
@include loader();
}
form {
.col2-set {

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@ -19,7 +19,7 @@ jQuery( function ( $ ) {
)
) {
/* State/Country select boxes */
this.states = $.parseJSON( woocommerce_admin_meta_boxes_order.countries.replace( /&quot;/g, '"' ) );
this.states = JSON.parse( woocommerce_admin_meta_boxes_order.countries.replace( /&quot;/g, '"' ) );
}
$( '.js_field-country' ).selectWoo().change( this.change_country );

View File

@ -9,7 +9,7 @@ jQuery( function ( $ ) {
init: function() {
if ( typeof wc_users_params.countries !== 'undefined' ) {
/* State/Country select boxes */
this.states = $.parseJSON( wc_users_params.countries.replace( /&quot;/g, '"' ) );
this.states = JSON.parse( wc_users_params.countries.replace( /&quot;/g, '"' ) );
}
$( '.js_field-country' ).selectWoo().change( this.change_country );

View File

@ -6,7 +6,7 @@ jQuery( function( $ ) {
return false;
}
var locale_json = wc_address_i18n_params.locale.replace( /&quot;/g, '"' ), locale = $.parseJSON( locale_json );
var locale_json = wc_address_i18n_params.locale.replace( /&quot;/g, '"' ), locale = JSON.parse( locale_json );
function field_is_required( field, is_required ) {
if ( is_required ) {
@ -51,7 +51,7 @@ jQuery( function( $ ) {
$statefield.attr( 'data-o_class', $statefield.attr( 'class' ) );
}
var locale_fields = $.parseJSON( wc_address_i18n_params.locale_fields );
var locale_fields = JSON.parse( wc_address_i18n_params.locale_fields );
$.each( locale_fields, function( key, value ) {

View File

@ -115,7 +115,7 @@ jQuery( function( $ ) {
} );
try {
var wc_fragments = $.parseJSON( sessionStorage.getItem( wc_cart_fragments_params.fragment_name ) ),
var wc_fragments = JSON.parse( sessionStorage.getItem( wc_cart_fragments_params.fragment_name ) ),
cart_hash = sessionStorage.getItem( cart_hash_key ),
cookie_hash = Cookies.get( 'woocommerce_cart_hash'),
cart_created = sessionStorage.getItem( 'wc_cart_created' );

View File

@ -191,7 +191,7 @@ jQuery( function( $ ) {
},
is_valid_json: function( raw_json ) {
try {
var json = $.parseJSON( raw_json );
var json = JSON.parse( raw_json );
return ( json && 'object' === typeof json );
} catch ( e ) {
@ -507,9 +507,8 @@ jQuery( function( $ ) {
$.ajax({
type: 'POST',
url: wc_checkout_params.checkout_url,
data: new FormData( this ),
contentType: false,
processData: false,
data: $form.serialize(),
dataType: 'json',
success: function( result ) {
// Detach the unload handler that prevents a reload / redirect
wc_checkout_form.detachUnloadEventsOnSubmit();

View File

@ -77,7 +77,7 @@ jQuery( function( $ ) {
/* State/Country select boxes */
var states_json = wc_country_select_params.countries.replace( /&quot;/g, '"' ),
states = $.parseJSON( states_json ),
states = JSON.parse( states_json ),
wrapper_selectors = '.woocommerce-billing-fields,' +
'.woocommerce-shipping-fields,' +
'.woocommerce-address-fields,' +

View File

@ -1,9 +0,0 @@
const { e2eBabelConfig } = require( '@woocommerce/e2e-environment' );
module.exports = function( api ) {
api.cache( true );
return {
...e2eBabelConfig,
};
};

View File

@ -4,7 +4,7 @@
},
"config": {
"platform": {
"php": "7.1"
"php": "7.0"
}
}
}

View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "ee5c0c106a076ca3b426771807c3ffeb",
"content-hash": "c50f65dd9f9a26d397f7bb30228d7a88",
"packages": [],
"packages-dev": [
{
@ -71,6 +71,10 @@
"stylecheck",
"tests"
],
"support": {
"issues": "https://github.com/dealerdirect/phpcodesniffer-composer-installer/issues",
"source": "https://github.com/dealerdirect/phpcodesniffer-composer-installer"
},
"time": "2020-06-25T14:57:39+00:00"
},
{
@ -129,6 +133,10 @@
"phpcs",
"standards"
],
"support": {
"issues": "https://github.com/PHPCompatibility/PHPCompatibility/issues",
"source": "https://github.com/PHPCompatibility/PHPCompatibility"
},
"time": "2019-12-27T09:44:58+00:00"
},
{
@ -181,6 +189,10 @@
"polyfill",
"standards"
],
"support": {
"issues": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie/issues",
"source": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie"
},
"time": "2019-11-04T15:17:54+00:00"
},
{
@ -231,6 +243,10 @@
"standards",
"wordpress"
],
"support": {
"issues": "https://github.com/PHPCompatibility/PHPCompatibilityWP/issues",
"source": "https://github.com/PHPCompatibility/PHPCompatibilityWP"
},
"time": "2019-08-28T14:22:28+00:00"
},
{
@ -282,6 +298,11 @@
"phpcs",
"standards"
],
"support": {
"issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues",
"source": "https://github.com/squizlabs/PHP_CodeSniffer",
"wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki"
},
"time": "2020-10-23T02:01:07+00:00"
},
{
@ -322,6 +343,10 @@
"woocommerce",
"wordpress"
],
"support": {
"issues": "https://github.com/woocommerce/woocommerce-sniffs/issues",
"source": "https://github.com/woocommerce/woocommerce-sniffs/tree/master"
},
"time": "2020-08-06T18:23:45+00:00"
},
{
@ -368,6 +393,11 @@
"standards",
"wordpress"
],
"support": {
"issues": "https://github.com/WordPress/WordPress-Coding-Standards/issues",
"source": "https://github.com/WordPress/WordPress-Coding-Standards",
"wiki": "https://github.com/WordPress/WordPress-Coding-Standards/wiki"
},
"time": "2020-05-13T23:57:56+00:00"
}
],
@ -379,6 +409,7 @@
"platform": [],
"platform-dev": [],
"platform-overrides": {
"php": "7.1"
}
"php": "7.0"
},
"plugin-api-version": "2.0.0"
}

View File

@ -2,11 +2,11 @@
"minimum-stability": "dev",
"prefer-stable": true,
"require-dev": {
"phpunit/phpunit": "7.5.20"
"phpunit/phpunit": "6.5.14"
},
"config": {
"platform": {
"php": "7.1"
"php": "7.0"
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,7 @@
},
"config": {
"platform": {
"php": "7.1"
"php": "7.0"
}
}
}

View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "f89bceee93cc1d38e71a45e4cbf4f4aa",
"content-hash": "4d4f2befccefe100869d30305083672b",
"packages": [],
"packages-dev": [
{
@ -67,6 +67,11 @@
"po",
"translation"
],
"support": {
"email": "oom@oscarotero.com",
"issues": "https://github.com/oscarotero/Gettext/issues",
"source": "https://github.com/php-gettext/Gettext/tree/v4.8.2"
},
"time": "2019-12-02T10:21:14+00:00"
},
{
@ -128,6 +133,10 @@
"translations",
"unicode"
],
"support": {
"issues": "https://github.com/php-gettext/Languages/issues",
"source": "https://github.com/php-gettext/Languages/tree/2.6.0"
},
"time": "2019-11-13T10:30:21+00:00"
},
{
@ -173,6 +182,10 @@
}
],
"description": "Peast is PHP library that generates AST for JavaScript code",
"support": {
"issues": "https://github.com/mck89/peast/issues",
"source": "https://github.com/mck89/peast/tree/v1.11.0"
},
"time": "2020-10-09T15:12:13+00:00"
},
{
@ -219,6 +232,10 @@
"mustache",
"templating"
],
"support": {
"issues": "https://github.com/bobthecow/mustache.php/issues",
"source": "https://github.com/bobthecow/mustache.php/tree/master"
},
"time": "2019-11-23T21:40:31+00:00"
},
{
@ -268,29 +285,33 @@
"iri",
"sockets"
],
"support": {
"issues": "https://github.com/rmccue/Requests/issues",
"source": "https://github.com/rmccue/Requests/tree/master"
},
"time": "2016-10-13T00:11:37+00:00"
},
{
"name": "symfony/finder",
"version": "v3.4.45",
"version": "v3.3.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
"reference": "52140652ed31cee3dabd0c481b5577201fa769b4"
"reference": "baea7f66d30854ad32988c11a09d7ffd485810c4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/52140652ed31cee3dabd0c481b5577201fa769b4",
"reference": "52140652ed31cee3dabd0c481b5577201fa769b4",
"url": "https://api.github.com/repos/symfony/finder/zipball/baea7f66d30854ad32988c11a09d7ffd485810c4",
"reference": "baea7f66d30854ad32988c11a09d7ffd485810c4",
"shasum": ""
},
"require": {
"php": "^5.5.9|>=7.0.8"
"php": ">=5.5.9"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.4-dev"
"dev-master": "3.3-dev"
}
},
"autoload": {
@ -317,7 +338,10 @@
],
"description": "Symfony Finder Component",
"homepage": "https://symfony.com",
"time": "2020-09-02T16:06:40+00:00"
"support": {
"source": "https://github.com/symfony/finder/tree/3.3"
},
"time": "2017-06-01T21:01:25+00:00"
},
{
"name": "wp-cli/i18n-command",
@ -374,6 +398,10 @@
],
"description": "Provides internationalization tools for WordPress projects.",
"homepage": "https://github.com/wp-cli/i18n-command",
"support": {
"issues": "https://github.com/wp-cli/i18n-command/issues",
"source": "https://github.com/wp-cli/i18n-command/tree/master"
},
"time": "2020-07-08T15:20:38+00:00"
},
{
@ -422,6 +450,9 @@
],
"description": "A simple YAML loader/dumper class for PHP (WP-CLI fork)",
"homepage": "https://github.com/mustangostang/spyc/",
"support": {
"source": "https://github.com/wp-cli/spyc/tree/autoload"
},
"time": "2017-04-25T11:26:20+00:00"
},
{
@ -472,6 +503,10 @@
"cli",
"console"
],
"support": {
"issues": "https://github.com/wp-cli/php-cli-tools/issues",
"source": "https://github.com/wp-cli/php-cli-tools/tree/master"
},
"time": "2018-09-04T13:28:00+00:00"
},
{
@ -534,6 +569,11 @@
"cli",
"wordpress"
],
"support": {
"docs": "https://make.wordpress.org/cli/handbook/",
"issues": "https://github.com/wp-cli/wp-cli/issues",
"source": "https://github.com/wp-cli/wp-cli"
},
"time": "2020-02-18T08:15:37+00:00"
}
],
@ -545,6 +585,7 @@
"platform": [],
"platform-dev": [],
"platform-overrides": {
"php": "7.1"
}
"php": "7.0"
},
"plugin-api-version": "2.0.0"
}

View File

@ -1,5 +1,64 @@
== Changelog ==
= 4.7.0 - 2020-11-10 =
**WooCommerce**
* Tweak - Update `product_cat/tag` taxonomy template file names to `product-cat/tag`. #27736
* Tweak - Exclude draft pages from the "Shop page" setting. #27890
* Tweak - Styling to properly display product reviews within the dashboard activity widget. #27968
* Fix - Fixes Photoswipe action buttons being obscured by admin bar. #27010
* Fix - Allow variation image to be removed via REST API. #27299
* Fix - Fixed WP CLI command to delete tax classes. #27310
* Fix - Prevent regenerate image filter loop. #27483
* Fix - Fixed some race conditions in `WC_Install`. #27696
* Fix - Improved PHP 8 support for `Automattic\WooCommerce\RestApi\Utilities\SingletonTrait`. #27707
* Fix - Adjust stock even if `reduce_stock` meta is not set in `wc_maybe_reduce_stock_levels`. #27763
* Fix - Removed duplicated CSS code from jQuery UI. #27767
* Fix - HTML syntax error in scheduled product message. #27842
* Fix - Update logic to determine if an order requires payment to check the order instead of the cart. #27893
* Fix - Use `Set password` title for lost password reset form when applicable. #27898
* Fix - REST API - Fixed deprecated notices while querying orders and refunds through REST API v1 endpoints. #27934
* Fix - Email address starting with `www` being displayed as a URL link in the admin order details page. #27983
* Fix - Unexpected HTTP 401 "Sorry, you cannot list resources" REST API responses that occur when a plugin or custom code determines the current WordPress user before WooCommerce is fully initialized. #27587
* Dev - Add `woocommerce_should_send_low_stock_notification` filter. #27819
* Dev - Introduce (again) a dependency injection framework for the code in the src directory. #27733
* Dev - Remove leftover code and data from the reverted improvement for variations filtering by attribute. #27748
* Dev - Escaped labels in `woocommerce_form_field()`. #27800
* Dev - Add a `NumberUtil::round` method to workaround a breaking change in the buil-in round function in PHP8. #27830
* Dev - Remove default value from optional parameters that are followed by required parameters in functions/methods, since those are de-facto required and trigger a deprectation notice in PHP 8. #27840
* Dev - REST API - Add user-friendly attribute names and values to order line items metadata.
* Dev - REST API - Adds `parent_name` to `line_items` of the GET /orders endpoint.
* Localization - Added Serbia districts. #27778
* Localization - Make city, and postcode non-required fields. #27779
* Localization - Add i18n locale information for Uganda, Kenya and Tanzania. #27164
* Localization - Renamed "Postcode / ZIP" to "Pin code", and renamed "State / County" to "State" for India. #27516
* Localization - Added postcode validation for addresses in India. #27546
**WooCommerce Blocks - 3.5.0 & 3.6.0**
* Make 'retry' property on errors from checkoutAfterProcessingWithSuccess/Error observers default to true if it's undefined. ([3261](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3261))
* Ensure new payment methods are only displayed when no saved payment method is selected. ([3247](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3247))
* Load WC Blocks CSS after editor CSS. ([3219](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3219))
* Restore saved payment method data after closing an express payment method. ([3210](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3210))
* Use light default background colour for country/state dropdowns. ([3189](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3189))
* Fix broken Express Payment Method use in the Checkout block for logged out or incognito users. ([3165](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3165))
* Fix State label for Spain. ([3147](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3147))
* Don't throw an error when registering a payment method fails. ([3134](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3134))
* Don't load contents of payment method hidden tabs. ([3227](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3227))
* Use noticeContexts from useEmitResponse instead of hardcoded values. ([3161](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3161))
**WooCommerce Admin - 1.6.3**
* Tweak: Add BR and IN to list of stripe countries [#5377](https://github.com/woocommerce/woocommerce-admin/pull/5377)
* Fix: Redirect instead of stalling on WCPay Inbox note action [#5413](https://github.com/woocommerce/woocommerce-admin/pull/5413)
= 4.6.2 - 2020-11-05 =
**WooCommerce**
* Prevent checkout from creating accounts when related setting is disabled.
= 4.6.1 - 2020-10-21 =
**WooCommerce**

View File

@ -10,13 +10,13 @@
"php": ">=7.0",
"automattic/jetpack-autoloader": "2.2.0",
"automattic/jetpack-constants": "1.5.0",
"composer/installers": "1.7.0",
"composer/installers": "~1.7",
"maxmind-db/reader": "1.6.0",
"pelago/emogrifier": "3.1.0",
"psr/container": "1.0.0",
"woocommerce/action-scheduler": "3.1.6",
"woocommerce/woocommerce-admin": "1.6.3",
"woocommerce/woocommerce-blocks": "3.6.0",
"woocommerce/woocommerce-admin": "1.7.0",
"woocommerce/woocommerce-blocks": "3.8.0",
"league/container": "3.3.3"
},
"require-dev": {
@ -24,7 +24,7 @@
},
"config": {
"platform": {
"php": "7.1"
"php": "7.0"
},
"preferred-install": {
"woocommerce/action-scheduler": "dist",
@ -90,10 +90,11 @@
},
"extra": {
"installer-paths": {
"packages/action-scheduler": ["woocommerce/action-scheduler"],
"packages/woocommerce-rest-api": ["woocommerce/woocommerce-rest-api"],
"packages/woocommerce-blocks": ["woocommerce/woocommerce-blocks"],
"packages/woocommerce-admin": ["woocommerce/woocommerce-admin"]
"packages/{$name}": [
"woocommerce/action-scheduler",
"woocommerce/woocommerce-blocks",
"woocommerce/woocommerce-admin"
]
},
"scripts-description": {
"test": "Run unit tests",

153
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "ed84c4d91482a5c508caaf50e843de58",
"content-hash": "6494b4d4b956386e32381541ebd79839",
"packages": [
{
"name": "automattic/jetpack-autoloader",
@ -40,6 +40,9 @@
"GPL-2.0-or-later"
],
"description": "Creates a custom autoloader for a plugin or theme.",
"support": {
"source": "https://github.com/Automattic/jetpack-autoloader/tree/master"
},
"time": "2020-08-14T20:34:36+00:00"
},
{
@ -71,32 +74,38 @@
"GPL-2.0-or-later"
],
"description": "A wrapper for defining constants in a more testable way.",
"support": {
"source": "https://github.com/Automattic/jetpack-constants/tree/master"
},
"time": "2020-08-13T14:33:09+00:00"
},
{
"name": "composer/installers",
"version": "v1.7.0",
"version": "v1.9.0",
"source": {
"type": "git",
"url": "https://github.com/composer/installers.git",
"reference": "141b272484481432cda342727a427dc1e206bfa0"
"reference": "b93bcf0fa1fccb0b7d176b0967d969691cd74cca"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/installers/zipball/141b272484481432cda342727a427dc1e206bfa0",
"reference": "141b272484481432cda342727a427dc1e206bfa0",
"url": "https://api.github.com/repos/composer/installers/zipball/b93bcf0fa1fccb0b7d176b0967d969691cd74cca",
"reference": "b93bcf0fa1fccb0b7d176b0967d969691cd74cca",
"shasum": ""
},
"require": {
"composer-plugin-api": "^1.0"
"composer-plugin-api": "^1.0 || ^2.0"
},
"replace": {
"roundcube/plugin-installer": "*",
"shama/baton": "*"
},
"require-dev": {
"composer/composer": "1.0.*@dev",
"phpunit/phpunit": "^4.8.36"
"composer/composer": "1.6.* || 2.0.*@dev",
"composer/semver": "1.0.* || 2.0.*@dev",
"phpunit/phpunit": "^4.8.36",
"sebastian/comparator": "^1.2.4",
"symfony/process": "^2.3"
},
"type": "composer-plugin",
"extra": {
@ -132,6 +141,7 @@
"Kanboard",
"Lan Management System",
"MODX Evo",
"MantisBT",
"Mautic",
"Maya",
"OXID",
@ -186,6 +196,7 @@
"shopware",
"silverstripe",
"sydes",
"sylius",
"symfony",
"typo3",
"wordpress",
@ -193,7 +204,21 @@
"zend",
"zikula"
],
"time": "2019-08-12T15:00:31+00:00"
"support": {
"issues": "https://github.com/composer/installers/issues",
"source": "https://github.com/composer/installers/tree/v1.9.0"
},
"funding": [
{
"url": "https://packagist.com",
"type": "custom"
},
{
"url": "https://tidelift.com/funding/github/packagist/composer/composer",
"type": "tidelift"
}
],
"time": "2020-04-07T06:57:05+00:00"
},
{
"name": "league/container",
@ -259,6 +284,10 @@
"provider",
"service"
],
"support": {
"issues": "https://github.com/thephpleague/container/issues",
"source": "https://github.com/thephpleague/container/tree/3.3.3"
},
"funding": [
{
"url": "https://github.com/philipobenito",
@ -325,6 +354,10 @@
"geolocation",
"maxmind"
],
"support": {
"issues": "https://github.com/maxmind/MaxMind-DB-Reader-php/issues",
"source": "https://github.com/maxmind/MaxMind-DB-Reader-php/tree/v1.6.0"
},
"time": "2019-12-19T22:59:03+00:00"
},
{
@ -399,6 +432,10 @@
"email",
"pre-processing"
],
"support": {
"issues": "https://github.com/MyIntervals/emogrifier/issues",
"source": "https://github.com/MyIntervals/emogrifier"
},
"time": "2019-12-26T19:37:31+00:00"
},
{
@ -448,26 +485,35 @@
"container-interop",
"psr"
],
"support": {
"issues": "https://github.com/php-fig/container/issues",
"source": "https://github.com/php-fig/container/tree/master"
},
"time": "2017-02-14T16:28:37+00:00"
},
{
"name": "symfony/css-selector",
"version": "v3.4.46",
"version": "v3.3.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/css-selector.git",
"reference": "da3d9da2ce0026771f5fe64cb332158f1bd2bc33"
"reference": "4d882dced7b995d5274293039370148e291808f2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/css-selector/zipball/da3d9da2ce0026771f5fe64cb332158f1bd2bc33",
"reference": "da3d9da2ce0026771f5fe64cb332158f1bd2bc33",
"url": "https://api.github.com/repos/symfony/css-selector/zipball/4d882dced7b995d5274293039370148e291808f2",
"reference": "4d882dced7b995d5274293039370148e291808f2",
"shasum": ""
},
"require": {
"php": "^5.5.9|>=7.0.8"
"php": ">=5.5.9"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.3-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Component\\CssSelector\\": ""
@ -481,14 +527,14 @@
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Jean-François Simon",
"email": "jeanfrancois.simon@sensiolabs.com"
},
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
@ -496,21 +542,10 @@
],
"description": "Symfony CssSelector Component",
"homepage": "https://symfony.com",
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2020-10-24T10:57:07+00:00"
"support": {
"source": "https://github.com/symfony/css-selector/tree/master"
},
"time": "2017-05-01T15:01:29+00:00"
},
{
"name": "woocommerce/action-scheduler",
@ -545,30 +580,35 @@
],
"description": "Action Scheduler for WordPress and WooCommerce",
"homepage": "https://actionscheduler.org/",
"support": {
"issues": "https://github.com/woocommerce/action-scheduler/issues",
"source": "https://github.com/woocommerce/action-scheduler/tree/master"
},
"time": "2020-05-12T16:22:33+00:00"
},
{
"name": "woocommerce/woocommerce-admin",
"version": "1.6.3",
"version": "1.7.0",
"source": {
"type": "git",
"url": "https://github.com/woocommerce/woocommerce-admin.git",
"reference": "3015abbda8657ef097b7763e4c941daa06dab6f7"
"reference": "14dc0c78ce163ed0d5daf8f83765b65a76f61010"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/woocommerce/woocommerce-admin/zipball/3015abbda8657ef097b7763e4c941daa06dab6f7",
"reference": "3015abbda8657ef097b7763e4c941daa06dab6f7",
"url": "https://api.github.com/repos/woocommerce/woocommerce-admin/zipball/14dc0c78ce163ed0d5daf8f83765b65a76f61010",
"reference": "14dc0c78ce163ed0d5daf8f83765b65a76f61010",
"shasum": ""
},
"require": {
"automattic/jetpack-autoloader": "^2.2.0",
"composer/installers": "1.7.0",
"composer/installers": "^1.9.0",
"php": ">=5.6|>=7.0"
},
"require-dev": {
"phpunit/phpunit": "7.5.20",
"woocommerce/woocommerce-sniffs": "0.0.9"
"suin/phpcs-psr4-sniff": "^2.2",
"woocommerce/woocommerce-sniffs": "0.1.0"
},
"type": "wordpress-plugin",
"extra": {
@ -579,9 +619,6 @@
}
},
"autoload": {
"classmap": [
"includes/"
],
"psr-4": {
"Automattic\\WooCommerce\\Admin\\": "src/"
}
@ -592,29 +629,33 @@
],
"description": "A modern, javascript-driven WooCommerce Admin experience.",
"homepage": "https://github.com/woocommerce/woocommerce-admin",
"time": "2020-10-26T20:25:00+00:00"
"support": {
"issues": "https://github.com/woocommerce/woocommerce-admin/issues",
"source": "https://github.com/woocommerce/woocommerce-admin/tree/v1.7.0"
},
"time": "2020-11-11T22:56:39+00:00"
},
{
"name": "woocommerce/woocommerce-blocks",
"version": "v3.6.0",
"version": "v3.8.0",
"source": {
"type": "git",
"url": "https://github.com/woocommerce/woocommerce-gutenberg-products-block.git",
"reference": "1046697451f5e8906e48f0a7532b7637c3ded108"
"reference": "8b7d485ec8d26a6d5c9011dbdb49443cad9beee7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/woocommerce/woocommerce-gutenberg-products-block/zipball/1046697451f5e8906e48f0a7532b7637c3ded108",
"reference": "1046697451f5e8906e48f0a7532b7637c3ded108",
"url": "https://api.github.com/repos/woocommerce/woocommerce-gutenberg-products-block/zipball/8b7d485ec8d26a6d5c9011dbdb49443cad9beee7",
"reference": "8b7d485ec8d26a6d5c9011dbdb49443cad9beee7",
"shasum": ""
},
"require": {
"automattic/jetpack-autoloader": "^2.0.0",
"composer/installers": "1.7.0"
"composer/installers": "^1.7.0"
},
"require-dev": {
"phpunit/phpunit": "6.5.14",
"woocommerce/woocommerce-sniffs": "0.0.7"
"woocommerce/woocommerce-sniffs": "0.1.0"
},
"type": "wordpress-plugin",
"extra": {
@ -639,7 +680,11 @@
"gutenberg",
"woocommerce"
],
"time": "2020-10-12T15:35:42+00:00"
"support": {
"issues": "https://github.com/woocommerce/woocommerce-gutenberg-products-block/issues",
"source": "https://github.com/woocommerce/woocommerce-gutenberg-products-block/tree/v3.8.0"
},
"time": "2020-11-10T15:07:11+00:00"
}
],
"packages-dev": [
@ -687,6 +732,10 @@
"isolation",
"tool"
],
"support": {
"issues": "https://github.com/bamarni/composer-bin-plugin/issues",
"source": "https://github.com/bamarni/composer-bin-plugin/tree/master"
},
"time": "2020-05-03T08:27:20+00:00"
}
],
@ -700,7 +749,7 @@
},
"platform-dev": [],
"platform-overrides": {
"php": "7.1"
"php": "7.0"
},
"plugin-api-version": "1.1.0"
"plugin-api-version": "2.0.0"
}

View File

@ -519,6 +519,50 @@ abstract class WC_Data {
}
}
/**
* Helper method to compute meta cache key. Different from WP Meta cache key in that meta data cached using this key also contains meta_id column.
*
* @since 4.7.0
*
* @return string
*/
public function get_meta_cache_key() {
if ( ! $this->get_id() ) {
wc_doing_it_wrong( 'get_meta_cache_key', 'ID needs to be set before fetching a cache key.', '4.7.0' );
return false;
}
return self::generate_meta_cache_key( $this->get_id(), $this->cache_group );
}
/**
* Generate cache key from id and group.
*
* @since 4.7.0
*
* @param int|string $id Object ID.
* @param string $cache_group Group name use to store cache. Whole group cache can be invalidated in one go.
*
* @return string Meta cache key.
*/
public static function generate_meta_cache_key( $id, $cache_group ) {
return WC_Cache_Helper::get_cache_prefix( $cache_group ) . WC_Cache_Helper::get_cache_prefix( 'object_' . $id ) . 'object_meta_' . $id;
}
/**
* Prime caches for raw meta data. This includes meta_id column as well, which is not included by default in WP meta data.
*
* @since 4.7.0
*
* @param array $raw_meta_data_collection Array of objects of { object_id => array( meta_row_1, meta_row_2, ... }.
* @param string $cache_group Name of cache group.
*/
public static function prime_raw_meta_data_cache( $raw_meta_data_collection, $cache_group ) {
foreach ( $raw_meta_data_collection as $object_id => $raw_meta_data_array ) {
$cache_key = self::generate_meta_cache_key( $object_id, $cache_group );
wp_cache_set( $cache_key, $raw_meta_data_array, $cache_group );
}
}
/**
* Read Meta Data from the database. Ignore any internal properties.
* Uses it's own caches because get_metadata does not provide meta_ids.
@ -540,7 +584,7 @@ abstract class WC_Data {
if ( ! empty( $this->cache_group ) ) {
// Prefix by group allows invalidation by group until https://core.trac.wordpress.org/ticket/4476 is implemented.
$cache_key = WC_Cache_Helper::get_cache_prefix( $this->cache_group ) . WC_Cache_Helper::get_cache_prefix( 'object_' . $this->get_id() ) . 'object_meta_' . $this->get_id();
$cache_key = $this->get_meta_cache_key();
}
if ( ! $force_read ) {
@ -550,7 +594,9 @@ abstract class WC_Data {
}
}
$raw_meta_data = $cache_loaded ? $cached_meta : $this->data_store->read_meta( $this );
// We filter the raw meta data again when loading from cache, in case we cached in an earlier version where filter conditions were different.
$raw_meta_data = $cache_loaded ? $this->data_store->filter_raw_meta_data( $this, $cached_meta ) : $this->data_store->read_meta( $this );
if ( $raw_meta_data ) {
foreach ( $raw_meta_data as $meta ) {
$this->meta_data[] = new WC_Meta_Data(

View File

@ -370,9 +370,9 @@ class WC_Admin_Addons {
$defaults = array(
'image' => WC()->plugin_url() . '/assets/images/wcs-extensions-banner-3x.png',
'image_alt' => __( 'WooCommerce Services', 'woocommerce' ),
'image_alt' => __( 'WooCommerce Shipping', 'woocommerce' ),
'title' => __( 'Buy discounted shipping labels — then print them from your dashboard.', 'woocommerce' ),
'description' => __( 'Integrate your store with USPS to buy discounted shipping labels, and print them directly from your WooCommerce dashboard. Powered by WooCommerce Services.', 'woocommerce' ),
'description' => __( 'Integrate your store with USPS to buy discounted shipping labels, and print them directly from your WooCommerce dashboard. Powered by WooCommerce Shipping.', 'woocommerce' ),
'button' => __( 'Free - Install now', 'woocommerce' ),
'href' => $button_url,
'logos' => array(),
@ -383,7 +383,7 @@ class WC_Admin_Addons {
$local_defaults = array(
'image' => WC()->plugin_url() . '/assets/images/wcs-truck-banner-3x.png',
'title' => __( 'Show Canada Post shipping rates', 'woocommerce' ),
'description' => __( 'Display live rates from Canada Post at checkout to make shipping a breeze. Powered by WooCommerce Services.', 'woocommerce' ),
'description' => __( 'Display live rates from Canada Post at checkout to make shipping a breeze. Powered by WooCommerce Shipping.', 'woocommerce' ),
'logos' => array_merge(
$defaults['logos'],
array(
@ -440,7 +440,69 @@ class WC_Admin_Addons {
self::output_button(
$block_data['href'],
$block_data['button'],
'addons-button-outline-green'
'addons-button-outline-purple'
);
?>
</div>
</div>
<?php
}
/**
* Handles the outputting of the WooCommerce Pay banner block.
*
* @param object $block Block data.
*/
public static function output_wcpay_banner_block( $block = array() ) {
$is_active = is_plugin_active( 'woocommerce-payments/woocommerce-payments.php' );
$location = wc_get_base_location();
if (
! in_array( $location['country'], array( 'US' ), true ) ||
$is_active ||
! current_user_can( 'install_plugins' ) ||
! current_user_can( 'activate_plugins' )
) {
return;
}
$button_url = wp_nonce_url(
add_query_arg(
array(
'install-addon' => 'woocommerce-payments',
)
),
'install-addon_woocommerce-payments'
);
$defaults = array(
'image' => WC()->plugin_url() . '/assets/images/wcpayments-icon-secure.png',
'image_alt' => __( 'WooCommerce Payments', 'woocommerce' ),
'title' => __( 'Payments made simple, with no monthly fees &mdash; exclusively for WooCommerce stores.', 'woocommerce' ),
'description' => __( 'Securely accept cards in your store. See payments, track cash flow into your bank account, and stay on top of disputes right from your dashboard.', 'woocommerce' ),
'button' => __( 'Free - Install now', 'woocommerce' ),
'href' => $button_url,
'logos' => array(),
);
$block_data = array_merge( $defaults, $block );
?>
<div class="addons-wcs-banner-block">
<div class="addons-wcs-banner-block-image">
<img
class="addons-img"
src="<?php echo esc_url( $block_data['image'] ); ?>"
alt="<?php echo esc_attr( $block_data['image_alt'] ); ?>"
/>
</div>
<div class="addons-wcs-banner-block-content">
<h1><?php echo esc_html( $block_data['title'] ); ?></h1>
<p><?php echo esc_html( $block_data['description'] ); ?></p>
<?php
self::output_button(
$block_data['href'],
$block_data['button'],
'addons-button-outline-purple'
);
?>
</div>
@ -477,6 +539,9 @@ class WC_Admin_Addons {
case 'wcs_banner_block':
self::output_wcs_banner_block( (array) $section );
break;
case 'wcpay_banner_block':
self::output_wcpay_banner_block( (array) $section );
break;
}
}
}
@ -520,7 +585,7 @@ class WC_Admin_Addons {
* @param string $plugin The plugin the button is promoting.
*/
public static function output_button( $url, $text, $style, $plugin = '' ) {
$style = __( 'Free', 'woocommerce' ) === $text ? 'addons-button-outline-green' : $style;
$style = __( 'Free', 'woocommerce' ) === $text ? 'addons-button-outline-purple' : $style;
$style = is_plugin_active( $plugin ) ? 'addons-button-installed' : $style;
$text = is_plugin_active( $plugin ) ? __( 'Installed', 'woocommerce' ) : $text;
$url = self::add_in_app_purchase_url_params( $url );
@ -546,8 +611,18 @@ class WC_Admin_Addons {
return;
}
if ( isset( $_GET['install-addon'] ) && 'woocommerce-services' === $_GET['install-addon'] ) {
self::install_woocommerce_services_addon();
if ( isset( $_GET['install-addon'] ) ) {
switch ( $_GET['install-addon'] ) {
case 'woocommerce-services':
self::install_woocommerce_services_addon();
break;
case 'woocommerce-payments':
self::install_woocommerce_payments_addon();
break;
default:
// Do nothing.
break;
}
}
$sections = self::get_sections();
@ -591,6 +666,26 @@ class WC_Admin_Addons {
exit;
}
/**
* Install WooCommerce Payments from the Extensions screens.
*
* @return void
*/
public static function install_woocommerce_payments_addon() {
check_admin_referer( 'install-addon_woocommerce-payments' );
$wcpay_plugin_id = 'woocommerce-payments';
$wcpay_plugin = array(
'name' => __( 'WooCommerce Payments', 'woocommerce' ),
'repo-slug' => 'woocommerce-payments',
);
WC_Install::background_installer( $services_plugin_id, $wcpay_plugin );
wp_safe_redirect( remove_query_arg( array( 'install-addon', '_wpnonce' ) ) );
exit;
}
/**
* Should an extension be shown on the featured page.
*

View File

@ -377,6 +377,11 @@ class WC_Admin_Status {
private static function output_plugins_info( $plugins, $untested_plugins ) {
$wc_version = Constants::get_constant( 'WC_VERSION' );
if ( 'major' === WC_SSR_PLUGIN_UPDATE_RELEASE_VERSION_TYPE ) {
// Since we're only testing against major, we don't need to show minor and patch version.
$wc_version = $wc_version[0] . '.0';
}
foreach ( $plugins as $plugin ) {
if ( ! empty( $plugin['name'] ) ) {
// Link the plugin name to the plugin url if available.

View File

@ -10,7 +10,7 @@
defined( 'ABSPATH' ) || exit;
use \Automattic\Jetpack\Constants;
use \Automattic\WooCommerce\Admin\Notes\WC_Admin_Note;
use Automattic\WooCommerce\Admin\Notes\Note;
/**
* WC_Notes_Run_Db_Update.
@ -58,7 +58,7 @@ class WC_Notes_Run_Db_Update {
// Remove weird duplicates. Leave the first one.
$current_notice = array_shift( $note_ids );
foreach ( $note_ids as $note_id ) {
$note = new WC_Admin_Note( $note_id );
$note = new Note( $note_id );
$data_store->delete( $note );
}
return $current_notice;
@ -77,8 +77,8 @@ class WC_Notes_Run_Db_Update {
return;
}
$note = new WC_Admin_Note( $note_id );
$note->set_status( WC_Admin_Note::E_WC_ADMIN_NOTE_ACTIONED );
$note = new Note( $note_id );
$note->set_status( Note::E_WC_ADMIN_NOTE_ACTIONED );
$note->save();
}
@ -89,7 +89,7 @@ class WC_Notes_Run_Db_Update {
* - actions are set up for the first 'Update database' notice, and
* - URL for note's action is equal to the given URL (to check for potential nonce update).
*
* @param WC_Admin_Note $note Note to check.
* @param Note $note Note to check.
* @param string $update_url URL to check the note against.
* @param array<int, string> $current_actions List of actions to check for.
* @return bool
@ -135,9 +135,9 @@ class WC_Notes_Run_Db_Update {
);
if ( $note_id ) {
$note = new WC_Admin_Note( $note_id );
$note = new Note( $note_id );
} else {
$note = new WC_Admin_Note();
$note = new Note();
}
// Check if the note needs to be updated (e.g. expired nonce or different note type stored in the previous run).
@ -151,13 +151,13 @@ class WC_Notes_Run_Db_Update {
/* translators: %1$s: opening <a> tag %2$s: closing </a> tag*/
. sprintf( ' ' . esc_html__( 'The database update process runs in the background and may take a little while, so please be patient. Advanced users can alternatively update via %1$sWP CLI%2$s.', 'woocommerce' ), '<a href="https://github.com/woocommerce/woocommerce/wiki/Upgrading-the-database-using-WP-CLI">', '</a>' )
);
$note->set_type( WC_Admin_Note::E_WC_ADMIN_NOTE_UPDATE );
$note->set_type( Note::E_WC_ADMIN_NOTE_UPDATE );
$note->set_name( self::NOTE_NAME );
$note->set_content_data( (object) array() );
$note->set_source( 'woocommerce-core' );
// In case db version is out of sync with WC version or during the next update, the notice needs to show up again,
// so set it to unactioned.
$note->set_status( WC_Admin_Note::E_WC_ADMIN_NOTE_UNACTIONED );
$note->set_status( Note::E_WC_ADMIN_NOTE_UNACTIONED );
// Set new actions.
$note->clear_actions();
@ -181,7 +181,7 @@ class WC_Notes_Run_Db_Update {
$cron_disabled = Constants::is_true( 'DISABLE_WP_CRON' );
$cron_cta = $cron_disabled ? __( 'You can manually run queued updates here.', 'woocommerce' ) : __( 'View progress →', 'woocommerce' );
$note = new WC_Admin_Note( $note_id );
$note = new Note( $note_id );
$note->set_title( __( 'WooCommerce database update in progress', 'woocommerce' ) );
$note->set_content( __( 'WooCommerce is updating the database in the background. The database update process may take a little while, so please be patient.', 'woocommerce' ) );
@ -227,7 +227,7 @@ class WC_Notes_Run_Db_Update {
),
);
$note = new WC_Admin_Note( $note_id );
$note = new Note( $note_id );
// Check if the note needs to be updated (e.g. expired nonce or different note type stored in the previous run).
if ( self::note_up_to_date( $note, $hide_notices_url, wp_list_pluck( $note_actions, 'name' ) ) ) {
@ -266,7 +266,7 @@ class WC_Notes_Run_Db_Update {
return;
}
$note = new WC_Admin_Note( $note_id );
$note = new Note( $note_id );
if ( $note::E_WC_ADMIN_NOTE_ACTIONED === $note->get_status() ) {
// Db update not needed && note actioned -> don't show it.
return;

View File

@ -163,15 +163,6 @@ class WC_Plugin_Updates {
$version .= '.' . $new_version_parts[1];
}
if ( 'major' === $release ) {
$current_version_parts = explode( '.', Constants::get_constant( 'WC_VERSION' ) );
// If user has already moved to the major version, we don't need to flag up anything.
if ( version_compare( $current_version_parts[0] . '.' . $current_version_parts[1], $new_version_parts[0] . '.0', '>=' ) ) {
return array();
}
}
foreach ( $extensions as $file => $plugin ) {
if ( ! empty( $plugin[ self::VERSION_TESTED_HEADER ] ) ) {
$plugin_version_parts = explode( '.', $plugin[ self::VERSION_TESTED_HEADER ] );

View File

@ -41,6 +41,7 @@ if ( ! defined( 'ABSPATH' ) ) {
<?php if ( isset( $_GET['search'] ) ) : // phpcs:ignore WordPress.Security.NonceVerification.Recommended ?>
<h1 class="search-form-title" >
<?php // translators: search keyword. ?>
<?php printf( esc_html__( 'Showing search results for: %s', 'woocommerce' ), '<strong>' . esc_html( sanitize_text_field( wp_unslash( $_GET['search'] ) ) ) . '</strong>' ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended ?>
</h1>
<?php endif; ?>
@ -71,6 +72,11 @@ if ( ! defined( 'ABSPATH' ) ) {
<?php WC_Admin_Addons::output_wcs_banner_block(); ?>
</div>
<?php endif; ?>
<?php if ( 'payment-gateways' === $current_section ) : ?>
<div class="addons-shipping-methods">
<?php WC_Admin_Addons::output_wcpay_banner_block(); ?>
</div>
<?php endif; ?>
<ul class="products">
<?php foreach ( $addons as $addon ) : ?>
<?php

View File

@ -9,6 +9,11 @@ defined( 'ABSPATH' ) || exit;
global $wpdb;
if ( ! defined( 'WC_SSR_PLUGIN_UPDATE_RELEASE_VERSION_TYPE' ) ) {
// Define if we're checking against major or minor versions.
define( 'WC_SSR_PLUGIN_UPDATE_RELEASE_VERSION_TYPE', 'major' );
}
$report = wc()->api->get_endpoint_data( '/wc/v3/system_status' );
$environment = $report['environment'];
$database = $report['database'];
@ -21,7 +26,7 @@ $security = $report['security'];
$settings = $report['settings'];
$wp_pages = $report['pages'];
$plugin_updates = new WC_Plugin_Updates();
$untested_plugins = $plugin_updates->get_untested_plugins( WC()->version, 'minor' );
$untested_plugins = $plugin_updates->get_untested_plugins( WC()->version, WC_SSR_PLUGIN_UPDATE_RELEASE_VERSION_TYPE );
?>
<div class="updated woocommerce-message inline">
<p>

View File

@ -218,6 +218,14 @@ function wc_maybe_adjust_line_item_product_stock( $item, $item_quantity = -1 ) {
$refunded_item_quantity = $order->get_qty_refunded_for_item( $item->get_id() );
$diff = $item_quantity + $refunded_item_quantity - $already_reduced_stock;
/*
* 0 as $item_quantity usually indicates we're deleting the order item.
* We need to perform different calculations for this case.
*/
if ( 0 === $item_quantity ) {
$diff = min( absint( $refunded_item_quantity ), $already_reduced_stock ) * -1;
}
if ( $diff < 0 ) {
$new_stock = wc_update_product_stock( $product, $diff * -1, 'increase' );
} elseif ( $diff > 0 ) {

View File

@ -1572,10 +1572,15 @@ class WC_AJAX {
$data_store = WC_Data_Store::load( 'product' );
$ids = $data_store->search_products( $term, '', (bool) $include_variations, false, $limit, $include_ids, $exclude_ids );
$product_objects = array_filter( array_map( 'wc_get_product', $ids ), 'wc_products_array_filter_readable' );
$products = array();
$products = array();
foreach ( $ids as $id ) {
$product_object = wc_get_product( $id );
if ( ! wc_products_array_filter_readable( $product_object ) ) {
continue;
}
foreach ( $product_objects as $product_object ) {
$formatted_name = $product_object->get_formatted_name();
$managing_stock = $product_object->managing_stock();

View File

@ -667,15 +667,18 @@ class WC_Checkout {
* @return array of data.
*/
public function get_posted_data() {
$skipped = array();
$data = array(
'terms' => (int) isset( $_POST['terms'] ), // WPCS: input var ok, CSRF ok.
'createaccount' => (int) ! empty( $_POST['createaccount'] ), // WPCS: input var ok, CSRF ok.
'payment_method' => isset( $_POST['payment_method'] ) ? wc_clean( wp_unslash( $_POST['payment_method'] ) ) : '', // WPCS: input var ok, CSRF ok.
'shipping_method' => isset( $_POST['shipping_method'] ) ? wc_clean( wp_unslash( $_POST['shipping_method'] ) ) : '', // WPCS: input var ok, CSRF ok.
'ship_to_different_address' => ! empty( $_POST['ship_to_different_address'] ) && ! wc_ship_to_billing_address_only(), // WPCS: input var ok, CSRF ok.
'woocommerce_checkout_update_totals' => isset( $_POST['woocommerce_checkout_update_totals'] ), // WPCS: input var ok, CSRF ok.
// phpcs:disable WordPress.Security.NonceVerification.Missing
$data = array(
'terms' => (int) isset( $_POST['terms'] ),
'createaccount' => (int) ( $this->is_registration_enabled() ? ! empty( $_POST['createaccount'] ) : false ),
'payment_method' => isset( $_POST['payment_method'] ) ? wc_clean( wp_unslash( $_POST['payment_method'] ) ) : '',
'shipping_method' => isset( $_POST['shipping_method'] ) ? wc_clean( wp_unslash( $_POST['shipping_method'] ) ) : '',
'ship_to_different_address' => ! empty( $_POST['ship_to_different_address'] ) && ! wc_ship_to_billing_address_only(),
'woocommerce_checkout_update_totals' => isset( $_POST['woocommerce_checkout_update_totals'] ),
);
// phpcs:enable WordPress.Security.NonceVerification.Missing
$skipped = array();
foreach ( $this->get_checkout_fields() as $fieldset_key => $fieldset ) {
if ( $this->maybe_skip_fieldset( $fieldset_key, $data ) ) {
$skipped[] = $fieldset_key;

View File

@ -906,12 +906,9 @@ class WC_Form_Handler {
}
// Prevent parent variable product from being added to cart.
if ( empty( $variation_id ) && $product->is_type( 'variable' ) ) {
$url = get_permalink( $product_id );
$product_name = $product->get_name();
/* translators: %1$s: Product link, %2$s: Product title, %3$s: Product name. */
wc_add_notice( sprintf( __( 'Please choose product options by visiting <a href="%1$s" title="%2$s">%3$s</a>.', 'woocommerce' ), esc_url( $url ), esc_html( $product_name ), esc_html( $product_name ) ), 'error' );
if ( empty( $variation_id ) && $product && $product->is_type( 'variable' ) ) {
/* translators: 1: product link, 2: product name */
wc_add_notice( sprintf( __( 'Please choose product options by visiting <a href="%1$s" title="%2$s">%2$s</a>.', 'woocommerce' ), esc_url( get_permalink( $product_id ) ), esc_html( $product->get_name() ) ), 'error' );
return false;
}

View File

@ -308,7 +308,7 @@ class WC_Frontend_Scripts {
}
/**
* Register all WC sty;es.
* Register all WC styles.
*/
private static function register_styles() {
$version = Constants::get_constant( 'WC_VERSION' );

View File

@ -220,7 +220,7 @@ class WC_Shortcodes {
foreach ( $product_categories as $category ) {
wc_get_template(
'content-product_cat.php',
'content-product-cat.php',
array(
'category' => $category,
)
@ -230,7 +230,7 @@ class WC_Shortcodes {
woocommerce_product_loop_end();
}
woocommerce_reset_loop();
wc_reset_loop();
return '<div class="woocommerce columns-' . $columns . '">' . ob_get_clean() . '</div>';
}

View File

@ -56,6 +56,9 @@ class WC_Validation {
case 'BA':
$valid = (bool) preg_match( '/^([7-8]{1})([0-9]{4})$/', $postcode );
break;
case 'BE':
$valid = (bool) preg_match( '/^([0-9]{4})$/i', $postcode );
break;
case 'BR':
$valid = (bool) preg_match( '/^([0-9]{5})([-])?([0-9]{3})$/', $postcode );
break;

View File

@ -22,7 +22,7 @@ final class WooCommerce {
*
* @var string
*/
public $version = '4.8.0';
public $version = '4.9.0';
/**
* WooCommerce Schema version.

View File

@ -93,14 +93,13 @@ abstract class Abstract_WC_Order_Data_Store_CPT extends WC_Data_Store_WP impleme
/**
* Method to read an order from the database.
*
* @param WC_Data $order Order object.
* @param WC_Order $order Order object.
*
* @throws Exception If passed order is invalid.
*/
public function read( &$order ) {
$order->set_defaults();
$post_object = get_post( $order->get_id() );
if ( ! $order->get_id() || ! $post_object || ! in_array( $post_object->post_type, wc_get_order_types(), true ) ) {
throw new Exception( __( 'Invalid order.', 'woocommerce' ) );
}
@ -108,8 +107,8 @@ abstract class Abstract_WC_Order_Data_Store_CPT extends WC_Data_Store_WP impleme
$order->set_props(
array(
'parent_id' => $post_object->post_parent,
'date_created' => 0 < $post_object->post_date_gmt ? wc_string_to_timestamp( $post_object->post_date_gmt ) : null,
'date_modified' => 0 < $post_object->post_modified_gmt ? wc_string_to_timestamp( $post_object->post_modified_gmt ) : null,
'date_created' => $this->string_to_timestamp( $post_object->post_date_gmt ),
'date_modified' => $this->string_to_timestamp( $post_object->post_modified_gmt ),
'status' => $post_object->post_status,
)
);

View File

@ -123,8 +123,8 @@ class WC_Coupon_Data_Store_CPT extends WC_Data_Store_WP implements WC_Coupon_Dat
array(
'code' => $post_object->post_title,
'description' => $post_object->post_excerpt,
'date_created' => 0 < $post_object->post_date_gmt ? wc_string_to_timestamp( $post_object->post_date_gmt ) : null,
'date_modified' => 0 < $post_object->post_modified_gmt ? wc_string_to_timestamp( $post_object->post_modified_gmt ) : null,
'date_created' => $this->string_to_timestamp( $post_object->post_date_gmt ),
'date_modified' => $this->string_to_timestamp( $post_object->post_modified_gmt ),
'date_expires' => metadata_exists( 'post', $coupon_id, 'date_expires' ) ? get_post_meta( $coupon_id, 'date_expires', true ) : get_post_meta( $coupon_id, 'expiry_date', true ), // @todo: Migrate expiry_date meta to date_expires in upgrade routine.
'discount_type' => get_post_meta( $coupon_id, 'discount_type', true ),
'amount' => get_post_meta( $coupon_id, 'coupon_amount', true ),

View File

@ -152,10 +152,11 @@ class WC_Customer_Data_Store extends WC_Data_Store_WP implements WC_Customer_Dat
$customer_id = $customer->get_id();
// Load meta but exclude deprecated props.
// Load meta but exclude deprecated props and parent keys.
$user_meta = array_diff_key(
array_change_key_case( array_map( 'wc_flatten_meta_callback', get_user_meta( $customer_id ) ) ),
array_flip( array( 'country', 'state', 'postcode', 'city', 'address', 'address_2', 'default', 'location' ) )
array_flip( array( 'country', 'state', 'postcode', 'city', 'address', 'address_2', 'default', 'location' ) ),
array_change_key_case( (array) $user_object->data )
);
$customer->set_props( $user_meta );

View File

@ -94,7 +94,20 @@ class WC_Data_Store_WP {
$object->get_id()
)
);
return $this->filter_raw_meta_data( $object, $raw_meta_data );
}
/**
* Helper method to filter internal meta keys from all meta data rows for the object.
*
* @since 4.7.0
*
* @param WC_Data $object WC_Data object.
* @param array $raw_meta_data Array of std object of meta data to be filtered.
*
* @return mixed|void
*/
public function filter_raw_meta_data( &$object, $raw_meta_data ) {
$this->internal_meta_keys = array_merge( array_map( array( $this, 'prefix_key' ), $object->get_data_keys() ), $this->internal_meta_keys );
$meta_data = array_filter( $raw_meta_data, array( $this, 'exclude_internal_meta_keys' ) );
return apply_filters( "woocommerce_data_store_wp_{$this->meta_type}_read_meta", $meta_data, $object, $this );
@ -630,4 +643,16 @@ class WC_Data_Store_WP {
);
wp_cache_delete( 'lookup_table', 'object_' . $id );
}
/**
* Converts a WP post date string into a timestamp.
*
* @since 4.8.0
*
* @param string $time_string The WP post date string.
* @return int|null The date string converted to a timestamp or null.
*/
protected function string_to_timestamp( $time_string ) {
return '0000-00-00 00:00:00' !== $time_string ? wc_string_to_timestamp( $time_string ) : null;
}
}

View File

@ -734,11 +734,12 @@ class WC_Order_Data_Store_CPT extends Abstract_WC_Order_Data_Store_CPT implement
* Get the order type based on Order ID.
*
* @since 3.0.0
* @param int $order_id Order ID.
* @param int|WP_Post $order Order | Order id.
*
* @return string
*/
public function get_order_type( $order_id ) {
return get_post_type( $order_id );
public function get_order_type( $order ) {
return get_post_type( $order );
}
/**
@ -865,7 +866,13 @@ class WC_Order_Data_Store_CPT extends Abstract_WC_Order_Data_Store_CPT implement
$query = new WP_Query( $args );
}
$orders = ( isset( $query_vars['return'] ) && 'ids' === $query_vars['return'] ) ? $query->posts : array_filter( array_map( 'wc_get_order', $query->posts ) );
if ( isset( $query_vars['return'] ) && 'ids' === $query_vars['return'] ) {
$orders = $query->posts;
} else {
update_post_caches( $query->posts ); // We already fetching posts, might as well hydrate some caches.
$order_ids = wp_list_pluck( $query->posts, 'ID' );
$orders = $this->compile_orders( $order_ids, $query_vars, $query );
}
if ( isset( $query_vars['paginate'] ) && $query_vars['paginate'] ) {
return (object) array(
@ -878,6 +885,213 @@ class WC_Order_Data_Store_CPT extends Abstract_WC_Order_Data_Store_CPT implement
return $orders;
}
/**
* Compile order response and set caches as needed for order ids.
*
* @param array $order_ids List of order IDS to compile.
* @param array $query_vars Original query arguments.
* @param WP_Query $query Query object.
*
* @return array Orders.
*/
private function compile_orders( $order_ids, $query_vars, $query ) {
if ( empty( $order_ids ) ) {
return array();
}
$orders = array();
// Lets do some cache hydrations so that we don't have to fetch data from DB for every order.
$this->prime_raw_meta_cache_for_orders( $order_ids, $query_vars );
$this->prime_refund_caches_for_order( $order_ids, $query_vars );
$this->prime_order_item_caches_for_orders( $order_ids, $query_vars );
foreach ( $query->posts as $post ) {
$orders[] = wc_get_order( $post );
}
return $orders;
}
/**
* Prime refund cache for orders.
*
* @param array $order_ids Order Ids to prime cache for.
* @param array $query_vars Query vars for the query.
*/
private function prime_refund_caches_for_order( $order_ids, $query_vars ) {
if ( ! isset( $query_vars['type'] ) || ! ( 'shop_order' === $query_vars['type'] ) ) {
return;
}
if ( isset( $query_vars['fields'] ) && 'all' !== $query_vars['fields'] ) {
if ( is_array( $query_vars['fields'] ) && ! in_array( 'refunds', $query_vars['fields'] ) ) {
return;
}
}
$cache_keys_mapping = array();
foreach ( $order_ids as $order_id ) {
$cache_keys_mapping[ $order_id ] = WC_Cache_Helper::get_cache_prefix( 'orders' ) . 'refunds' . $order_id;
}
$non_cached_ids = array();
$cache_values = wc_cache_get_multiple( array_values( $cache_keys_mapping ), 'orders' );
foreach ( $order_ids as $order_id ) {
if ( false === $cache_values[ $cache_keys_mapping[ $order_id ] ] ) {
$non_cached_ids[] = $order_id;
}
}
if ( empty( $non_cached_ids ) ) {
return;
}
$refunds = wc_get_orders(
array(
'type' => 'shop_order_refund',
'post_parent__in' => $non_cached_ids,
'limit' => - 1,
)
);
$order_refunds = array_reduce(
$refunds,
function ( $order_refunds_array, WC_Order_Refund $refund ) {
if ( ! isset( $order_refunds_array[ $refund->get_parent_id() ] ) ) {
$order_refunds_array[ $refund->get_parent_id() ] = array();
}
$order_refunds_array[ $refund->get_parent_id() ][] = $refund;
return $order_refunds_array;
},
array()
);
foreach ( $non_cached_ids as $order_id ) {
$refunds = array();
if ( isset( $order_refunds[ $order_id ] ) ) {
$refunds = $order_refunds[ $order_id ];
}
wp_cache_set( $cache_keys_mapping[ $order_id ], $refunds, 'orders' );
}
}
/**
* Prime following caches:
* 1. item-$order_item_id For individual items.
* 2. order-items-$order-id For fetching items associated with an order.
* 3. order-item meta.
*
* @param array $order_ids Order Ids to prime cache for.
* @param array $query_vars Query vars for the query.
*/
private function prime_order_item_caches_for_orders( $order_ids, $query_vars ) {
global $wpdb;
if ( isset( $query_vars['fields'] ) && 'all' !== $query_vars['fields'] ) {
$line_items = array(
'line_items',
'shipping_lines',
'fee_lines',
'coupon_lines',
);
if ( is_array( $query_vars['fields'] ) && 0 === count( array_intersect( $line_items, $query_vars['fields'] ) ) ) {
return;
}
}
$cache_keys = array_map(
function ( $order_id ) {
return 'order-items-' . $order_id;
},
$order_ids
);
$cache_values = wc_cache_get_multiple( $cache_keys, 'orders' );
$non_cached_ids = array();
foreach ( $order_ids as $order_id ) {
if ( false === $cache_values[ 'order-items-' . $order_id ] ) {
$non_cached_ids[] = $order_id;
}
}
if ( empty( $non_cached_ids ) ) {
return;
}
$non_cached_ids = esc_sql( $non_cached_ids );
$non_cached_ids_string = implode( ',', $non_cached_ids );
$order_items = $wpdb->get_results(
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
"SELECT order_item_type, order_item_id, order_id, order_item_name FROM {$wpdb->prefix}woocommerce_order_items WHERE order_id in ( $non_cached_ids_string ) ORDER BY order_item_id;"
);
if ( empty( $order_items ) ) {
return;
}
$order_items_for_all_orders = array_reduce(
$order_items,
function ( $order_items_collection, $order_item ) {
if ( ! isset( $order_items_collection[ $order_item->order_id ] ) ) {
$order_items_collection[ $order_item->order_id ] = array();
}
$order_items_collection[ $order_item->order_id ][] = $order_item;
return $order_items_collection;
}
);
foreach ( $order_items_for_all_orders as $order_id => $items ) {
wp_cache_set( 'order-items-' . $order_id, $items, 'orders' );
}
foreach ( $order_items as $item ) {
wp_cache_set( 'item-' . $item->order_item_id, $item, 'order-items' );
}
$order_item_ids = wp_list_pluck( $order_items, 'order_item_id' );
update_meta_cache( 'order_item', $order_item_ids );
}
/**
* Prime cache for raw meta data for orders in bulk. Difference between this and WP built-in metadata is that this method also fetches `meta_id` field which we use and cache it.
*
* @param array $order_ids Order Ids to prime cache for.
* @param array $query_vars Query vars for the query.
*/
private function prime_raw_meta_cache_for_orders( $order_ids, $query_vars ) {
global $wpdb;
if ( isset( $query_vars['fields'] ) && 'all' !== $query_vars['fields'] ) {
if ( is_array( $query_vars['fields'] ) && ! in_array( 'meta_data', $query_vars['fields'] ) ) {
return;
}
}
$cache_keys_mapping = array();
foreach ( $order_ids as $order_id ) {
$cache_keys_mapping[ $order_id ] = WC_Order::generate_meta_cache_key( $order_id, 'orders' );
}
$cache_values = wc_cache_get_multiple( array_values( $cache_keys_mapping ), 'orders' );
$non_cached_ids = array();
foreach ( $order_ids as $order_id ) {
if ( false === $cache_values[ $cache_keys_mapping[ $order_id ] ] ) {
$non_cached_ids[] = $order_id;
}
}
if ( empty( $non_cached_ids ) ) {
return;
}
$order_ids = esc_sql( $non_cached_ids );
$order_ids_in = "'" . implode( "', '", $order_ids ) . "'";
$raw_meta_data_array = $wpdb->get_results(
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
"SELECT post_id as object_id, meta_id, meta_key, meta_value
FROM {$wpdb->postmeta}
WHERE post_id IN ( $order_ids_in )
ORDER BY post_id"
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
);
$raw_meta_data_collection = array_reduce(
$raw_meta_data_array,
function ( $collection, $raw_meta_data ) {
if ( ! isset( $collection[ $raw_meta_data->object_id ] ) ) {
$collection[ $raw_meta_data->object_id ] = array();
}
$collection[ $raw_meta_data->object_id ][] = $raw_meta_data;
return $collection;
},
array()
);
WC_Order::prime_raw_meta_data_cache( $raw_meta_data_collection, 'orders' );
}
/**
* Return the order type of a given item which belongs to WC_Order.
*

View File

@ -47,12 +47,15 @@ class WC_Order_Refund_Data_Store_CPT extends Abstract_WC_Order_Data_Store_CPT im
*/
public function delete( &$order, $args = array() ) {
$id = $order->get_id();
$parent_order_id = $order->get_parent_id();
$refund_cache_key = WC_Cache_Helper::get_cache_prefix( 'orders' ) . 'refunds' . $parent_order_id;
if ( ! $id ) {
return;
}
wp_delete_post( $id );
wp_cache_delete( $refund_cache_key, 'orders' );
$order->set_id( 0 );
do_action( 'woocommerce_delete_order_refund', $id );
}

View File

@ -170,8 +170,8 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
array(
'name' => $post_object->post_title,
'slug' => $post_object->post_name,
'date_created' => 0 < $post_object->post_date_gmt ? wc_string_to_timestamp( $post_object->post_date_gmt ) : null,
'date_modified' => 0 < $post_object->post_modified_gmt ? wc_string_to_timestamp( $post_object->post_modified_gmt ) : null,
'date_created' => $this->string_to_timestamp( $post_object->post_date_gmt ),
'date_modified' => $this->string_to_timestamp( $post_object->post_modified_gmt ),
'status' => $post_object->post_status,
'description' => $post_object->post_content,
'short_description' => $post_object->post_excerpt,

View File

@ -62,8 +62,8 @@ class WC_Product_Variation_Data_Store_CPT extends WC_Product_Data_Store_CPT impl
array(
'name' => $post_object->post_title,
'slug' => $post_object->post_name,
'date_created' => 0 < $post_object->post_date_gmt ? wc_string_to_timestamp( $post_object->post_date_gmt ) : null,
'date_modified' => 0 < $post_object->post_modified_gmt ? wc_string_to_timestamp( $post_object->post_modified_gmt ) : null,
'date_created' => $this->string_to_timestamp( $post_object->post_date_gmt ),
'date_modified' => $this->string_to_timestamp( $post_object->post_modified_gmt ),
'status' => $post_object->post_status,
'menu_order' => $post_object->menu_order,
'reviews_allowed' => 'open' === $post_object->comment_status,

View File

@ -1048,11 +1048,14 @@ class WC_Email extends WC_Settings_API {
var view = '" . esc_js( __( 'View template', 'woocommerce' ) ) . "';
var hide = '" . esc_js( __( 'Hide template', 'woocommerce' ) ) . "';
jQuery( 'a.toggle_editor' ).text( view ).toggle( function() {
jQuery( this ).text( hide ).closest(' .template' ).find( '.editor' ).slideToggle();
return false;
}, function() {
jQuery( this ).text( view ).closest( '.template' ).find( '.editor' ).slideToggle();
jQuery( 'a.toggle_editor' ).text( view ).click( function() {
var label = hide;
if ( jQuery( this ).closest(' .template' ).find( '.editor' ).is(':visible') ) {
var label = view;
}
jQuery( this ).text( label ).closest(' .template' ).find( '.editor' ).slideToggle();
return false;
} );

View File

@ -58,7 +58,9 @@ class WC_REST_Order_Refunds_V2_Controller extends WC_REST_Orders_V2_Controller {
*/
public function register_routes() {
register_rest_route(
$this->namespace, '/' . $this->rest_base, array(
$this->namespace,
'/' . $this->rest_base,
array(
'args' => array(
'order_id' => array(
'description' => __( 'The order ID.', 'woocommerce' ),
@ -82,7 +84,9 @@ class WC_REST_Order_Refunds_V2_Controller extends WC_REST_Orders_V2_Controller {
);
register_rest_route(
$this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array(
$this->namespace,
'/' . $this->rest_base . '/(?P<id>[\d]+)',
array(
'args' => array(
'order_id' => array(
'description' => __( 'The order ID.', 'woocommerce' ),
@ -140,7 +144,7 @@ class WC_REST_Order_Refunds_V2_Controller extends WC_REST_Orders_V2_Controller {
$data = $object->get_data();
$format_decimal = array( 'amount' );
$format_date = array( 'date_created' );
$format_line_items = array( 'line_items' );
$format_line_items = array( 'line_items', 'shipping_lines', 'tax_lines', 'fee_lines' );
// Format decimal values.
foreach ( $format_decimal as $key ) {
@ -169,6 +173,9 @@ class WC_REST_Order_Refunds_V2_Controller extends WC_REST_Orders_V2_Controller {
'refunded_payment' => $data['refunded_payment'],
'meta_data' => $data['meta_data'],
'line_items' => $data['line_items'],
'shipping_lines' => $data['shipping_lines'],
'tax_lines' => $data['tax_lines'],
'fee_lines' => $data['fee_lines'],
);
}

View File

@ -283,6 +283,7 @@ abstract class WC_REST_CRUD_Controller extends WC_REST_Posts_Controller {
$args['post_parent__in'] = $request['parent'];
$args['post_parent__not_in'] = $request['parent_exclude'];
$args['s'] = $request['search'];
$args['fields'] = $this->get_fields_for_response( $request );
if ( 'date' === $args['orderby'] ) {
$args['orderby'] = 'date ID';

View File

@ -2501,3 +2501,23 @@ function wc_is_running_from_async_action_scheduler() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
return isset( $_REQUEST['action'] ) && 'as_async_request_queue_runner' === $_REQUEST['action'];
}
/**
* Polyfill for wp_cache_get_multiple for WP versions before 5.5.
*
* @param array $keys Array of keys to get from group.
* @param string $group Optional. Where the cache contents are grouped. Default empty.
* @param bool $force Optional. Whether to force an update of the local cache from the persistent
* cache. Default false.
* @return array|bool Array of values.
*/
function wc_cache_get_multiple( $keys, $group = '', $force = false ) {
if ( function_exists( 'wp_cache_get_multiple' ) ) {
return wp_cache_get_multiple( $keys, $group, $force );
}
$values = array();
foreach ( $keys as $key ) {
$values[ $key ] = wp_cache_get( $key, $group, $force );
}
return $values;
}

View File

@ -71,7 +71,7 @@ function wc_get_orders( $args ) {
*
* @since 2.2
*
* @param mixed $the_order Post object or post ID of the order.
* @param mixed $the_order Post object or post ID of the order.
*
* @return bool|WC_Order|WC_Order_Refund
*/

View File

@ -2491,7 +2491,7 @@ if ( ! function_exists( 'woocommerce_output_product_categories' ) ) {
foreach ( $product_categories as $category ) {
wc_get_template(
'content-product_cat.php',
'content-product-cat.php',
array(
'category' => $category,
)

4100
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
{
"name": "woocommerce",
"title": "WooCommerce",
"version": "4.8.0",
"version": "4.9.0",
"homepage": "https://woocommerce.com/",
"repository": {
"type": "git",
@ -13,7 +13,7 @@
"wp_org_slug": "woocommerce"
},
"scripts": {
"install": "lerna bootstrap",
"install": "lerna bootstrap --hoist",
"build": "./bin/build-zip.sh",
"build:core": "grunt && npm run makepot",
"build:dev": "npm run build:core && npm run build:packages",
@ -34,11 +34,11 @@
"git:update-hooks": "rm -r .git/hooks && mkdir -p .git/hooks && node ./node_modules/husky/husky.js install"
},
"devDependencies": {
"@babel/cli": "7.12.0",
"@babel/core": "7.12.0",
"@babel/polyfill": "7.11.5",
"@babel/preset-env": "7.12.0",
"@babel/register": "7.12.0",
"@babel/cli": "7.12.1",
"@babel/core": "7.12.3",
"@babel/polyfill": "7.12.1",
"@babel/preset-env": "7.12.1",
"@babel/register": "7.12.1",
"@typescript-eslint/eslint-plugin": "3.10.1",
"@typescript-eslint/experimental-utils": "3.10.1",
"@typescript-eslint/parser": "3.10.1",
@ -48,8 +48,8 @@
"@woocommerce/e2e-utils": "file:tests/e2e/utils",
"@wordpress/babel-plugin-import-jsx-pragma": "1.1.3",
"@wordpress/babel-preset-default": "3.0.2",
"@wordpress/e2e-test-utils": "4.6.0",
"@wordpress/eslint-plugin": "7.1.0",
"@wordpress/e2e-test-utils": "^4.6.0",
"@wordpress/eslint-plugin": "7.3.0",
"autoprefixer": "9.8.6",
"babel-eslint": "10.1.0",
"chai": "4.2.0",
@ -77,17 +77,16 @@
"gruntify-eslint": "5.0.0",
"husky": "4.3.0",
"istanbul": "1.0.0-alpha.2",
"jest": "25.1.0",
"jest": "^25.1.0",
"lerna": "3.22.1",
"lint-staged": "9.5.0",
"mocha": "7.2.0",
"node-sass": "4.13.1",
"node-sass": "4.14.1",
"prettier": "npm:wp-prettier@2.0.5",
"puppeteer": "^2.1.1",
"stylelint": "12.0.1",
"stylelint-config-wordpress": "16.0.0",
"typescript": "3.9.7",
"webpack": "4.44.1",
"webpack": "4.44.2",
"webpack-cli": "3.3.12",
"wp-textdomain": "1.0.1"
},

View File

@ -4,7 +4,7 @@ Tags: e-commerce, store, sales, sell, woo, shop, cart, checkout, downloadable, d
Requires at least: 5.3
Tested up to: 5.5
Requires PHP: 7.0
Stable tag: 4.6.1
Stable tag: 4.6.2
License: GPLv3
License URI: https://www.gnu.org/licenses/gpl-3.0.html
@ -160,7 +160,7 @@ WooCommerce comes with some sample data you can use to see how products look; im
== Changelog ==
= 4.8.0 - 2020-12-xx =
= 4.9.0 - 2021-01-xx =
[See changelog for all versions](https://raw.githubusercontent.com/woocommerce/woocommerce/master/changelog.txt).

View File

@ -36,6 +36,7 @@ do_action( 'woocommerce_before_add_to_cart_form' ); ?>
),
$product
);
$show_add_to_cart_button = false;
do_action( 'woocommerce_grouped_product_list_before', $grouped_product_columns, $quantites_required, $product );
@ -45,6 +46,10 @@ do_action( 'woocommerce_before_add_to_cart_form' ); ?>
$post = $post_object; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
setup_postdata( $post );
if ( $grouped_product_child->is_in_stock() ) {
$show_add_to_cart_button = true;
}
echo '<tr id="product-' . esc_attr( $grouped_product_child->get_id() ) . '" class="woocommerce-grouped-product-list-item ' . esc_attr( implode( ' ', wc_get_product_class( '', $grouped_product_child ) ) ) . '">';
// Output columns for each product.
@ -107,7 +112,7 @@ do_action( 'woocommerce_before_add_to_cart_form' ); ?>
<input type="hidden" name="add-to-cart" value="<?php echo esc_attr( $product->get_id() ); ?>" />
<?php if ( $quantites_required ) : ?>
<?php if ( $quantites_required && $show_add_to_cart_button ) : ?>
<?php do_action( 'woocommerce_before_add_to_cart_button' ); ?>

View File

@ -5,7 +5,7 @@ if [[ ${RUN_PHPCS} == 1 ]] || [[ ${RUN_E2E} == 1 ]]; then
fi
if [[ ${RUN_CODE_COVERAGE} == 1 ]]; then
phpdbg -qrr $HOME/.composer/vendor/bin/phpunit -d memory_limit=-1 -c phpunit.xml --coverage-clover=coverage.clover --exclude-group=timeout $@
phpdbg -qrr ./vendor/bin/phpunit -d memory_limit=-1 -c phpunit.xml --coverage-clover=coverage.clover --exclude-group=timeout $@
else
$HOME/.composer/vendor/bin/phpunit -c phpunit.xml $@
vendor/bin/phpunit -c phpunit.xml $@
fi

View File

@ -0,0 +1,19 @@
# Unreleased
## Breaking Changes
- The `HTTPClientFactory` API was changed to make it easier to configure instances with
## Added
- `HTTPClientFactory` methods `withIndexPermalinks()` and `withoutIndexPermalinks()` to enable/disable API pretty permalinks
- Expanded properties of `AbstractProduct` model type
- Added `list`, `read`, `update`, and `delete` operations for `SimpleProduct` repositories
## Changes
- Added a tranformation layer between API responses and internal models
# 0.1.0
- Initial/beta release

View File

@ -24,24 +24,14 @@ The simplest way to use the client is directly:
import { HTTPClientFactory } from '@woocommerce/api';
// You can create an API client using the client factory with pre-configured middleware for convenience.
let httpClient = HTTPClientFactory.withBasicAuth(
// The base URL of your REST API.
'https://example.com/wp-json/',
// The username for your WordPress user.
'username',
// The password for your WordPress user.
'password',
);
let client = HTTPClientFactory.build( 'https://example.com' )
.withBasicAuth( 'username', 'password' )
.create();
// You can also create an API client configured for requests using OAuth.
httpClient = HTTPClientFactory.withOAuth(
// The base URL of your REST API.
'https://example.com/wp-json/',
// The OAuth API Key's consumer secret.
'consumer_secret',
// The OAuth API Key's consumer password.
'consumer_pasword',
);
client = HTTPClientFactory.build( 'https://example.com' )
.withOAuth( 'consumer_secret', 'consumer_password' )
.create();
// You can then use the client to make API requests.
httpClient.get( '/wc/v3/products' ).then( ( response ) => {
@ -54,6 +44,7 @@ httpClient.get( '/wc/v3/products' ).then( ( response ) => {
}, ( error ) => {
// Handle errors that may have come up.
} );
```
### Repositories
@ -66,7 +57,9 @@ import { SimpleProduct } from '@woocommerce/api';
// Prepare the HTTP client that will be consumed by the repository.
// This is necessary so that it can make requests to the REST API.
const httpClient = HTTPClientFactory.withBasicAuth( 'https://example.com/wp-json/','username','password' );
const httpClient = HTTPClientFactory.build( 'https://example.com' )
.withBasicAuth( 'username', 'password' )
.create();
const repository = SimpleProduct.restRepository( httpClient );

View File

@ -21,7 +21,8 @@
"!*.tsbuildinfo",
"!/dist/**/__tests__/",
"!/dist/**/__mocks__/",
"!/dist/**/__snapshops__/"
"!/dist/**/__snapshops__/",
"!/dist/**/__test_data__/"
],
"sideEffects": false,
"scripts": {
@ -42,7 +43,7 @@
"@types/jest": "25.2.1",
"@types/moxios": "^0.4.9",
"@types/node": "13.13.5",
"jest": "25.5.4",
"jest": "^25.1.0",
"jest-mock-extended": "^1.0.10",
"moxios": "0.4.0",
"ts-jest": "25.5.0",

View File

@ -0,0 +1,13 @@
import { Model } from '../models/model';
/**
* A dummy model that can be used in test files.
*/
export class DummyModel extends Model {
public name: string = '';
public constructor( partial?: Partial< DummyModel > ) {
super();
Object.assign( this, partial );
}
}

View File

@ -12,15 +12,8 @@ import {
UpdatesChildModels,
UpdatesModels,
} from '../model-repository';
import { DummyModel } from '../../__test_data__/dummy-model';
class DummyModel extends Model {
public name: string = '';
public constructor( partial?: Partial< DummyModel > ) {
super();
Object.assign( this, partial );
}
}
type DummyModelParams = ModelRepositoryParams< DummyModel, never, { search: string }, 'name' >
class DummyChildModel extends Model {

View File

@ -0,0 +1,100 @@
import { ModelTransformation, ModelTransformer } from '../model-transformer';
import { DummyModel } from '../../__test_data__/dummy-model';
class DummyTransformation implements ModelTransformation {
public readonly fromModelOrder: number;
private readonly fn: ( ( p: any ) => any ) | null;
public constructor( order: number, fn: ( ( p: any ) => any ) | null ) {
this.fromModelOrder = order;
this.fn = fn;
}
public fromModel( properties: any ): any {
if ( ! this.fn ) {
return properties;
}
return this.fn( properties );
}
public toModel( properties: any ): any {
if ( ! this.fn ) {
return properties;
}
return this.fn( properties );
}
}
describe( 'ModelTransformer', () => {
it( 'should order transformers correctly', () => {
const fn1 = jest.fn();
fn1.mockReturnValue( { name: 'fn1' } );
const fn2 = jest.fn();
fn2.mockReturnValue( { name: 'fn2' } );
const transformer = new ModelTransformer< DummyModel >(
[
// Ensure the orders are backwards so sorting is tested.
new DummyTransformation( 1, fn2 ),
new DummyTransformation( 0, fn1 ),
],
);
let transformed = transformer.fromModel( new DummyModel( { name: 'fn0' } ) );
expect( fn1 ).toHaveBeenCalledWith( { name: 'fn0' } );
expect( fn2 ).toHaveBeenCalledWith( { name: 'fn1' } );
expect( transformed ).toMatchObject( { name: 'fn2' } );
// Reset and make sure "toModel" happens in reverse order.
fn1.mockClear();
fn2.mockClear();
transformed = transformer.toModel( DummyModel, { name: 'fn3' } );
expect( fn2 ).toHaveBeenCalledWith( { name: 'fn3' } );
expect( fn1 ).toHaveBeenCalledWith( { name: 'fn2' } );
expect( transformed ).toMatchObject( { name: 'fn1' } );
} );
it( 'should transform to model', () => {
const transformer = new ModelTransformer< DummyModel >(
[
new DummyTransformation(
0,
( p: any ) => {
p.name = 'Transformed-' + p.name;
return p;
},
),
],
);
const model = transformer.toModel( DummyModel, { name: 'Test' } );
expect( model ).toBeInstanceOf( DummyModel );
expect( model.name ).toEqual( 'Transformed-Test' );
} );
it( 'should transform from model', () => {
const transformer = new ModelTransformer< DummyModel >(
[
new DummyTransformation(
0,
( p: any ) => {
p.name = 'Transformed-' + p.name;
return p;
},
),
],
);
const transformed = transformer.fromModel( new DummyModel( { name: 'Test' } ) );
expect( transformed ).not.toBeInstanceOf( DummyModel );
expect( transformed.name ).toEqual( 'Transformed-Test' );
} );
} );

View File

@ -31,13 +31,14 @@ export interface ModelRepositoryParams<
/**
* These helpers will extract information about a model from its repository params to be used in the repository.
*/
type ModelClass< T extends ModelRepositoryParams > = [ T ] extends [ ModelRepositoryParams< infer X > ] ? X : never;
type ParentID< T extends ModelRepositoryParams > = [ T ] extends [ ModelRepositoryParams< any, infer X > ] ? X : never;
export type ModelClass< T extends ModelRepositoryParams > = [ T ] extends [ ModelRepositoryParams< infer X > ] ? X : never;
export type ParentID< T extends ModelRepositoryParams > = [ T ] extends [ ModelRepositoryParams< any, infer X > ] ? X : never;
export type HasParent< T extends ModelRepositoryParams, P, C > = [ ParentID< T > ] extends [ never ] ? C : P;
type ListParams< T extends ModelRepositoryParams > = [ T ] extends [ ModelRepositoryParams< any, any, infer X > ] ? X : never;
type PickUpdateParams<T, K extends keyof T> = { [P in K]?: T[P]; };
type UpdateParams< T extends ModelRepositoryParams > = [ T ] extends [ ModelRepositoryParams< infer C, any, any, infer X > ] ?
( [ X ] extends [ keyof C ] ? Pick< C, X > : never ) :
( [ X ] extends [ keyof C ] ? PickUpdateParams< C, X > : never ) :
never;
type HasParent< T extends ModelRepositoryParams, P, C > = [ ParentID< T > ] extends [ never ] ? C : P;
/**
* A callback for listing models using a data source.

View File

@ -0,0 +1,113 @@
import { Model } from '../models/model';
import { ModelConstructor } from '../models/shared-types';
/**
* An interface for an object that can perform transformations both to and from a representation
* and return the input data after performing the desired transformation.
*
* @interface ModelTransformation
*/
export interface ModelTransformation {
/**
* The order of execution for the transformer.
* - For "fromModel" higher numbers execute later.
* - For "toModel" the order is reversed.
*
* @type {number}
*/
readonly fromModelOrder: number;
/**
* Performs a transformation from model properties to raw properties.
*
* @param {*} properties The properties to transform.
* @return {*} The transformed properties.
*/
fromModel( properties: any ): any;
/**
* Performs a transformation from raw properties to model properties.
*
* @param {*} properties The properties to transform.
* @return {*} The transformed properties.
*/
toModel( properties: any ): any;
}
/**
* An enum for defining the "toModel" transformation order values.
*/
export enum TransformationOrder {
First = 0,
Normal = 500000,
Last = 1000000,
/**
* A special value reserved for transformations that MUST come after all orders due to
* the way that they destroy the property keys or values.
*/
VeryLast = 2000000
}
/**
* A class for transforming models to/from a generic representation.
*/
export class ModelTransformer< T extends Model > {
/**
* An array of transformations to use when converting data to/from models.
*
* @type {Array.<ModelTransformation>}
* @private
*/
private transformations: readonly ModelTransformation[];
/**
* Creates a new model transformer instance.
*
* @param {Array.<ModelTransformation>} transformations The transformations to use.
*/
public constructor( transformations: ModelTransformation[] ) {
// Ensure that the transformations are sorted by priority.
transformations.sort( ( a, b ) => ( a.fromModelOrder > b.fromModelOrder ) ? 1 : -1 );
this.transformations = transformations;
}
/**
* Takes the input model and runs all of the transformations on it before returning the data.
*
* @param {Partial.<T>} model The model to transform.
* @return {*} The transformed data.
* @template T
*/
public fromModel( model: Partial< T > ): any {
// Convert the model class to raw properties so that the transformations can be simple.
const raw = Object.assign( {}, model );
return this.transformations.reduce(
( properties: any, transformer: ModelTransformation ) => {
return transformer.fromModel( properties );
},
raw,
);
}
/**
* Takes the input data and runs all of the transformations on it before returning the created model.
*
* @param {Function.<T>} modelClass The model class we're trying to create.
* @param {*} data The data we're transforming.
* @return {T} The transformed model.
* @template T
*/
public toModel( modelClass: ModelConstructor< T >, data: any ): T {
const transformed: any = this.transformations.reduceRight(
( properties: any, transformer: ModelTransformation ) => {
return transformer.toModel( properties );
},
data,
);
return new modelClass( transformed );
}
}

View File

@ -0,0 +1,56 @@
import { AddPropertyTransformation } from '../add-property-transformation';
describe( 'AddPropertyTransformation', () => {
let transformation: AddPropertyTransformation;
beforeEach( () => {
transformation = new AddPropertyTransformation(
{ toProperty: 'Test' },
{ fromProperty: 'Test' },
);
} );
it( 'should add property when missing', () => {
let transformed = transformation.toModel( { id: 1, name: 'Test' } );
expect( transformed ).toMatchObject(
{
id: 1,
name: 'Test',
toProperty: 'Test',
},
);
transformed = transformation.fromModel( { id: 1, name: 'Test' } );
expect( transformed ).toMatchObject(
{
id: 1,
name: 'Test',
fromProperty: 'Test',
},
);
} );
it( 'should not add property when present', () => {
let transformed = transformation.toModel( { id: 1, name: 'Test', toProperty: 'Existing' } );
expect( transformed ).toMatchObject(
{
id: 1,
name: 'Test',
toProperty: 'Existing',
},
);
transformed = transformation.fromModel( { id: 1, name: 'Test', fromProperty: 'Existing' } );
expect( transformed ).toMatchObject(
{
id: 1,
name: 'Test',
fromProperty: 'Existing',
},
);
} );
} );

View File

@ -0,0 +1,26 @@
import { CustomTransformation } from '../custom-transformation';
describe( 'CustomTransformation', () => {
it( 'should do nothing without hooks', () => {
const transformation = new CustomTransformation( 0, null, null );
const expected = { test: 'Test' };
expect( transformation.toModel( expected ) ).toMatchObject( expected );
expect( transformation.fromModel( expected ) ).toMatchObject( expected );
} );
it( 'should execute hooks', () => {
const toHook = jest.fn();
toHook.mockReturnValue( { toModel: 'Test' } );
const fromHook = jest.fn();
fromHook.mockReturnValue( { fromModel: 'Test' } );
const transformation = new CustomTransformation( 0, toHook, fromHook );
expect( transformation.toModel( { test: 'Test' } ) ).toMatchObject( { toModel: 'Test' } );
expect( toHook ).toHaveBeenCalledWith( { test: 'Test' } );
expect( transformation.fromModel( { test: 'Test' } ) ).toMatchObject( { fromModel: 'Test' } );
expect( fromHook ).toHaveBeenCalledWith( { test: 'Test' } );
} );
} );

View File

@ -0,0 +1,31 @@
import { IgnorePropertyTransformation } from '../ignore-property-transformation';
describe( 'IgnorePropertyTransformation', () => {
let transformation: IgnorePropertyTransformation;
beforeEach( () => {
transformation = new IgnorePropertyTransformation( [ 'skip' ] );
} );
it( 'should remove ignored properties', () => {
let transformed = transformation.fromModel(
{
test: 'Test',
skip: 'Test',
},
);
expect( transformed ).toHaveProperty( 'test', 'Test' );
expect( transformed ).not.toHaveProperty( 'skip' );
transformed = transformation.toModel(
{
test: 'Test',
skip: 'Test',
},
);
expect( transformed ).toHaveProperty( 'test', 'Test' );
expect( transformed ).not.toHaveProperty( 'skip' );
} );
} );

View File

@ -0,0 +1,28 @@
import { KeyChangeTransformation } from '../key-change-transformation';
import { DummyModel } from '../../../__test_data__/dummy-model';
describe( 'KeyChangeTransformation', () => {
let transformation: KeyChangeTransformation< DummyModel >;
beforeEach( () => {
transformation = new KeyChangeTransformation< DummyModel >(
{
name: 'new-name',
},
);
} );
it( 'should transform to model', () => {
const transformed = transformation.toModel( { 'new-name': 'Test Name' } );
expect( transformed ).toHaveProperty( 'name', 'Test Name' );
expect( transformed ).not.toHaveProperty( 'new-name' );
} );
it( 'should transform from model', () => {
const transformed = transformation.fromModel( { name: 'Test Name' } );
expect( transformed ).toHaveProperty( 'new-name', 'Test Name' );
expect( transformed ).not.toHaveProperty( 'name' );
} );
} );

View File

@ -0,0 +1,52 @@
import { ModelTransformerTransformation } from '../model-transformer-transformation';
import { ModelTransformer } from '../../model-transformer';
import { mock, MockProxy } from 'jest-mock-extended';
import { DummyModel } from '../../../__test_data__/dummy-model';
describe( 'ModelTransformerTransformation', () => {
let mockTransformer: MockProxy< ModelTransformer< any > > & ModelTransformer< any >;
let transformation: ModelTransformerTransformation< any >;
beforeEach( () => {
mockTransformer = mock< ModelTransformer< any > >();
transformation = new ModelTransformerTransformation< DummyModel >(
'test',
DummyModel,
mockTransformer,
);
} );
it( 'should execute child transformer', () => {
mockTransformer.toModel.mockReturnValue( { toModel: 'Test' } );
let transformed = transformation.toModel( { test: 'Test' } );
expect( transformed ).toMatchObject( { test: { toModel: 'Test' } } );
expect( mockTransformer.toModel ).toHaveBeenCalledWith( DummyModel, 'Test' );
mockTransformer.fromModel.mockReturnValue( { fromModel: 'Test' } );
transformed = transformation.fromModel( { test: 'Test' } );
expect( transformed ).toMatchObject( { test: { fromModel: 'Test' } } );
expect( mockTransformer.fromModel ).toHaveBeenCalledWith( 'Test' );
} );
it( 'should execute child transformer on array', () => {
mockTransformer.toModel.mockReturnValue( { toModel: 'Test' } );
let transformed = transformation.toModel( { test: [ 'Test', 'Test2' ] } );
expect( transformed ).toMatchObject( { test: [ { toModel: 'Test' }, { toModel: 'Test' } ] } );
expect( mockTransformer.toModel ).toHaveBeenCalledWith( DummyModel, 'Test' );
expect( mockTransformer.toModel ).toHaveBeenCalledWith( DummyModel, 'Test2' );
mockTransformer.fromModel.mockReturnValue( { fromModel: 'Test' } );
transformed = transformation.fromModel( { test: [ 'Test', 'Test2' ] } );
expect( transformed ).toMatchObject( { test: [ { fromModel: 'Test' }, { fromModel: 'Test' } ] } );
expect( mockTransformer.fromModel ).toHaveBeenCalledWith( 'Test' );
expect( mockTransformer.fromModel ).toHaveBeenCalledWith( 'Test2' );
} );
} );

View File

@ -0,0 +1,108 @@
import { PropertyType, PropertyTypeTransformation } from '../property-type-transformation';
describe( 'PropertyTypeTransformation', () => {
let transformation: PropertyTypeTransformation;
beforeEach( () => {
transformation = new PropertyTypeTransformation(
{
string: PropertyType.String,
integer: PropertyType.Integer,
float: PropertyType.Float,
boolean: PropertyType.Boolean,
date: PropertyType.Date,
callback: ( value: string ) => 'Transformed-' + value,
},
);
} );
it( 'should convert strings', () => {
let transformed = transformation.toModel( { string: 'Test' } );
expect( transformed.string ).toStrictEqual( 'Test' );
transformed = transformation.fromModel( { string: 'Test' } );
expect( transformed.string ).toStrictEqual( 'Test' );
} );
it( 'should convert integers', () => {
let transformed = transformation.toModel( { integer: '100' } );
expect( transformed.integer ).toStrictEqual( 100 );
transformed = transformation.fromModel( { integer: 100 } );
expect( transformed.integer ).toStrictEqual( '100' );
} );
it( 'should convert floats', () => {
let transformed = transformation.toModel( { float: '2.5' } );
expect( transformed.float ).toStrictEqual( 2.5 );
transformed = transformation.fromModel( { float: 2.5 } );
expect( transformed.float ).toStrictEqual( '2.5' );
} );
it( 'should convert booleans', () => {
let transformed = transformation.toModel( { boolean: 'true' } );
expect( transformed.boolean ).toStrictEqual( true );
transformed = transformation.fromModel( { boolean: false } );
expect( transformed.boolean ).toStrictEqual( 'false' );
} );
it( 'should convert dates', () => {
let transformed = transformation.toModel( { date: '2020-11-06T03:11:41.000Z' } );
expect( transformed.date ).toStrictEqual( new Date( '2020-11-06T03:11:41.000Z' ) );
transformed = transformation.fromModel( { date: new Date( '2020-11-06T03:11:41.000Z' ) } );
expect( transformed.date ).toStrictEqual( '2020-11-06T03:11:41.000Z' );
} );
it( 'should use conversion callbacks', () => {
let transformed = transformation.toModel( { callback: 'Test' } );
expect( transformed.callback ).toStrictEqual( 'Transformed-Test' );
transformed = transformation.fromModel( { callback: 'Test' } );
expect( transformed.callback ).toStrictEqual( 'Transformed-Test' );
} );
it( 'should convert arrays', () => {
let transformed = transformation.toModel( { integer: [ '100', '200', '300' ] } );
expect( transformed.integer ).toStrictEqual( [ 100, 200, 300 ] );
transformed = transformation.fromModel( { integer: [ 100, 200, 300 ] } );
expect( transformed.integer ).toStrictEqual( [ '100', '200', '300' ] );
} );
it( 'should do nothing without property', () => {
let transformed = transformation.toModel( { name: 'Test' } );
expect( transformed.name ).toStrictEqual( 'Test' );
transformed = transformation.fromModel( { name: 'Test' } );
expect( transformed.name ).toStrictEqual( 'Test' );
} );
it( 'should preserve null', () => {
let transformed = transformation.toModel( { integer: null } );
expect( transformed.integer ).toStrictEqual( null );
transformed = transformation.fromModel( { integer: null } );
expect( transformed.integer ).toStrictEqual( null );
} );
} );

View File

@ -0,0 +1,76 @@
import { ModelTransformation, TransformationOrder } from '../model-transformer';
/**
* @typedef AdditionalProperties
* @alias Object.<string,string>
*/
type AdditionalProperties = { [ key: string ]: any };
/**
* A model transformation that adds a property with
* a default value if it is not already set.
*/
export class AddPropertyTransformation implements ModelTransformation {
public readonly fromModelOrder = TransformationOrder.Normal;
/**
*The additional properties to add when executing toModel.
*
* @type {AdditionalProperties}
* @private
*/
private readonly toProperties: AdditionalProperties;
/**
* The additional properties to add when executing fromModel.
*
* @type {AdditionalProperties}
* @private
*/
private readonly fromProperties: AdditionalProperties;
/**
* Creates a new transformation.
*
* @param {AdditionalProperties} toProperties The properties to add when executing toModel.
* @param {AdditionalProperties} fromProperties The properties to add when executing fromModel.
*/
public constructor( toProperties: AdditionalProperties, fromProperties: AdditionalProperties ) {
this.toProperties = toProperties;
this.fromProperties = fromProperties;
}
/**
* Performs a transformation from model properties to raw properties.
*
* @param {*} properties The properties to transform.
* @return {*} The transformed properties.
*/
public fromModel( properties: any ): any {
for ( const key in this.fromProperties ) {
if ( properties.hasOwnProperty( key ) ) {
continue;
}
properties[ key ] = this.fromProperties[ key ];
}
return properties;
}
/**
* Performs a transformation from raw properties to model properties.
*
* @param {*} properties The properties to transform.
* @return {*} The transformed properties.
*/
public toModel( properties: any ): any {
for ( const key in this.toProperties ) {
if ( properties.hasOwnProperty( key ) ) {
continue;
}
properties[ key ] = this.toProperties[ key ];
}
return properties;
}
}

View File

@ -0,0 +1,78 @@
import { ModelTransformation } from '../model-transformer';
/**
* A callback for transforming model properties.
*
* @callback TransformationCallback
* @param {*} properties The properties to transform.
* @return {*} The transformed properties.
*/
type TransformationCallback = ( properties: any ) => any;
/**
* A model transformer for executing arbitrary callbacks on input properties.
*/
export class CustomTransformation implements ModelTransformation {
public readonly fromModelOrder: number;
/**
* The hook to run for toModel.
*
* @type {TransformationCallback|null}
* @private
*/
private readonly toHook: TransformationCallback | null;
/**
* The hook to run for fromModel.
*
* @type {TransformationCallback|null}
* @private
*/
private readonly fromHook: TransformationCallback | null;
/**
* Creates a new transformation.
*
* @param {number} order The order for the transformation.
* @param {TransformationCallback|null} toHook The hook to run for toModel.
* @param {TransformationCallback|null} fromHook The hook to run for fromModel.
*/
public constructor(
order: number,
toHook: TransformationCallback | null,
fromHook: TransformationCallback | null,
) {
this.fromModelOrder = order;
this.toHook = toHook;
this.fromHook = fromHook;
}
/**
* Performs a transformation from model properties to raw properties.
*
* @param {*} properties The properties to transform.
* @return {*} The transformed properties.
*/
public fromModel( properties: any ): any {
if ( ! this.fromHook ) {
return properties;
}
return this.fromHook( properties );
}
/**
* Performs a transformation from raw properties to model properties.
*
* @param {*} properties The properties to transform.
* @return {*} The transformed properties.
*/
public toModel( properties: any ): any {
if ( ! this.toHook ) {
return properties;
}
return this.toHook( properties );
}
}

View File

@ -0,0 +1,50 @@
import { ModelTransformation, TransformationOrder } from '../model-transformer';
export class IgnorePropertyTransformation implements ModelTransformation {
public readonly fromModelOrder = TransformationOrder.Normal;
/**
* A list of properties that should be removed.
*
* @type {Array.<string>}
* @private
*/
private readonly ignoreList: readonly string[];
/**
* Creates a new transformation.
*
* @param {Array.<string>} ignoreList The properties to ignore.
*/
public constructor( ignoreList: string[] ) {
this.ignoreList = ignoreList;
}
/**
* Performs a transformation from model properties to raw properties.
*
* @param {*} properties The properties to transform.
* @return {*} The transformed properties.
*/
public fromModel( properties: any ): any {
for ( const key of this.ignoreList ) {
delete properties[ key ];
}
return properties;
}
/**
* Performs a transformation from raw properties to model properties.
*
* @param {*} properties The properties to transform.
* @return {*} The transformed properties.
*/
public toModel( properties: any ): any {
for ( const key of this.ignoreList ) {
delete properties[ key ];
}
return properties;
}
}

View File

@ -0,0 +1,82 @@
import { ModelTransformation, TransformationOrder } from '../model-transformer';
import { Model } from '../../models/model';
/**
* @typedef KeyChanges
* @alias Object.<string,string>
*/
type KeyChanges< T extends Model > = { readonly [ key in keyof Partial< T > ]: string };
/**
* A model transformation that can be used to change property keys between two formats.
* This transformation has a very high priority so that it will be executed after all
* other transformations to prevent the changed key from causing problems.
*/
export class KeyChangeTransformation< T extends Model > implements ModelTransformation {
/**
* Ensure that this transformation always happens at the very end since it changes the keys
* in the transformed object.
*/
public readonly fromModelOrder = TransformationOrder.VeryLast + 1;
/**
* The key change transformations that this object should perform.
* This is structured with the model's property key as the key
* of the object and the raw property key as the value.
*
* @type {KeyChanges}
* @private
*/
private readonly changes: KeyChanges< T >;
/**
* Creates a new transformation.
*
* @param {KeyChanges} changes The changes we want the transformation to make.
*/
public constructor( changes: KeyChanges< T > ) {
this.changes = changes;
}
/**
* Performs a transformation from model properties to raw properties.
*
* @param {*} properties The properties to transform.
* @return {*} The transformed properties.
*/
public fromModel( properties: any ): any {
for ( const key in this.changes ) {
const value = this.changes[ key ];
if ( ! properties.hasOwnProperty( key ) ) {
continue;
}
properties[ value ] = properties[ key ];
delete properties[ key ];
}
return properties;
}
/**
* Performs a transformation from raw properties to model properties.
*
* @param {*} properties The properties to transform.
* @return {*} The transformed properties.
*/
public toModel( properties: any ): any {
for ( const key in this.changes ) {
const value = this.changes[ key ];
if ( ! properties.hasOwnProperty( value ) ) {
continue;
}
properties[ key ] = properties[ value ];
delete properties[ value ];
}
return properties;
}
}

View File

@ -0,0 +1,89 @@
import { ModelTransformation, ModelTransformer, TransformationOrder } from '../model-transformer';
import { Model } from '../../models/model';
import { ModelConstructor } from '../../models/shared-types';
/**
* A model transformation that applies another transformer to a property.
*
* @template T
*/
export class ModelTransformerTransformation< T extends Model > implements ModelTransformation {
public readonly fromModelOrder = TransformationOrder.Normal;
/**
* The property that the transformation should be applied to.
*
* @type {string}
* @private
*/
private readonly property: string;
/**
* The model class we want to transform into.
*
* @type {Function.<T>}
* @private
* @template T
*/
private readonly modelClass: ModelConstructor< T >;
/**
* The transformer that should be used.
*
* @type {ModelTransformer}
* @private
*/
private readonly transformer: ModelTransformer< T >;
/**
* Creates a new transformation.
*
* @param {string} property The property we want to apply the transformer to.
* @param {ModelConstructor.<T>} modelClass The model to transform into.
* @param {ModelTransformer} transformer The transformer we want to apply.
* @template T
*/
public constructor( property: string, modelClass: ModelConstructor< T >, transformer: ModelTransformer< T > ) {
this.property = property;
this.modelClass = modelClass;
this.transformer = transformer;
}
/**
* Performs a transformation from model properties to raw properties.
*
* @param {*} properties The properties to transform.
* @return {*} The transformed properties.
*/
public fromModel( properties: any ): any {
const val = properties[ this.property ];
if ( val ) {
if ( Array.isArray( val ) ) {
properties[ this.property ] = val.map( ( v ) => this.transformer.fromModel( v ) );
} else {
properties[ this.property ] = this.transformer.fromModel( val );
}
}
return properties;
}
/**
* Performs a transformation from raw properties to model properties.
*
* @param {*} properties The properties to transform.
* @return {*} The transformed properties.
*/
public toModel( properties: any ): any {
const val = properties[ this.property ];
if ( val ) {
if ( Array.isArray( val ) ) {
properties[ this.property ] = val.map( ( v ) => this.transformer.toModel( this.modelClass, v ) );
} else {
properties[ this.property ] = this.transformer.toModel( this.modelClass, val );
}
}
return properties;
}
}

View File

@ -0,0 +1,167 @@
import { ModelTransformation, TransformationOrder } from '../model-transformer';
/**
* An enum defining all of the property types that we might want to transform.
*
* @enum {number}
*/
export enum PropertyType {
String,
Integer,
Float,
Boolean,
Date,
}
type PropertyTypeTypes = null | string | number | boolean | Date;
/**
* A callback that can be used to transform property types.
*
* @callback PropertyTypeCallback
* @param {*} value The value to transform.
* @return {*} The transformed value.
*/
type PropertyTypeCallback = ( value: any ) => any;
/**
* The types for all of a model's properties.
*
* @typedef PropertyTypes
* @alias Object.<string,PropertyType>
*/
type PropertyTypes = { [ key: string ]: PropertyType | PropertyTypeCallback };
/**
* A model transformer for converting property types between representation formats.
*/
export class PropertyTypeTransformation implements ModelTransformation {
/**
* We want the type transformation to take place after all of the others,
* since they may be operating on internal data types.
*/
public readonly fromModelOrder = TransformationOrder.VeryLast;
/**
* The property types we will want to transform.
*
* @type {PropertyTypes}
* @private
*/
private readonly types: PropertyTypes;
/**
* Creates a new transformation.
*
* @param {PropertyTypes} types The property types we want to transform.
*/
public constructor( types: PropertyTypes ) {
this.types = types;
}
/**
* Performs a transformation from model properties to raw properties.
*
* @param {*} properties The properties to transform.
* @return {*} The transformed properties.
*/
public fromModel( properties: any ): any {
for ( const key in this.types ) {
if ( ! properties.hasOwnProperty( key ) ) {
continue;
}
const value = properties[ key ];
const type = this.types[ key ];
if ( type instanceof Function ) {
properties[ key ] = type( value );
continue;
}
properties[ key ] = this.convertFrom( value, type );
}
return properties;
}
/**
* Performs a transformation from raw properties to model properties.
*
* @param {*} properties The properties to transform.
* @return {*} The transformed properties.
*/
public toModel( properties: any ): any {
for ( const key in this.types ) {
if ( ! properties.hasOwnProperty( key ) ) {
continue;
}
const value = properties[ key ];
const type = this.types[ key ];
if ( type instanceof Function ) {
properties[ key ] = type( value );
continue;
}
properties[ key ] = this.convertTo( value, type );
}
return properties;
}
/**
* Converts the given value into the requested type.
*
* @param {*} value The value to transform.
* @param {PropertyType} type The type to transform it into.
* @return {*} The converted type.
* @private
*/
private convertTo( value: any, type: PropertyType ): PropertyTypeTypes | PropertyTypeTypes[] {
if ( Array.isArray( value ) ) {
return value.map( ( v: string ) => this.convertTo( v, type ) as PropertyTypeTypes );
}
if ( null === value ) {
return null;
}
switch ( type ) {
case PropertyType.String: return String( value );
case PropertyType.Integer: return parseInt( value );
case PropertyType.Float: return parseFloat( value );
case PropertyType.Boolean: return Boolean( value );
case PropertyType.Date:
return new Date( value );
}
}
/**
* Converts the given type into a string.
*
* @param {*} value The value to transform.
* @param {PropertyType} type The type to transform it into.
* @return {*} The converted type.
* @private
*/
private convertFrom( value: PropertyTypeTypes | PropertyTypeTypes[], type: PropertyType ): any {
if ( Array.isArray( value ) ) {
return value.map( ( v ) => this.convertFrom( v, type ) );
}
if ( null === value ) {
return null;
}
switch ( type ) {
case PropertyType.String:
case PropertyType.Integer:
case PropertyType.Float:
case PropertyType.Boolean:
return String( value );
case PropertyType.Date: {
return ( value as Date ).toISOString();
}
}
}
}

View File

@ -0,0 +1,34 @@
import axios, { AxiosInstance } from 'axios';
import * as moxios from 'moxios';
import { AxiosURLToQueryInterceptor } from '../axios-url-to-query-interceptor';
describe( 'AxiosURLToQueryInterceptor', () => {
let urlToQueryInterceptor: AxiosURLToQueryInterceptor;
let axiosInstance: AxiosInstance;
beforeEach( () => {
axiosInstance = axios.create();
moxios.install( axiosInstance );
urlToQueryInterceptor = new AxiosURLToQueryInterceptor( 'test' );
urlToQueryInterceptor.start( axiosInstance );
} );
afterEach( () => {
urlToQueryInterceptor.stop( axiosInstance );
moxios.uninstall();
} );
it( 'should put path in query string', async () => {
moxios.stubRequest( 'http://test.test/?test=%2Ftest%2Froute', {
status: 200,
headers: {
'Content-Type': 'application/json',
},
responseText: JSON.stringify( { test: 'value' } ),
} );
const response = await axiosInstance.get( 'http://test.test/test/route' );
expect( response.status ).toEqual( 200 );
} );
} );

View File

@ -1,4 +1,4 @@
import { buildURL } from '../utils';
import { buildURL, buildURLWithParams } from '../utils';
describe( 'buildURL', () => {
it( 'should use base when given no url', () => {
@ -15,9 +15,16 @@ describe( 'buildURL', () => {
const url = buildURL( { baseURL: 'http://test.test', url: 'yes/test' } );
expect( url ).toBe( 'http://test.test/yes/test' );
} );
} );
it( 'should combine base and url with trailing/leading slashes', () => {
const url = buildURL( { baseURL: 'http://test.test/////', url: '////yes/test' } );
expect( url ).toBe( 'http://test.test/yes/test' );
describe( 'buildURLWithParams', () => {
it( 'should do nothing without query string', () => {
const url = buildURLWithParams( { baseURL: 'http://test.test' } );
expect( url ).toBe( 'http://test.test' );
} );
it( 'should append query string', () => {
const url = buildURLWithParams( { baseURL: 'http://test.test', params: { test: 'yes' } } );
expect( url ).toBe( 'http://test.test?test=yes' );
} );
} );

View File

@ -2,10 +2,10 @@ import type { AxiosRequestConfig } from 'axios';
import * as createHmac from 'create-hmac';
import * as OAuth from 'oauth-1.0a';
import { AxiosInterceptor } from './axios-interceptor';
import { buildURL } from './utils';
import { buildURLWithParams } from './utils';
/**
* A utility class for managing the lifecycle of an authentication interceptor.
* An interceptor for adding OAuth 1.0a signatures to HTTP requests.
*/
export class AxiosOAuthInterceptor extends AxiosInterceptor {
/**
@ -14,7 +14,7 @@ export class AxiosOAuthInterceptor extends AxiosInterceptor {
* @type {Object}
* @private
*/
private oauth: OAuth;
private readonly oauth: OAuth;
/**
* Creates a new interceptor.
@ -44,7 +44,7 @@ export class AxiosOAuthInterceptor extends AxiosInterceptor {
* @return {AxiosRequestConfig} The request with the additional authorization headers.
*/
protected handleRequest( request: AxiosRequestConfig ): AxiosRequestConfig {
const url = buildURL( request );
const url = buildURLWithParams( request );
if ( url.startsWith( 'https' ) ) {
request.auth = {
username: this.oauth.consumer.key,

View File

@ -2,6 +2,9 @@ import { AxiosResponse } from 'axios';
import { AxiosInterceptor } from './axios-interceptor';
import { HTTPResponse } from '../http-client';
/**
* An interceptor for transforming the responses from axios into a consistent format for package consumers.
*/
export class AxiosResponseInterceptor extends AxiosInterceptor {
/**
* Transforms the Axios response into our HTTP response.

View File

@ -0,0 +1,53 @@
import { AxiosInterceptor } from './axios-interceptor';
import { AxiosRequestConfig } from 'axios';
import { buildURL } from './utils';
/**
* An interceptor for transforming the request's path into a query parameter.
*/
export class AxiosURLToQueryInterceptor extends AxiosInterceptor {
/**
* The query parameter we want to assign the path to.
*
* @type {string}
* @private
*/
private readonly queryParam: string;
/**
* Constructs a new interceptor.
*
* @param {string} queryParam The query parameter we want to assign the path to.
*/
public constructor( queryParam: string ) {
super();
this.queryParam = queryParam;
}
/**
* Converts the outgoing path into a query parameter.
*
* @param {AxiosRequestConfig} config The axios config.
* @return {AxiosRequestConfig} The axios config.
*/
protected handleRequest( config: AxiosRequestConfig ): AxiosRequestConfig {
const url = new URL( buildURL( config ) );
// Store the path in the query string.
if ( config.params instanceof URLSearchParams ) {
config.params.set( this.queryParam, url.pathname );
} else if ( config.params ) {
config.params[ this.queryParam ] = url.pathname;
} else {
config.params = { [ this.queryParam ]: url.pathname };
}
// Store the URL without the path now that it's in the query string.
url.pathname = '';
config.url = url.toString();
delete config.baseURL;
return config;
}
}

View File

@ -1,5 +1,10 @@
import { AxiosRequestConfig } from 'axios';
// @ts-ignore
import buildFullPath = require( 'axios/lib/core/buildFullPath' );
// @ts-ignore
import appendParams = require( 'axios/lib/helpers/buildURL' );
/**
* Given an Axios request config this function generates the URL that Axios will
* use to make the request.
@ -8,17 +13,16 @@ import { AxiosRequestConfig } from 'axios';
* @return {string} The merged URL.
*/
export function buildURL( request: AxiosRequestConfig ): string {
const base = request.baseURL || '';
if ( ! request.url ) {
return base;
}
// Axios ignores the base when the URL is absolute.
const url = request.url;
if ( ! base || url.match( /^([a-z][a-z\d+\-.]*:)?\/\/[^\/]/i ) ) {
return url;
}
// Remove trailing slashes from the base and leading slashes from the URL so we can combine them consistently.
return base.replace( /\/+$/, '' ) + '/' + url.replace( /^\/+/, '' );
return buildFullPath( request.baseURL, request.url );
}
/**
* Given an Axios request config this function generates the URL that Axios will
* use to make the request with the query parameters included.
*
* @param {AxiosRequestConfig} request The Axios request we're building the URL for.
* @return {string} The merged URL.
*/
export function buildURLWithParams( request: AxiosRequestConfig ): string {
return appendParams( buildURL( request ), request.params, request.paramsSerializer );
}

View File

@ -1,39 +1,141 @@
import { HTTPClient } from './http-client';
import { AxiosClient, AxiosOAuthInterceptor } from './axios';
import { AxiosRequestConfig } from 'axios';
import { AxiosInterceptor } from './axios/axios-interceptor';
import { AxiosURLToQueryInterceptor } from './axios/axios-url-to-query-interceptor';
/**
* A class for generating HTTPClient instances with desired configurations.
* These types describe the shape of the different auth methods our factory supports.
*/
type OAuthMethod = {
type: 'oauth',
key: string,
secret: string,
};
type BasicAuthMethod = {
type: 'basic',
username: string,
password: string,
}
/**
* An interface for describing the shape of a client to create using the factory.
*/
interface BuildParams {
wpURL: string,
useIndexPermalinks?: boolean,
auth?: OAuthMethod | BasicAuthMethod,
}
/**
* A factory for generating an HTTPClient with a desired configuration.
*/
export class HTTPClientFactory {
/**
* Creates a new client instance prepared for basic auth.
* The configuration object describing the client we're trying to create.
*
* @param {string} apiURL
* @param {string} username
* @param {string} password
* @return {HTTPClient} An HTTP client configured for OAuth requests.
* @private
*/
public static withBasicAuth( apiURL: string, username: string, password: string ): HTTPClient {
return new AxiosClient(
{
baseURL: apiURL,
auth: { username, password },
},
);
private clientConfig: BuildParams;
private constructor( wpURL: string ) {
this.clientConfig = { wpURL };
}
/**
* Creates a new client instance prepared for oauth.
* Creates a new factory that can be used to build clients.
*
* @param {string} apiURL
* @param {string} consumerKey
* @param {string} consumerSecret
* @return {HTTPClient} An HTTP client configured for OAuth requests.
* @param {string} wpURL The root URL of the WordPress installation we're querying.
* @return {HTTPClientFactory} The new factory instance.
*/
public static withOAuth( apiURL: string, consumerKey: string, consumerSecret: string ): HTTPClient {
return new AxiosClient(
{ baseURL: apiURL },
[ new AxiosOAuthInterceptor( consumerKey, consumerSecret ) ],
);
public static build( wpURL: string ): HTTPClientFactory {
return new HTTPClientFactory( wpURL );
}
/**
* Configures the client to utilize OAuth.
*
* @param {string} key The OAuth consumer key to use.
* @param {string} secret The OAuth consumer secret to use.
* @return {HTTPClientFactory} This factory.
*/
public withOAuth( key: string, secret: string ): this {
this.clientConfig.auth = { type: 'oauth', key, secret };
return this;
}
/**
* Configures the client to utilize basic auth.
*
* @param {string} username The WordPress username to use.
* @param {string} password The password for the WordPress user.
* @return {HTTPClientFactory} This factory.
*/
public withBasicAuth( username: string, password: string ): this {
this.clientConfig.auth = { type: 'basic', username, password };
return this;
}
/**
* Configures the client to use index permalinks.
*
* @return {HTTPClientFactory} This factory.
*/
public withIndexPermalinks(): this {
this.clientConfig.useIndexPermalinks = true;
return this;
}
/**
* Configures the client to use query permalinks.
*
* @return {HTTPClientFactory} This factory.
*/
public withoutIndexPermalinks(): this {
this.clientConfig.useIndexPermalinks = false;
return this;
}
/**
* Creates a client instance using the configuration stored within.
*
* @return {HTTPClient} The created client.
*/
public create(): HTTPClient {
const axiosConfig: AxiosRequestConfig = {};
const interceptors: AxiosInterceptor[] = [];
axiosConfig.baseURL = this.clientConfig.wpURL;
if ( ! axiosConfig.baseURL.endsWith( '/' ) ) {
axiosConfig.baseURL += '/';
}
if ( this.clientConfig.useIndexPermalinks ) {
axiosConfig.baseURL += 'wp-json/';
} else {
interceptors.push( new AxiosURLToQueryInterceptor( 'rest_route' ) );
}
if ( this.clientConfig.auth ) {
switch ( this.clientConfig.auth.type ) {
case 'basic':
axiosConfig.auth = {
username: this.clientConfig.auth.username,
password: this.clientConfig.auth.password,
};
break;
case 'oauth':
interceptors.push(
new AxiosOAuthInterceptor(
this.clientConfig.auth.key,
this.clientConfig.auth.secret,
),
);
break;
}
}
return new AxiosClient( axiosConfig, interceptors );
}
}

View File

@ -1,4 +1,32 @@
import { Model } from '../model';
import { MetaData, PostStatus } from '../shared-types';
import {
BackorderStatus,
CatalogVisibility,
ProductAttribute,
ProductDownload,
ProductImage,
ProductTerm, StockStatus,
Taxability,
} from './shared-types';
/**
* The common parameters that all products can use in search.
*/
export type ProductSearchParams = { search: string };
/**
* The common parameters that all products can update.
*/
export type ProductUpdateParams = 'name' | 'slug' | 'created' | 'postStatus' | 'shortDescription'
| 'description' | 'sku' | 'categories' | 'tags' | 'isFeatured'
| 'isVirtual' | 'attributes' | 'images' | 'catalogVisibility'
| 'regularPrice' | 'onePerOrder' | 'taxStatus' | 'taxClass'
| 'salePrice' | 'saleStart' | 'saleEnd' | 'isDownloadable'
| 'downloadLimit' | 'daysToDownload' | 'weight' | 'length'
| 'width' | 'height' | 'trackInventory' | 'remainingStock'
| 'stockStatus' | 'backorderStatus' | 'allowReviews'
| 'metaData';
/**
* The base class for all product types.
@ -11,10 +39,325 @@ export abstract class AbstractProduct extends Model {
*/
public readonly name: string = '';
/**
* The slug of the product.
*
* @type {string}
*/
public readonly slug: string = '';
/**
* The permalink of the product.
*
* @type {string}
*/
public readonly permalink: string = '';
/**
* The GMT datetime when the product was created.
*
* @type {Date}
*/
public readonly created: Date = new Date();
/**
* The GMT datetime when the product was last modified.
*
* @type {Date}
*/
public readonly modified: Date = new Date();
/**
* The product's current post status.
*
* @type {PostStatus}
*/
public readonly postStatus: PostStatus = '';
/**
* The product's short description.
*
* @type {string}
*/
public readonly shortDescription: string = '';
/**
* The product's full description.
*
* @type {string}
*/
public readonly description: string = '';
/**
* The product's SKU.
*
* @type {string}
*/
public readonly sku: string = '';
/**
* An array of the categories this product is in.
*
* @type {ReadonlyArray.<ProductTerm>}
*/
public readonly categories: readonly ProductTerm[] = [];
/**
* An array of the tags this product has.
*
* @type {ReadonlyArray.<ProductTerm>}
*/
public readonly tags: readonly ProductTerm[] = [];
/**
* Indicates whether or not the product is currently able to be purchased.
*
* @type {boolean}
*/
public readonly isPurchasable: boolean = true;
/**
* Indicates whether or not the product should be featured.
*
* @type {boolean}
*/
public readonly isFeatured: boolean = false;
/**
* Indicates that the product is delivered virtually.
*
* @type {boolean}
*/
public readonly isVirtual: boolean = false;
/**
* The attributes for the product.
*
* @type {ReadonlyArray.<ProductAttribute>}
*/
public readonly attributes: readonly ProductAttribute[] = [];
/**
* The images for the product.
*
* @type {ReadonlyArray.<ProductImage>}
*/
public readonly images: readonly ProductImage[] = [];
/**
* Indicates whether or not the product should be visible in the catalog.
*
* @type {CatalogVisibility}
*/
public readonly catalogVisibility: CatalogVisibility = CatalogVisibility.Everywhere;
/**
* The current price of the product.
*
* @type {string}
*/
public readonly price: string = '';
/**
* The regular price of the product when not discounted.
*
* @type {string}
*/
public readonly regularPrice: string = '';
/**
* Indicates that only one of a product may be held in the order at a time.
*
* @type {boolean}
*/
public readonly onePerOrder: boolean = false;
/**
* The taxability of the product.
*
* @type {Taxability}
*/
public readonly taxStatus: Taxability = Taxability.ProductAndShipping;
/**
* The tax class of the product
*
* @type {string}
*/
public readonly taxClass: string = '';
/**
* Indicates whether or not the product is currently on sale.
*
* @type {boolean}
*/
public readonly onSale: boolean = false;
/**
* The price of the product when on sale.
*
* @type {string}
*/
public readonly salePrice: string = '';
/**
* The GMT datetime when the product should start to be on sale.
*
* @type {Date|null}
*/
public readonly saleStart: Date | null = null;
/**
* The GMT datetime when the product should no longer be on sale.
*
* @type {Date|null}
*/
public readonly saleEnd: Date | null = null;
/**
* Indicates whether or not the product is downloadable.
*
* @type {boolean}
*/
public readonly isDownloadable: boolean = false;
/**
* The downloads available for the product.
*
* @type {ReadonlyArray.<ProductDownload>}
*/
public readonly downloads: readonly ProductDownload[] = [];
/**
* The maximum number of times a customer may download the product's contents.
*
* @type {number}
*/
public readonly downloadLimit: number = -1;
/**
* The number of days after purchase that a customer may still download the product's contents.
*
* @type {number}
*/
public readonly daysToDownload: number = -1;
/**
* The weight of the product in the store's current units.
*
* @type {string}
*/
public readonly weight: string = '';
/**
* The length of the product in the store's current units.
*
* @type {string}
*/
public readonly length: string = '';
/**
* The width of the product in the store's current units.
*
* @type {string}
*/
public readonly width: string = '';
/**
* The height of the product in the store's current units.
*
* @type {string}
*/
public readonly height: string = '';
/**
* Indicates that the product must be shipped.
*
* @type {boolean}
*/
public readonly requiresShipping: boolean = false;
/**
* Indicates that the product's shipping is taxable.
*
* @type {boolean}
*/
public readonly isShippingTaxable: boolean = false;
/**
* The shipping class for the product.
*
* @type {string}
*/
public readonly shippingClass: string = '';
/**
* Indicates that a product should use the inventory system.
*
* @type {boolean}
*/
public readonly trackInventory: boolean = false;
/**
* The number of inventory units remaining for this product.
*
* @type {number}
*/
public readonly remainingStock: number = -1;
/**
* The product's stock status.
*
* @type {StockStatus}
*/
public readonly stockStatus: StockStatus = ''
/**
* The status of backordering for a product.
*
* @type {BackorderStatus}
*/
public readonly backorderStatus: BackorderStatus = BackorderStatus.Allowed;
/**
* Indicates whether or not a product can be backordered.
*
* @type {boolean}
*/
public readonly canBackorder: boolean = false;
/**
* Indicates whether or not a product is on backorder.
*
* @type {boolean}
*/
public readonly isOnBackorder: boolean = false;
/**
* Indicates whether or not a product allows reviews.
*
* @type {boolean}
*/
public readonly allowReviews: boolean = false;
/**
* The average rating for the product.
*
* @type {number}
*/
public readonly averageRating: number = -1;
/**
* The number of ratings for the product.
*
* @type {number}
*/
public readonly numRatings: number = -1;
/**
* The extra metadata for the product.
*
* @type {ReadonlyArray.<MetaData>}
*/
public readonly metaData: readonly MetaData[] = [];
}

View File

@ -0,0 +1,261 @@
/**
* An enum describing the catalog visibility options for products.
*
* @enum {string}
*/
export enum CatalogVisibility {
/**
* The product should be visible everywhere.
*/
Everywhere = 'visible',
/**
* The product should only be visible in the shop catalog.
*/
ShopOnly = 'catalog',
/**
* The product should only be visible in search results.
*/
SearchOnly = 'search',
/**
* The product should be hidden everywhere.
*/
Hidden = 'hidden'
}
/**
* Indicates the taxability of a product.
*
* @enum {string}
*/
export enum Taxability {
/**
* The product and shipping are both taxable.
*/
ProductAndShipping = 'taxable',
/**
* Only the product's shipping is taxable.
*/
ShippingOnly = 'shipping',
/**
* The product and shipping are not taxable.
*/
None = 'none'
}
/**
* Indicates the status for backorders for a product.
*
* @enum {string}
*/
export enum BackorderStatus {
/**
* The product is allowed to be backordered.
*/
Allowed = 'yes',
/**
* The product is allowed to be backordered but it will notify the customer of that fact.
*/
AllowedWithNotification = 'notify',
/**
* The product is not allowed to be backordered.
*/
NotAllowed = 'no'
}
/**
* A product's stock status.
*
* @typedef StockStatus
* @alias 'instock'|'outofstock'|'onbackorder'|string
*/
export type StockStatus = 'instock' | 'outofstock' | 'onbackorder' | string
/**
* A products taxonomy term such as categories or tags.
*/
export class ProductTerm {
/**
* The ID of the term.
*
* @type {number}
*/
public readonly id: number = -1;
/**
* The name of the term.
*
* @type {string}
*/
public readonly name: string = '';
/**
* The slug of the term.
*
* @type {string}
*/
public readonly slug: string = '';
/**
* Creates a new product term.
*
* @param {Partial.<ProductTerm>} properties The properties to set.
*/
public constructor( properties?: Partial< ProductTerm > ) {
Object.assign( this, properties );
}
}
/**
* A product's download.
*/
export class ProductDownload {
/**
* The ID of the downloadable file.
*
* @type {string}
*/
public readonly id: string = '';
/**
* The name of the downloadable file.
*
* @type {string}
*/
public readonly name: string = '';
/**
* The URL of the downloadable file.
*
*
* @type {string}
*/
public readonly url: string = '';
/**
* Creates a new product download.
*
* @param {Partial.<ProductDownload>} properties The properties to set.
*/
public constructor( properties?: Partial< ProductDownload > ) {
Object.assign( this, properties );
}
}
/**
* A product's attributes.
*/
export class ProductAttribute {
/**
* The ID of the attribute.
*
* @type {number}
*/
public readonly id: number = -1;
/**
* The name of the attribute.
*
* @type {string}
*/
public readonly name: string = '';
/**
* The sort order of the attribute.
*
* @type {number}
*/
public readonly sortOrder: number = -1;
/**
* Indicates whether or not the attribute is visible on the product page.
*
* @type {boolean}
*/
public readonly isVisibleOnProductPage: boolean = false;
/**
* Indicates whether or not the attribute should be used in variations.
*
* @type {boolean}
*/
public readonly isForVariations: boolean = false;
/**
* The options which are available for the attribute.
*
* @type {ReadonlyArray.<string>}
*/
public readonly options: readonly string[] = [];
/**
* Creates a new product attribute.
*
* @param {Partial.<ProductAttribute>} properties The properties to set.
*/
public constructor( properties?: Partial< ProductAttribute > ) {
Object.assign( this, properties );
}
}
/**
* A product's image.
*/
export class ProductImage {
/**
* The ID of the image.
*
* @type {number}
*/
public readonly id: number = -1;
/**
* The GMT datetime when the image was created.
*
* @type {Date}
*/
public readonly created: Date = new Date();
/**
* The GMT datetime when the image was last modified.
*
* @type {Date}
*/
public readonly modified: Date = new Date();
/**
* The URL for the image file.
*
* @type {string}
*/
public readonly url: string = '';
/**
* The name of the image file.
*
* @type {string}
*/
public readonly name: string = '';
/**
* The alt text to use on the image.
*
* @type {string}
*/
public readonly altText: string = '';
/**
* Creates a new product image.
*
* @param {Partial.<ProductImage>} properties The properties to set.
*/
public constructor( properties?: Partial< ProductImage > ) {
Object.assign( this, properties );
}
}

View File

@ -1,13 +1,27 @@
import { AbstractProduct } from './abstract-product';
import { AbstractProduct, ProductSearchParams, ProductUpdateParams } from './abstract-product';
import { HTTPClient } from '../../http';
import { simpleProductRESTRepository } from '../../repositories/rest/products/simple-product';
import { CreatesModels, ModelRepositoryParams } from '../../framework/model-repository';
import {
CreatesModels,
DeletesModels, ListsModels,
ModelRepositoryParams,
ReadsModels,
UpdatesModels,
} from '../../framework/model-repository';
/**
* The parameters embedded in this generic can be used in the ModelRepository in order to give
* type-safety in an incredibly granular way.
*/
export type SimpleProductRepositoryParams = ModelRepositoryParams< SimpleProduct, never, never, 'regularPrice' >;
export type SimpleProductRepositoryParams = ModelRepositoryParams< SimpleProduct, never, ProductSearchParams, ProductUpdateParams >;
/**
* An interface for listing simple products using the repository.
*
* @typedef ListsSimpleProducts
* @alias ListsModels.<SimpleProduct>
*/
export type ListsSimpleProducts = ListsModels< SimpleProductRepositoryParams >;
/**
* An interface for creating simple products using the repository.
@ -17,6 +31,30 @@ export type SimpleProductRepositoryParams = ModelRepositoryParams< SimpleProduct
*/
export type CreatesSimpleProducts = CreatesModels< SimpleProductRepositoryParams >;
/**
* An interface for reading simple products using the repository.
*
* @typedef ReadsSimpleProducts
* @alias ReadsModels.<SimpleProduct>
*/
export type ReadsSimpleProducts = ReadsModels< SimpleProductRepositoryParams >;
/**
* An interface for updating simple products using the repository.
*
* @typedef UpdatesSimpleProducts
* @alias UpdatesModels.<SimpleProduct>
*/
export type UpdatesSimpleProducts = UpdatesModels< SimpleProductRepositoryParams >;
/**
* An interface for deleting simple products using the repository.
*
* @typedef DeletesSimpleProducts
* @alias DeletesModels.<SimpleProduct>
*/
export type DeletesSimpleProducts = DeletesModels< SimpleProductRepositoryParams >;
/**
* A simple product object.
*/
@ -26,7 +64,7 @@ export class SimpleProduct extends AbstractProduct {
*
* @param {Object} properties The properties to set in the object.
*/
public constructor( properties: Partial< SimpleProduct > = {} ) {
public constructor( properties?: Partial< SimpleProduct > ) {
super();
Object.assign( this, properties );
}

View File

@ -47,7 +47,7 @@ export class SettingGroup extends Model {
*
* @param {Object} properties The properties to set in the object.
*/
public constructor( properties: Partial< SettingGroup > = {} ) {
public constructor( properties?: Partial< SettingGroup > ) {
super();
Object.assign( this, properties );
}

View File

@ -94,7 +94,7 @@ export class Setting extends Model {
*
* @param {Object} properties The properties to set in the object.
*/
public constructor( properties: Partial< Setting > = {} ) {
public constructor( properties?: Partial< Setting > ) {
super();
Object.assign( this, properties );
}

View File

@ -0,0 +1,47 @@
import { Model } from './model';
/**
* A constructor for a model.
*
* @typedef ModelConstructor
* @alias Function.<T>
* @template T
*/
export type ModelConstructor< T extends Model > = new ( properties: Partial< T > ) => T;
/**
* A post's status.
*
* @typedef PostStatus
* @alias 'draft'|'pending'|'private'|'publish'|string
*/
export type PostStatus = 'draft' | 'pending' | 'private' | 'publish' | string;
/**
* A metadata object.
*/
export class MetaData extends Model {
/**
* The key of the metadata.
*
* @type {string}
*/
public readonly key: string = '';
/**
* The value of the metadata.
*
* @type {*}
*/
public readonly value: any = '';
/**
* Creates a new metadata.
*
* @param {Partial.<MetaData>} properties The properties to set.
*/
public constructor( properties?: Partial< MetaData > ) {
super();
Object.assign( this, properties );
}
}

View File

@ -0,0 +1,247 @@
import { mock, MockProxy } from 'jest-mock-extended';
import { HTTPClient, HTTPResponse } from '../../../http';
import { ModelTransformer } from '../../../framework/model-transformer';
import { DummyModel } from '../../../__test_data__/dummy-model';
import {
restCreate,
restDelete, restDeleteChild,
restList,
restListChild,
restRead,
restReadChild,
restUpdate,
restUpdateChild,
} from '../shared';
import { ModelRepositoryParams } from '../../../framework/model-repository';
import { Model } from '../../../models/model';
type DummyModelParams = ModelRepositoryParams< DummyModel, never, { search: string }, 'name' >
class DummyChildModel extends Model {
public childName: string = '';
public constructor( partial?: Partial< DummyModel > ) {
super();
Object.assign( this, partial );
}
}
type DummyChildParams = ModelRepositoryParams< DummyChildModel, { parent: string }, { childSearch: string }, 'childName' >
describe( 'Shared REST Functions', () => {
let mockClient: MockProxy< HTTPClient >;
let mockTransformer: MockProxy< ModelTransformer< any > > & ModelTransformer< any >;
beforeEach( () => {
mockClient = mock< HTTPClient >();
mockTransformer = mock< ModelTransformer< any > >();
} );
it( 'restList', async () => {
mockClient.get.mockResolvedValue( new HTTPResponse(
200,
{},
[
{
id: 'Test-1',
label: 'Test 1',
},
{
id: 'Test-2',
label: 'Test 2',
},
],
) );
mockTransformer.toModel.mockReturnValue( new DummyModel( { name: 'Test' } ) );
const fn = restList< DummyModelParams >( () => 'test-url', DummyModel, mockClient, mockTransformer );
const result = await fn( { search: 'Test' } );
expect( result ).toHaveLength( 2 );
expect( result[ 0 ] ).toMatchObject( new DummyModel( { name: 'Test' } ) );
expect( result[ 1 ] ).toMatchObject( new DummyModel( { name: 'Test' } ) );
expect( mockClient.get ).toHaveBeenCalledWith( 'test-url', { search: 'Test' } );
expect( mockTransformer.toModel ).toHaveBeenCalledWith( DummyModel, { id: 'Test-1', label: 'Test 1' } );
expect( mockTransformer.toModel ).toHaveBeenCalledWith( DummyModel, { id: 'Test-2', label: 'Test 2' } );
} );
it( 'restListChildren', async () => {
mockClient.get.mockResolvedValue( new HTTPResponse(
200,
{},
[
{
id: 'Test-1',
label: 'Test 1',
},
{
id: 'Test-2',
label: 'Test 2',
},
],
) );
mockTransformer.toModel.mockReturnValue( new DummyChildModel( { name: 'Test' } ) );
const fn = restListChild< DummyChildParams >(
( parent ) => 'test-url-' + parent.parent,
DummyChildModel,
mockClient,
mockTransformer,
);
const result = await fn( { parent: '123' }, { childSearch: 'Test' } );
expect( result ).toHaveLength( 2 );
expect( result[ 0 ] ).toMatchObject( new DummyChildModel( { name: 'Test' } ) );
expect( result[ 1 ] ).toMatchObject( new DummyChildModel( { name: 'Test' } ) );
expect( mockClient.get ).toHaveBeenCalledWith( 'test-url-123', { childSearch: 'Test' } );
expect( mockTransformer.toModel ).toHaveBeenCalledWith( DummyChildModel, { id: 'Test-1', label: 'Test 1' } );
expect( mockTransformer.toModel ).toHaveBeenCalledWith( DummyChildModel, { id: 'Test-2', label: 'Test 2' } );
} );
it( 'restCreate', async () => {
mockClient.post.mockResolvedValue( new HTTPResponse(
200,
{},
{
id: 'Test-1',
label: 'Test 1',
},
) );
mockTransformer.fromModel.mockReturnValue( { name: 'From-Test' } );
mockTransformer.toModel.mockReturnValue( new DummyModel( { name: 'Test' } ) );
const fn = restCreate< DummyModelParams >(
( properties ) => 'test-url-' + properties.name,
DummyModel,
mockClient,
mockTransformer,
);
const result = await fn( { name: 'Test' } );
expect( result ).toMatchObject( new DummyModel( { name: 'Test' } ) );
expect( mockTransformer.fromModel ).toHaveBeenCalledWith( { name: 'Test' } );
expect( mockClient.post ).toHaveBeenCalledWith( 'test-url-Test', { name: 'From-Test' } );
expect( mockTransformer.toModel ).toHaveBeenCalledWith( DummyModel, { id: 'Test-1', label: 'Test 1' } );
} );
it( 'restRead', async () => {
mockClient.get.mockResolvedValue( new HTTPResponse(
200,
{},
{
id: 'Test-1',
label: 'Test 1',
},
) );
mockTransformer.toModel.mockReturnValue( new DummyModel( { name: 'Test' } ) );
const fn = restRead< DummyModelParams >( ( id ) => 'test-url-' + id, DummyModel, mockClient, mockTransformer );
const result = await fn( 123 );
expect( result ).toMatchObject( new DummyModel( { name: 'Test' } ) );
expect( mockClient.get ).toHaveBeenCalledWith( 'test-url-123' );
expect( mockTransformer.toModel ).toHaveBeenCalledWith( DummyModel, { id: 'Test-1', label: 'Test 1' } );
} );
it( 'restReadChildren', async () => {
mockClient.get.mockResolvedValue( new HTTPResponse(
200,
{},
{
id: 'Test-1',
label: 'Test 1',
},
) );
mockTransformer.toModel.mockReturnValue( new DummyChildModel( { name: 'Test' } ) );
const fn = restReadChild< DummyChildParams >(
( parent, id ) => 'test-url-' + parent.parent + '-' + id,
DummyChildModel,
mockClient,
mockTransformer,
);
const result = await fn( { parent: '123' }, 456 );
expect( result ).toMatchObject( new DummyModel( { name: 'Test' } ) );
expect( mockClient.get ).toHaveBeenCalledWith( 'test-url-123-456' );
expect( mockTransformer.toModel ).toHaveBeenCalledWith( DummyChildModel, { id: 'Test-1', label: 'Test 1' } );
} );
it( 'restUpdate', async () => {
mockClient.patch.mockResolvedValue( new HTTPResponse(
200,
{},
{
id: 'Test-1',
label: 'Test 1',
},
) );
mockTransformer.fromModel.mockReturnValue( { name: 'From-Test' } );
mockTransformer.toModel.mockReturnValue( new DummyModel( { name: 'Test' } ) );
const fn = restUpdate< DummyModelParams >( ( id ) => 'test-url-' + id, DummyModel, mockClient, mockTransformer );
const result = await fn( 123, { name: 'Test' } );
expect( result ).toMatchObject( new DummyModel( { name: 'Test' } ) );
expect( mockTransformer.fromModel ).toHaveBeenCalledWith( { name: 'Test' } );
expect( mockClient.patch ).toHaveBeenCalledWith( 'test-url-123', { name: 'From-Test' } );
expect( mockTransformer.toModel ).toHaveBeenCalledWith( DummyModel, { id: 'Test-1', label: 'Test 1' } );
} );
it( 'restUpdateChildren', async () => {
mockClient.patch.mockResolvedValue( new HTTPResponse(
200,
{},
{
id: 'Test-1',
label: 'Test 1',
},
) );
mockTransformer.fromModel.mockReturnValue( { name: 'From-Test' } );
mockTransformer.toModel.mockReturnValue( new DummyChildModel( { name: 'Test' } ) );
const fn = restUpdateChild< DummyChildParams >(
( parent, id ) => 'test-url-' + parent.parent + '-' + id,
DummyChildModel,
mockClient,
mockTransformer,
);
const result = await fn( { parent: '123' }, 456, { childName: 'Test' } );
expect( result ).toMatchObject( new DummyChildModel( { name: 'Test' } ) );
expect( mockTransformer.fromModel ).toHaveBeenCalledWith( { childName: 'Test' } );
expect( mockClient.patch ).toHaveBeenCalledWith( 'test-url-123-456', { name: 'From-Test' } );
expect( mockTransformer.toModel ).toHaveBeenCalledWith( DummyChildModel, { id: 'Test-1', label: 'Test 1' } );
} );
it( 'restDelete', async () => {
mockClient.delete.mockResolvedValue( new HTTPResponse( 200, {}, {} ) );
const fn = restDelete< DummyModelParams >( ( id ) => 'test-url-' + id, mockClient );
const result = await fn( 123 );
expect( result ).toBe( true );
expect( mockClient.delete ).toHaveBeenCalledWith( 'test-url-123' );
} );
it( 'restDeleteChildren', async () => {
mockClient.delete.mockResolvedValue( new HTTPResponse( 200, {}, {} ) );
const fn = restDeleteChild< DummyChildParams >(
( parent, id ) => 'test-url-' + parent.parent + '-' + id,
mockClient,
);
const result = await fn( { parent: '123' }, 456 );
expect( result ).toBe( true );
expect( mockClient.delete ).toHaveBeenCalledWith( 'test-url-123-456' );
} );
} );

View File

@ -1,28 +0,0 @@
import { simpleProductRESTRepository } from '../simple-product';
import { mock, MockProxy } from 'jest-mock-extended';
import { HTTPClient, HTTPResponse } from '../../../../http';
import { SimpleProduct } from '../../../../models';
describe( 'simpleProductRESTRepository', () => {
let httpClient: MockProxy< HTTPClient >;
let repository: ReturnType< typeof simpleProductRESTRepository >;
beforeEach( () => {
httpClient = mock< HTTPClient >();
repository = simpleProductRESTRepository( httpClient );
} );
it( 'should create', async () => {
httpClient.post.mockResolvedValue( new HTTPResponse(
200,
{},
{ id: 123 },
) );
const created = await repository.create( { name: 'Test Product' } );
expect( created ).toBeInstanceOf( SimpleProduct );
expect( created ).toMatchObject( { id: 123 } );
expect( httpClient.post ).toHaveBeenCalledWith( '/wc/v3/products', { type: 'simple', name: 'Test Product' } );
} );
} );

View File

@ -0,0 +1,221 @@
import { ModelTransformation, ModelTransformer, TransformationOrder } from '../../../framework/model-transformer';
import { KeyChangeTransformation } from '../../../framework/transformations/key-change-transformation';
import { AbstractProduct } from '../../../models/products/abstract-product';
import { AddPropertyTransformation } from '../../../framework/transformations/add-property-transformation';
import { IgnorePropertyTransformation } from '../../../framework/transformations/ignore-property-transformation';
import {
PropertyType,
PropertyTypeTransformation,
} from '../../../framework/transformations/property-type-transformation';
import { CustomTransformation } from '../../../framework/transformations/custom-transformation';
import { ProductAttribute, ProductDownload, ProductImage, ProductTerm } from '../../../models/products/shared-types';
import { ModelTransformerTransformation } from '../../../framework/transformations/model-transformer-transformation';
import { MetaData } from '../../../models/shared-types';
import { createMetaDataTransformer } from '../shared';
/**
* Creates a transformer for the product term object.
*
* @return {ModelTransformer} The created transformer.
*/
function createProductTermTransformer(): ModelTransformer< ProductTerm > {
return new ModelTransformer(
[
new PropertyTypeTransformation( { id: PropertyType.Integer } ),
],
);
}
/**
* Creates a transformer for the product attribute object.
*
* @return {ModelTransformer} The created transformer.
*/
function createProductAttributeTransformer(): ModelTransformer< ProductAttribute > {
return new ModelTransformer(
[
new PropertyTypeTransformation(
{
id: PropertyType.Integer,
sortOrder: PropertyType.Integer,
isVisibleOnProductPage: PropertyType.Boolean,
isForVariations: PropertyType.Boolean,
},
),
new KeyChangeTransformation< ProductAttribute >(
{
sortOrder: 'position',
isVisibleOnProductPage: 'visible',
isForVariations: 'variation',
},
),
],
);
}
/**
* Creates a transformer for the product image object.
*
* @return {ModelTransformer} The created transformer.
*/
function createProductImageTransformer(): ModelTransformer< ProductImage > {
return new ModelTransformer(
[
new IgnorePropertyTransformation( [ 'date_created', 'date_modified' ] ),
new PropertyTypeTransformation(
{
id: PropertyType.Integer,
created: PropertyType.Date,
modified: PropertyType.Date,
},
),
new KeyChangeTransformation< ProductImage >(
{
created: 'date_created_gmt',
modified: 'date_modified_gmt',
url: 'src',
altText: 'altText',
},
),
],
);
}
/**
* Creates a transformer for the product download object.
*
* @return {ModelTransformer} The created transformer.
*/
function createProductDownloadTransformer(): ModelTransformer< ProductDownload > {
return new ModelTransformer(
[
new KeyChangeTransformation< ProductDownload >( { url: 'file' } ),
],
);
}
/**
* Creates a transformer for the shared properties of all products.
*
* @param {string} type The product type.
* @param {Array.<ModelTransformation>} transformations Optional transformers to add to the transformer.
* @return {ModelTransformer} The created transformer.
*/
export function createProductTransformer< T extends AbstractProduct >(
type: string,
transformations?: ModelTransformation[],
): ModelTransformer< T > {
if ( ! transformations ) {
transformations = [];
}
transformations.push(
new AddPropertyTransformation( {}, { type } ),
new IgnorePropertyTransformation(
[
'date_created',
'date_modified',
'date_on_sale_from',
'date_on_sale_to',
],
),
new ModelTransformerTransformation( 'categories', ProductTerm, createProductTermTransformer() ),
new ModelTransformerTransformation( 'tags', ProductTerm, createProductTermTransformer() ),
new ModelTransformerTransformation( 'attributes', ProductAttribute, createProductAttributeTransformer() ),
new ModelTransformerTransformation( 'images', ProductImage, createProductImageTransformer() ),
new ModelTransformerTransformation( 'downloads', ProductDownload, createProductDownloadTransformer() ),
new ModelTransformerTransformation( 'metaData', MetaData, createMetaDataTransformer() ),
new CustomTransformation(
TransformationOrder.Normal,
( properties: any ) => {
if ( properties.hasOwnProperty( 'dimensions' ) ) {
properties.length = properties.dimensions.length;
properties.width = properties.dimensions.width;
properties.height = properties.dimensions.height;
delete properties.dimensions;
}
return properties;
},
( properties: any ) => {
if ( properties.hasOwnProperty( 'length ' ) ||
properties.hasOwnProperty( 'width' ) ||
properties.hasOwnProperty( 'height' ) ) {
properties.dimensions = {
length: properties.length,
width: properties.width,
height: properties.height,
};
delete properties.length;
delete properties.width;
delete properties.height;
}
return properties;
},
),
new PropertyTypeTransformation(
{
created: PropertyType.Date,
modified: PropertyType.Date,
isPurchasable: PropertyType.Boolean,
isFeatured: PropertyType.Boolean,
isVirtual: PropertyType.Boolean,
onePerOrder: PropertyType.Boolean,
onSale: PropertyType.Boolean,
saleStart: PropertyType.Date,
saleEnd: PropertyType.Date,
isDownloadable: PropertyType.Boolean,
downloadLimit: PropertyType.Integer,
daysToDownload: PropertyType.Integer,
requiresShipping: PropertyType.Boolean,
isShippingTaxable: PropertyType.Boolean,
trackInventory: PropertyType.Boolean,
remainingStock: PropertyType.Integer,
canBackorder: PropertyType.Boolean,
isOnBackorder: PropertyType.Boolean,
allowReviews: PropertyType.Boolean,
averageRating: PropertyType.Integer,
numRatings: PropertyType.Integer,
},
),
new KeyChangeTransformation< AbstractProduct >(
{
created: 'date_created_gmt',
modified: 'date_modified_gmt',
postStatus: 'status',
shortDescription: 'short_description',
isPurchasable: 'purchasable',
isFeatured: 'featured',
isVirtual: 'virtual',
catalogVisibility: 'catalog_visibility',
regularPrice: 'regular_price',
onePerOrder: 'sold_individually',
taxStatus: 'tax_status',
taxClass: 'tax_class',
onSale: 'on_sale',
salePrice: 'sale_price',
saleStart: 'date_on_sale_from_gmt',
saleEnd: 'date_on_sale_to_gmt',
isDownloadable: 'downloadable',
downloadLimit: 'download_limit',
daysToDownload: 'download_expiry',
requiresShipping: 'shipping_required',
isShippingTaxable: 'shipping_taxable',
shippingClass: 'shipping_class',
trackInventory: 'manage_stock',
remainingStock: 'stock_quantity',
stockStatus: 'stock_status',
backorderStatus: 'backorders',
canBackorder: 'backorders_allowed',
isOnBackorder: 'backordered',
allowReviews: 'reviews_allowed',
averageRating: 'average_rating',
numRatings: 'rating_count',
metaData: 'meta_data',
},
),
);
return new ModelTransformer( transformations );
}

View File

@ -1,39 +1,43 @@
import { HTTPClient } from '../../../http';
import { CreateFn, ModelRepository } from '../../../framework/model-repository';
import { ModelRepository } from '../../../framework/model-repository';
import { SimpleProduct } from '../../../models';
import { CreatesSimpleProducts, SimpleProductRepositoryParams } from '../../../models/products/simple-product';
function restCreate( httpClient: HTTPClient ): CreateFn< SimpleProductRepositoryParams > {
return async ( properties ) => {
const response = await httpClient.post(
'/wc/v3/products',
{
type: 'simple',
name: properties.name,
regular_price: properties.regularPrice,
},
);
return Promise.resolve( new SimpleProduct( {
id: response.data.id,
name: response.data.name,
regularPrice: response.data.regular_price,
} ) );
};
}
import {
CreatesSimpleProducts,
DeletesSimpleProducts,
ListsSimpleProducts,
ReadsSimpleProducts,
SimpleProductRepositoryParams,
UpdatesSimpleProducts,
} from '../../../models/products/simple-product';
import { createProductTransformer } from './shared';
import { restCreate, restDelete, restList, restRead, restUpdate } from '../shared';
import { ModelID } from '../../../models/model';
/**
* Creates a new ModelRepository instance for interacting with models via the REST API.
*
* @param {HTTPClient} httpClient The HTTP client for the REST requests to be made using.
* @return {CreatesSimpleProducts} The created repository.
* @return {
* ListsSimpleProducts|
* CreatesSimpleProducts|
* ReadsSimpleProducts|
* UpdatesSimpleProducts|
* DeletesSimpleProducts
* } The created repository.
*/
export function simpleProductRESTRepository( httpClient: HTTPClient ): CreatesSimpleProducts {
export function simpleProductRESTRepository( httpClient: HTTPClient ): ListsSimpleProducts
& CreatesSimpleProducts
& ReadsSimpleProducts
& UpdatesSimpleProducts
& DeletesSimpleProducts {
const buildURL = ( id: ModelID ) => '/wc/v3/products/' + id;
const transformer = createProductTransformer( 'simple' );
return new ModelRepository(
null,
restCreate( httpClient ),
null,
null,
null,
restList< SimpleProductRepositoryParams >( () => '/wc/v3/products', SimpleProduct, httpClient, transformer ),
restCreate< SimpleProductRepositoryParams >( () => '/wc/v3/products', SimpleProduct, httpClient, transformer ),
restRead< SimpleProductRepositoryParams >( buildURL, SimpleProduct, httpClient, transformer ),
restUpdate< SimpleProductRepositoryParams >( buildURL, SimpleProduct, httpClient, transformer ),
restDelete< SimpleProductRepositoryParams >( buildURL, httpClient ),
);
}

View File

@ -1,37 +0,0 @@
import { mock, MockProxy } from 'jest-mock-extended';
import { HTTPClient, HTTPResponse } from '../../../../http';
import { settingGroupRESTRepository } from '../setting-group';
describe( 'settingGroupRESTRepository', () => {
let httpClient: MockProxy< HTTPClient >;
let repository: ReturnType< typeof settingGroupRESTRepository >;
beforeEach( () => {
httpClient = mock< HTTPClient >();
repository = settingGroupRESTRepository( httpClient );
} );
it( 'should list', async () => {
httpClient.get.mockResolvedValue( new HTTPResponse(
200,
{},
[
{
id: 'group_1',
label: 'Test Group 1',
},
{
id: 'group_2',
label: 'Test Group 2',
},
],
) );
const list = await repository.list();
expect( list ).toHaveLength( 2 );
expect( list[ 0 ] ).toMatchObject( { id: 'group_1', label: 'Test Group 1' } );
expect( list[ 1 ] ).toMatchObject( { id: 'group_2', label: 'Test Group 2' } );
expect( httpClient.get ).toHaveBeenCalledWith( '/wc/v3/settings' );
} );
} );

View File

@ -1,73 +0,0 @@
import { mock, MockProxy } from 'jest-mock-extended';
import { HTTPClient, HTTPResponse } from '../../../../http';
import { settingRESTRepository } from '../setting';
describe( 'settingGroupRESTRepository', () => {
let httpClient: MockProxy< HTTPClient >;
let repository: ReturnType< typeof settingRESTRepository >;
beforeEach( () => {
httpClient = mock< HTTPClient >();
repository = settingRESTRepository( httpClient );
} );
it( 'should list', async () => {
httpClient.get.mockResolvedValue( new HTTPResponse(
200,
{},
[
{
id: 'setting_1',
label: 'Test Setting 1',
},
{
id: 'setting_2',
label: 'Test Setting 2',
},
],
) );
const list = await repository.list( 'general' );
expect( list ).toHaveLength( 2 );
expect( list[ 0 ] ).toMatchObject( { id: 'setting_1', label: 'Test Setting 1' } );
expect( list[ 1 ] ).toMatchObject( { id: 'setting_2', label: 'Test Setting 2' } );
expect( httpClient.get ).toHaveBeenCalledWith( '/wc/v3/settings/general' );
} );
it( 'should read', async () => {
httpClient.get.mockResolvedValue( new HTTPResponse(
200,
{},
{
id: 'setting_1',
label: 'Test Setting',
},
) );
const read = await repository.read( 'general', 'setting_1' );
expect( read ).toMatchObject( { id: 'setting_1', label: 'Test Setting' } );
expect( httpClient.get ).toHaveBeenCalledWith( '/wc/v3/settings/general/setting_1' );
} );
it( 'should update', async () => {
httpClient.patch.mockResolvedValue( new HTTPResponse(
200,
{},
{
id: 'setting_1',
label: 'Test Setting',
value: 'updated-value',
},
) );
const updated = await repository.update( 'general', 'setting_1', { value: 'test-value' } );
expect( updated ).toMatchObject( { id: 'setting_1', value: 'updated-value' } );
expect( httpClient.patch ).toHaveBeenCalledWith(
'/wc/v3/settings/general/setting_1',
{ value: 'test-value' },
);
} );
} );

View File

@ -1,24 +1,17 @@
import { HTTPClient } from '../../../http';
import { ListFn, ModelRepository } from '../../../framework/model-repository';
import { ModelRepository } from '../../../framework/model-repository';
import { SettingGroup } from '../../../models';
import { ListsSettingGroups, SettingGroupRepositoryParams } from '../../../models/settings/setting-group';
import { ModelTransformer } from '../../../framework/model-transformer';
import { KeyChangeTransformation } from '../../../framework/transformations/key-change-transformation';
import { restList } from '../shared';
function restList( httpClient: HTTPClient ): ListFn< SettingGroupRepositoryParams > {
return async () => {
const response = await httpClient.get( '/wc/v3/settings' );
const list: SettingGroup[] = [];
for ( const raw of response.data ) {
list.push( new SettingGroup( {
id: raw.id,
label: raw.label,
description: raw.description,
parentID: raw.parent_id,
} ) );
}
return Promise.resolve( list );
};
function createTransformer(): ModelTransformer< SettingGroup > {
return new ModelTransformer(
[
new KeyChangeTransformation< SettingGroup >( { parentID: 'parent_id' } ),
],
);
}
/**
@ -28,8 +21,10 @@ function restList( httpClient: HTTPClient ): ListFn< SettingGroupRepositoryParam
* @return {ListsSettingGroups} The created repository.
*/
export function settingGroupRESTRepository( httpClient: HTTPClient ): ListsSettingGroups {
const transformer = createTransformer();
return new ModelRepository(
restList( httpClient ),
restList< SettingGroupRepositoryParams >( () => '/wc/v3/settings', SettingGroup, httpClient, transformer ),
null,
null,
null,

View File

@ -1,10 +1,5 @@
import { HTTPClient } from '../../../http';
import {
ListChildFn,
ModelRepository,
ReadChildFn,
UpdateChildFn,
} from '../../../framework/model-repository';
import { ModelRepository, ParentID } from '../../../framework/model-repository';
import { Setting } from '../../../models';
import {
ListsSettings,
@ -12,61 +7,12 @@ import {
SettingRepositoryParams,
UpdatesSettings,
} from '../../../models/settings/setting';
import { ModelTransformer } from '../../../framework/model-transformer';
import { restListChild, restReadChild, restUpdateChild } from '../shared';
import { ModelID } from '../../../models/model';
function restList( httpClient: HTTPClient ): ListChildFn< SettingRepositoryParams > {
return async ( parent ) => {
const response = await httpClient.get( '/wc/v3/settings/' + parent );
const list: Setting[] = [];
for ( const raw of response.data ) {
list.push( new Setting( {
id: raw.id,
label: raw.label,
description: raw.description,
type: raw.type,
options: raw.options,
default: raw.default,
value: raw.value,
} ) );
}
return Promise.resolve( list );
};
}
function restRead( httpClient: HTTPClient ): ReadChildFn< SettingRepositoryParams > {
return async ( parent, id ) => {
const response = await httpClient.get( '/wc/v3/settings/' + parent + '/' + id );
return Promise.resolve( new Setting( {
id: response.data.id,
label: response.data.label,
description: response.data.description,
type: response.data.type,
options: response.data.options,
default: response.data.default,
value: response.data.value,
} ) );
};
}
function restUpdate( httpClient: HTTPClient ): UpdateChildFn< SettingRepositoryParams > {
return async ( parent, id, params ) => {
const response = await httpClient.patch(
'/wc/v3/settings/' + parent + '/' + id,
params,
);
return Promise.resolve( new Setting( {
id: response.data.id,
label: response.data.label,
description: response.data.description,
type: response.data.type,
options: response.data.options,
default: response.data.default,
value: response.data.value,
} ) );
};
function createTransformer(): ModelTransformer< Setting > {
return new ModelTransformer( [] );
}
/**
@ -76,11 +22,14 @@ function restUpdate( httpClient: HTTPClient ): UpdateChildFn< SettingRepositoryP
* @return {ListsSettings|ReadsSettings|UpdatesSettings} The created repository.
*/
export function settingRESTRepository( httpClient: HTTPClient ): ListsSettings & ReadsSettings & UpdatesSettings {
const buildURL = ( parent: ParentID< SettingRepositoryParams >, id: ModelID ) => '/wc/v3/settings/' + parent + '/' + id;
const transformer = createTransformer();
return new ModelRepository(
restList( httpClient ),
restListChild< SettingRepositoryParams >( ( parent ) => '/wc/v3/settings/' + parent, Setting, httpClient, transformer ),
null,
restRead( httpClient ),
restUpdate( httpClient ),
restReadChild< SettingRepositoryParams >( buildURL, Setting, httpClient, transformer ),
restUpdateChild< SettingRepositoryParams >( buildURL, Setting, httpClient, transformer ),
null,
);
}

Some files were not shown because too many files have changed in this diff Show More