Merge branch 'master' into use-mozart-to-renamespace-dependencies

This commit is contained in:
Nestor Soriano 2020-11-18 09:00:02 +01:00
commit 556ea1ecfa
141 changed files with 7190 additions and 3978 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:
@ -89,3 +88,13 @@ branches:
- master
- /^\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 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

@ -5,8 +5,7 @@
<a href="https://packagist.org/packages/woocommerce/woocommerce"><img src="https://poser.pugx.org/woocommerce/woocommerce/v/stable" alt="Latest Stable Version"></a>
<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.org/woocommerce/woocommerce"><img src="https://travis-ci.org/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://travis-ci.com/woocommerce/woocommerce"><img src="https://travis-ci.com/woocommerce/woocommerce.svg?branch=master" alt="Build Status"></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();
@ -563,7 +562,7 @@ jQuery( function( $ ) {
wc_checkout_form.$checkout_form.removeClass( 'processing' ).unblock();
wc_checkout_form.$checkout_form.find( '.input-text, select, input:checkbox' ).trigger( 'validate' ).blur();
wc_checkout_form.scroll_to_notices();
$( document.body ).trigger( 'checkout_error' );
$( document.body ).trigger( 'checkout_error' , [ error_message ] );
},
scroll_to_notices: function() {
var scrollElement = $( '.woocommerce-NoticeGroup-updateOrderReview, .woocommerce-NoticeGroup-checkout' );

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": [
{
@ -379,7 +379,7 @@
"platform": [],
"platform-dev": [],
"platform-overrides": {
"php": "7.1"
"php": "7.0"
},
"plugin-api-version": "1.1.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"
}
}
}

View File

@ -4,39 +4,37 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "7c7613fd37a21222e941715ffa70af20",
"content-hash": "cbe696cc9c487e3027f943e82d88261f",
"packages": [],
"packages-dev": [
{
"name": "doctrine/instantiator",
"version": "1.3.1",
"version": "1.0.5",
"source": {
"type": "git",
"url": "https://github.com/doctrine/instantiator.git",
"reference": "f350df0268e904597e3bd9c4685c53e0e333feea"
"reference": "8e884e78f9f0eb1329e445619e04456e64d8051d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/instantiator/zipball/f350df0268e904597e3bd9c4685c53e0e333feea",
"reference": "f350df0268e904597e3bd9c4685c53e0e333feea",
"url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d",
"reference": "8e884e78f9f0eb1329e445619e04456e64d8051d",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
"php": ">=5.3,<8.0-DEV"
},
"require-dev": {
"doctrine/coding-standard": "^6.0",
"athletic/athletic": "~0.1.8",
"ext-pdo": "*",
"ext-phar": "*",
"phpbench/phpbench": "^0.13",
"phpstan/phpstan-phpunit": "^0.11",
"phpstan/phpstan-shim": "^0.11",
"phpunit/phpunit": "^7.0"
"phpunit/phpunit": "~4.0",
"squizlabs/php_codesniffer": "~2.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.2.x-dev"
"dev-master": "1.0.x-dev"
}
},
"autoload": {
@ -56,51 +54,38 @@
}
],
"description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors",
"homepage": "https://www.doctrine-project.org/projects/instantiator.html",
"homepage": "https://github.com/doctrine/instantiator",
"keywords": [
"constructor",
"instantiate"
],
"funding": [
{
"url": "https://www.doctrine-project.org/sponsorship.html",
"type": "custom"
},
{
"url": "https://www.patreon.com/phpdoctrine",
"type": "patreon"
},
{
"url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator",
"type": "tidelift"
}
],
"time": "2020-05-29T17:27:14+00:00"
"support": {
"issues": "https://github.com/doctrine/instantiator/issues",
"source": "https://github.com/doctrine/instantiator/tree/master"
},
"time": "2015-06-14T21:17:01+00:00"
},
{
"name": "myclabs/deep-copy",
"version": "1.10.1",
"version": "1.7.0",
"source": {
"type": "git",
"url": "https://github.com/myclabs/DeepCopy.git",
"reference": "969b211f9a51aa1f6c01d1d2aef56d3bd91598e5"
"reference": "3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/969b211f9a51aa1f6c01d1d2aef56d3bd91598e5",
"reference": "969b211f9a51aa1f6c01d1d2aef56d3bd91598e5",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e",
"reference": "3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
"replace": {
"myclabs/deep-copy": "self.version"
"php": "^5.6 || ^7.0"
},
"require-dev": {
"doctrine/collections": "^1.0",
"doctrine/common": "^2.6",
"phpunit/phpunit": "^7.1"
"phpunit/phpunit": "^4.1"
},
"type": "library",
"autoload": {
@ -123,32 +108,30 @@
"object",
"object graph"
],
"funding": [
{
"url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
"type": "tidelift"
}
],
"time": "2020-06-29T13:22:24+00:00"
"support": {
"issues": "https://github.com/myclabs/DeepCopy/issues",
"source": "https://github.com/myclabs/DeepCopy/tree/1.x"
},
"time": "2017-10-19T19:58:43+00:00"
},
{
"name": "phar-io/manifest",
"version": "1.0.3",
"version": "1.0.1",
"source": {
"type": "git",
"url": "https://github.com/phar-io/manifest.git",
"reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4"
"reference": "2df402786ab5368a0169091f61a7c1e0eb6852d0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phar-io/manifest/zipball/7761fcacf03b4d4f16e7ccb606d4879ca431fcf4",
"reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4",
"url": "https://api.github.com/repos/phar-io/manifest/zipball/2df402786ab5368a0169091f61a7c1e0eb6852d0",
"reference": "2df402786ab5368a0169091f61a7c1e0eb6852d0",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-phar": "*",
"phar-io/version": "^2.0",
"phar-io/version": "^1.0.1",
"php": "^5.6 || ^7.0"
},
"type": "library",
@ -184,20 +167,24 @@
}
],
"description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)",
"time": "2018-07-08T19:23:20+00:00"
"support": {
"issues": "https://github.com/phar-io/manifest/issues",
"source": "https://github.com/phar-io/manifest/tree/master"
},
"time": "2017-03-05T18:14:27+00:00"
},
{
"name": "phar-io/version",
"version": "2.0.1",
"version": "1.0.1",
"source": {
"type": "git",
"url": "https://github.com/phar-io/version.git",
"reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6"
"reference": "a70c0ced4be299a63d32fa96d9281d03e94041df"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phar-io/version/zipball/45a2ec53a73c70ce41d55cedef9063630abaf1b6",
"reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6",
"url": "https://api.github.com/repos/phar-io/version/zipball/a70c0ced4be299a63d32fa96d9281d03e94041df",
"reference": "a70c0ced4be299a63d32fa96d9281d03e94041df",
"shasum": ""
},
"require": {
@ -231,34 +218,43 @@
}
],
"description": "Library for handling version information and constraints",
"time": "2018-07-08T19:19:57+00:00"
"support": {
"issues": "https://github.com/phar-io/version/issues",
"source": "https://github.com/phar-io/version/tree/master"
},
"time": "2017-03-05T17:38:23+00:00"
},
{
"name": "phpdocumentor/reflection-common",
"version": "2.1.0",
"version": "1.0.1",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/ReflectionCommon.git",
"reference": "6568f4687e5b41b054365f9ae03fcb1ed5f2069b"
"reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/6568f4687e5b41b054365f9ae03fcb1ed5f2069b",
"reference": "6568f4687e5b41b054365f9ae03fcb1ed5f2069b",
"url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6",
"reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6",
"shasum": ""
},
"require": {
"php": ">=7.1"
"php": ">=5.5"
},
"require-dev": {
"phpunit/phpunit": "^4.6"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.x-dev"
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"phpDocumentor\\Reflection\\": "src/"
"phpDocumentor\\Reflection\\": [
"src"
]
}
},
"notification-url": "https://packagist.org/downloads/",
@ -280,7 +276,11 @@
"reflection",
"static analysis"
],
"time": "2020-04-27T09:25:28+00:00"
"support": {
"issues": "https://github.com/phpDocumentor/ReflectionCommon/issues",
"source": "https://github.com/phpDocumentor/ReflectionCommon/tree/master"
},
"time": "2017-09-11T18:02:19+00:00"
},
{
"name": "phpdocumentor/reflection-docblock",
@ -336,31 +336,30 @@
},
{
"name": "phpdocumentor/type-resolver",
"version": "1.0.1",
"version": "0.5.1",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/TypeResolver.git",
"reference": "2e32a6d48972b2c1976ed5d8967145b6cec4a4a9"
"reference": "cf842904952e64e703800d094cdf34e715a8a3ae"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/2e32a6d48972b2c1976ed5d8967145b6cec4a4a9",
"reference": "2e32a6d48972b2c1976ed5d8967145b6cec4a4a9",
"url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/cf842904952e64e703800d094cdf34e715a8a3ae",
"reference": "cf842904952e64e703800d094cdf34e715a8a3ae",
"shasum": ""
},
"require": {
"php": "^7.1",
"phpdocumentor/reflection-common": "^2.0"
"php": "^7.0",
"phpdocumentor/reflection-common": "^1.0"
},
"require-dev": {
"ext-tokenizer": "^7.1",
"mockery/mockery": "~1",
"phpunit/phpunit": "^7.0"
"mockery/mockery": "^1.0",
"phpunit/phpunit": "^6.4"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.x-dev"
"dev-master": "1.0.x-dev"
}
},
"autoload": {
@ -378,8 +377,11 @@
"email": "me@mikevanriel.com"
}
],
"description": "A PSR-5 based resolver of Class names, Types and Structural Element Names",
"time": "2019-08-22T18:11:29+00:00"
"support": {
"issues": "https://github.com/phpDocumentor/TypeResolver/issues",
"source": "https://github.com/phpDocumentor/TypeResolver/tree/master"
},
"time": "2017-12-30T13:23:38+00:00"
},
{
"name": "phpspec/prophecy",
@ -446,40 +448,40 @@
},
{
"name": "phpunit/php-code-coverage",
"version": "6.1.4",
"version": "5.3.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
"reference": "807e6013b00af69b6c5d9ceb4282d0393dbb9d8d"
"reference": "c89677919c5dd6d3b3852f230a663118762218ac"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/807e6013b00af69b6c5d9ceb4282d0393dbb9d8d",
"reference": "807e6013b00af69b6c5d9ceb4282d0393dbb9d8d",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/c89677919c5dd6d3b3852f230a663118762218ac",
"reference": "c89677919c5dd6d3b3852f230a663118762218ac",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-xmlwriter": "*",
"php": "^7.1",
"phpunit/php-file-iterator": "^2.0",
"php": "^7.0",
"phpunit/php-file-iterator": "^1.4.2",
"phpunit/php-text-template": "^1.2.1",
"phpunit/php-token-stream": "^3.0",
"phpunit/php-token-stream": "^2.0.1",
"sebastian/code-unit-reverse-lookup": "^1.0.1",
"sebastian/environment": "^3.1 || ^4.0",
"sebastian/environment": "^3.0",
"sebastian/version": "^2.0.1",
"theseer/tokenizer": "^1.1"
},
"require-dev": {
"phpunit/phpunit": "^7.0"
"phpunit/phpunit": "^6.0"
},
"suggest": {
"ext-xdebug": "^2.6.0"
"ext-xdebug": "^2.5.5"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "6.1-dev"
"dev-master": "5.3.x-dev"
}
},
"autoload": {
@ -505,32 +507,33 @@
"testing",
"xunit"
],
"time": "2018-10-31T16:06:48+00:00"
"support": {
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/5.3"
},
"time": "2018-04-06T15:36:58+00:00"
},
{
"name": "phpunit/php-file-iterator",
"version": "2.0.2",
"version": "1.4.5",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-file-iterator.git",
"reference": "050bedf145a257b1ff02746c31894800e5122946"
"reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/050bedf145a257b1ff02746c31894800e5122946",
"reference": "050bedf145a257b1ff02746c31894800e5122946",
"url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/730b01bc3e867237eaac355e06a36b85dd93a8b4",
"reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4",
"shasum": ""
},
"require": {
"php": "^7.1"
},
"require-dev": {
"phpunit/phpunit": "^7.1"
"php": ">=5.3.3"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.0.x-dev"
"dev-master": "1.4.x-dev"
}
},
"autoload": {
@ -545,7 +548,7 @@
"authors": [
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de",
"email": "sb@sebastian-bergmann.de",
"role": "lead"
}
],
@ -555,7 +558,12 @@
"filesystem",
"iterator"
],
"time": "2018-09-13T20:33:42+00:00"
"support": {
"irc": "irc://irc.freenode.net/phpunit",
"issues": "https://github.com/sebastianbergmann/php-file-iterator/issues",
"source": "https://github.com/sebastianbergmann/php-file-iterator/tree/1.4.5"
},
"time": "2017-11-27T13:52:08+00:00"
},
{
"name": "phpunit/php-text-template",
@ -600,28 +608,28 @@
},
{
"name": "phpunit/php-timer",
"version": "2.1.2",
"version": "1.0.9",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-timer.git",
"reference": "1038454804406b0b5f5f520358e78c1c2f71501e"
"reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/1038454804406b0b5f5f520358e78c1c2f71501e",
"reference": "1038454804406b0b5f5f520358e78c1c2f71501e",
"url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3dcf38ca72b158baf0bc245e9184d3fdffa9c46f",
"reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f",
"shasum": ""
},
"require": {
"php": "^7.1"
"php": "^5.3.3 || ^7.0"
},
"require-dev": {
"phpunit/phpunit": "^7.0"
"phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.1-dev"
"dev-master": "1.0-dev"
}
},
"autoload": {
@ -636,7 +644,7 @@
"authors": [
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de",
"email": "sb@sebastian-bergmann.de",
"role": "lead"
}
],
@ -645,33 +653,37 @@
"keywords": [
"timer"
],
"time": "2019-06-07T04:22:29+00:00"
"support": {
"issues": "https://github.com/sebastianbergmann/php-timer/issues",
"source": "https://github.com/sebastianbergmann/php-timer/tree/master"
},
"time": "2017-02-26T11:10:40+00:00"
},
{
"name": "phpunit/php-token-stream",
"version": "3.1.1",
"version": "2.0.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-token-stream.git",
"reference": "995192df77f63a59e47f025390d2d1fdf8f425ff"
"reference": "791198a2c6254db10131eecfe8c06670700904db"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/995192df77f63a59e47f025390d2d1fdf8f425ff",
"reference": "995192df77f63a59e47f025390d2d1fdf8f425ff",
"url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/791198a2c6254db10131eecfe8c06670700904db",
"reference": "791198a2c6254db10131eecfe8c06670700904db",
"shasum": ""
},
"require": {
"ext-tokenizer": "*",
"php": "^7.1"
"php": "^7.0"
},
"require-dev": {
"phpunit/phpunit": "^7.0"
"phpunit/phpunit": "^6.2.4"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.1-dev"
"dev-master": "2.0-dev"
}
},
"autoload": {
@ -694,58 +706,62 @@
"keywords": [
"tokenizer"
],
"support": {
"issues": "https://github.com/sebastianbergmann/php-token-stream/issues",
"source": "https://github.com/sebastianbergmann/php-token-stream/tree/master"
},
"abandoned": true,
"time": "2019-09-17T06:23:10+00:00"
"time": "2017-11-27T05:48:46+00:00"
},
{
"name": "phpunit/phpunit",
"version": "7.5.20",
"version": "6.5.14",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "9467db479d1b0487c99733bb1e7944d32deded2c"
"reference": "bac23fe7ff13dbdb461481f706f0e9fe746334b7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9467db479d1b0487c99733bb1e7944d32deded2c",
"reference": "9467db479d1b0487c99733bb1e7944d32deded2c",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/bac23fe7ff13dbdb461481f706f0e9fe746334b7",
"reference": "bac23fe7ff13dbdb461481f706f0e9fe746334b7",
"shasum": ""
},
"require": {
"doctrine/instantiator": "^1.1",
"ext-dom": "*",
"ext-json": "*",
"ext-libxml": "*",
"ext-mbstring": "*",
"ext-xml": "*",
"myclabs/deep-copy": "^1.7",
"phar-io/manifest": "^1.0.2",
"phar-io/version": "^2.0",
"php": "^7.1",
"myclabs/deep-copy": "^1.6.1",
"phar-io/manifest": "^1.0.1",
"phar-io/version": "^1.0",
"php": "^7.0",
"phpspec/prophecy": "^1.7",
"phpunit/php-code-coverage": "^6.0.7",
"phpunit/php-file-iterator": "^2.0.1",
"phpunit/php-code-coverage": "^5.3",
"phpunit/php-file-iterator": "^1.4.3",
"phpunit/php-text-template": "^1.2.1",
"phpunit/php-timer": "^2.1",
"sebastian/comparator": "^3.0",
"sebastian/diff": "^3.0",
"sebastian/environment": "^4.0",
"phpunit/php-timer": "^1.0.9",
"phpunit/phpunit-mock-objects": "^5.0.9",
"sebastian/comparator": "^2.1",
"sebastian/diff": "^2.0",
"sebastian/environment": "^3.1",
"sebastian/exporter": "^3.1",
"sebastian/global-state": "^2.0",
"sebastian/object-enumerator": "^3.0.3",
"sebastian/resource-operations": "^2.0",
"sebastian/resource-operations": "^1.0",
"sebastian/version": "^2.0.1"
},
"conflict": {
"phpunit/phpunit-mock-objects": "*"
"phpdocumentor/reflection-docblock": "3.0.2",
"phpunit/dbunit": "<3.0"
},
"require-dev": {
"ext-pdo": "*"
},
"suggest": {
"ext-soap": "*",
"ext-xdebug": "*",
"phpunit/php-invoker": "^2.0"
"phpunit/php-invoker": "^1.1"
},
"bin": [
"phpunit"
@ -753,7 +769,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "7.5-dev"
"dev-master": "6.5.x-dev"
}
},
"autoload": {
@ -779,7 +795,75 @@
"testing",
"xunit"
],
"time": "2020-01-08T08:45:45+00:00"
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"source": "https://github.com/sebastianbergmann/phpunit/tree/6.5.14"
},
"time": "2019-02-01T05:22:47+00:00"
},
{
"name": "phpunit/phpunit-mock-objects",
"version": "5.0.10",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git",
"reference": "cd1cf05c553ecfec36b170070573e540b67d3f1f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/cd1cf05c553ecfec36b170070573e540b67d3f1f",
"reference": "cd1cf05c553ecfec36b170070573e540b67d3f1f",
"shasum": ""
},
"require": {
"doctrine/instantiator": "^1.0.5",
"php": "^7.0",
"phpunit/php-text-template": "^1.2.1",
"sebastian/exporter": "^3.1"
},
"conflict": {
"phpunit/phpunit": "<6.0"
},
"require-dev": {
"phpunit/phpunit": "^6.5.11"
},
"suggest": {
"ext-soap": "*"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "5.0.x-dev"
}
},
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de",
"role": "lead"
}
],
"description": "Mock Object library for PHPUnit",
"homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/",
"keywords": [
"mock",
"xunit"
],
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit-mock-objects/issues",
"source": "https://github.com/sebastianbergmann/phpunit-mock-objects/tree/5.0.10"
},
"abandoned": true,
"time": "2018-08-09T05:50:03+00:00"
},
{
"name": "sebastian/code-unit-reverse-lookup",
@ -828,30 +912,30 @@
},
{
"name": "sebastian/comparator",
"version": "3.0.2",
"version": "2.1.3",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/comparator.git",
"reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da"
"reference": "34369daee48eafb2651bea869b4b15d75ccc35f9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/5de4fc177adf9bce8df98d8d141a7559d7ccf6da",
"reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da",
"url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/34369daee48eafb2651bea869b4b15d75ccc35f9",
"reference": "34369daee48eafb2651bea869b4b15d75ccc35f9",
"shasum": ""
},
"require": {
"php": "^7.1",
"sebastian/diff": "^3.0",
"php": "^7.0",
"sebastian/diff": "^2.0 || ^3.0",
"sebastian/exporter": "^3.1"
},
"require-dev": {
"phpunit/phpunit": "^7.1"
"phpunit/phpunit": "^6.4"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.0-dev"
"dev-master": "2.1.x-dev"
}
},
"autoload": {
@ -888,33 +972,36 @@
"compare",
"equality"
],
"time": "2018-07-12T15:12:46+00:00"
"support": {
"issues": "https://github.com/sebastianbergmann/comparator/issues",
"source": "https://github.com/sebastianbergmann/comparator/tree/master"
},
"time": "2018-02-01T13:46:46+00:00"
},
{
"name": "sebastian/diff",
"version": "3.0.2",
"version": "2.0.1",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/diff.git",
"reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29"
"reference": "347c1d8b49c5c3ee30c7040ea6fc446790e6bddd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/720fcc7e9b5cf384ea68d9d930d480907a0c1a29",
"reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29",
"url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/347c1d8b49c5c3ee30c7040ea6fc446790e6bddd",
"reference": "347c1d8b49c5c3ee30c7040ea6fc446790e6bddd",
"shasum": ""
},
"require": {
"php": "^7.1"
"php": "^7.0"
},
"require-dev": {
"phpunit/phpunit": "^7.5 || ^8.0",
"symfony/process": "^2 || ^3.3 || ^4"
"phpunit/phpunit": "^6.2"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.0-dev"
"dev-master": "2.0-dev"
}
},
"autoload": {
@ -939,40 +1026,38 @@
"description": "Diff implementation",
"homepage": "https://github.com/sebastianbergmann/diff",
"keywords": [
"diff",
"udiff",
"unidiff",
"unified diff"
"diff"
],
"time": "2019-02-04T06:01:07+00:00"
"support": {
"issues": "https://github.com/sebastianbergmann/diff/issues",
"source": "https://github.com/sebastianbergmann/diff/tree/master"
},
"time": "2017-08-03T08:09:46+00:00"
},
{
"name": "sebastian/environment",
"version": "4.2.3",
"version": "3.1.0",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/environment.git",
"reference": "464c90d7bdf5ad4e8a6aea15c091fec0603d4368"
"reference": "cd0871b3975fb7fc44d11314fd1ee20925fce4f5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/464c90d7bdf5ad4e8a6aea15c091fec0603d4368",
"reference": "464c90d7bdf5ad4e8a6aea15c091fec0603d4368",
"url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/cd0871b3975fb7fc44d11314fd1ee20925fce4f5",
"reference": "cd0871b3975fb7fc44d11314fd1ee20925fce4f5",
"shasum": ""
},
"require": {
"php": "^7.1"
"php": "^7.0"
},
"require-dev": {
"phpunit/phpunit": "^7.5"
},
"suggest": {
"ext-posix": "*"
"phpunit/phpunit": "^6.1"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "4.2-dev"
"dev-master": "3.1.x-dev"
}
},
"autoload": {
@ -997,7 +1082,11 @@
"environment",
"hhvm"
],
"time": "2019-11-20T08:46:58+00:00"
"support": {
"issues": "https://github.com/sebastianbergmann/environment/issues",
"source": "https://github.com/sebastianbergmann/environment/tree/master"
},
"time": "2017-07-01T08:51:00+00:00"
},
{
"name": "sebastian/exporter",
@ -1264,25 +1353,25 @@
},
{
"name": "sebastian/resource-operations",
"version": "2.0.1",
"version": "1.0.0",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/resource-operations.git",
"reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9"
"reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/4d7a795d35b889bf80a0cc04e08d77cedfa917a9",
"reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9",
"url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/ce990bb21759f94aeafd30209e8cfcdfa8bc3f52",
"reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52",
"shasum": ""
},
"require": {
"php": "^7.1"
"php": ">=5.6.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.0-dev"
"dev-master": "1.0.x-dev"
}
},
"autoload": {
@ -1302,7 +1391,11 @@
],
"description": "Provides a list of PHP built-in functions that operate on resources",
"homepage": "https://www.github.com/sebastianbergmann/resource-operations",
"time": "2018-10-04T04:07:39+00:00"
"support": {
"issues": "https://github.com/sebastianbergmann/resource-operations/issues",
"source": "https://github.com/sebastianbergmann/resource-operations/tree/master"
},
"time": "2015-07-28T20:34:47+00:00"
},
{
"name": "sebastian/version",
@ -1349,20 +1442,20 @@
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.20.0",
"version": "v1.19.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "f4ba089a5b6366e453971d3aad5fe8e897b37f41"
"reference": "aed596913b70fae57be53d86faa2e9ef85a2297b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/f4ba089a5b6366e453971d3aad5fe8e897b37f41",
"reference": "f4ba089a5b6366e453971d3aad5fe8e897b37f41",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/aed596913b70fae57be53d86faa2e9ef85a2297b",
"reference": "aed596913b70fae57be53d86faa2e9ef85a2297b",
"shasum": ""
},
"require": {
"php": ">=7.1"
"php": ">=5.3.3"
},
"suggest": {
"ext-ctype": "For best performance"
@ -1370,7 +1463,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.20-dev"
"dev-main": "1.19-dev"
},
"thanks": {
"name": "symfony/polyfill",
@ -1407,6 +1500,9 @@
"polyfill",
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.19.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
@ -1421,7 +1517,7 @@
"type": "tidelift"
}
],
"time": "2020-10-23T14:02:19+00:00"
"time": "2020-10-23T09:01:57+00:00"
},
{
"name": "theseer/tokenizer",
@ -1521,7 +1617,7 @@
"platform": [],
"platform-dev": [],
"platform-overrides": {
"php": "7.1"
"php": "7.0"
},
"plugin-api-version": "1.1.0"
}

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": [
{
@ -272,22 +272,27 @@
},
{
"name": "symfony/finder",
"version": "v3.4.46",
"version": "v3.3.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
"reference": "4e1da3c110c52d868f8a9153b7de3ebc381fba78"
"reference": "baea7f66d30854ad32988c11a09d7ffd485810c4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/4e1da3c110c52d868f8a9153b7de3ebc381fba78",
"reference": "4e1da3c110c52d868f8a9153b7de3ebc381fba78",
"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.3-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Component\\Finder\\": ""
@ -312,21 +317,10 @@
],
"description": "Symfony Finder Component",
"homepage": "https://symfony.com",
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2020-10-24T10:57:07+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",
@ -554,7 +548,7 @@
"platform": [],
"platform-dev": [],
"platform-overrides": {
"php": "7.1"
"php": "7.0"
},
"plugin-api-version": "1.1.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,14 @@
"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": {
"bamarni/composer-bin-plugin": "^1.4",
@ -24,7 +25,7 @@
},
"config": {
"platform": {
"php": "7.1"
"php": "7.0"
},
"preferred-install": {
"woocommerce/action-scheduler": "dist",
@ -90,10 +91,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",

265
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": "f2b2db27127990ff25db309d00a0dbcb",
"content-hash": "0373a0a4d2df08885d048edab884822b",
"packages": [
{
"name": "automattic/jetpack-autoloader",
@ -75,28 +75,31 @@
},
{
"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 +135,7 @@
"Kanboard",
"Lan Management System",
"MODX Evo",
"MantisBT",
"Mautic",
"Maya",
"OXID",
@ -186,6 +190,7 @@
"shopware",
"silverstripe",
"sydes",
"sylius",
"symfony",
"typo3",
"wordpress",
@ -193,7 +198,93 @@
"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",
"version": "3.3.3",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/container.git",
"reference": "7dc67bdf89efc338e674863c0ea70a63efe4de05"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/container/zipball/7dc67bdf89efc338e674863c0ea70a63efe4de05",
"reference": "7dc67bdf89efc338e674863c0ea70a63efe4de05",
"shasum": ""
},
"require": {
"php": "^7.0 || ^8.0",
"psr/container": "^1.0"
},
"provide": {
"psr/container-implementation": "^1.0"
},
"replace": {
"orno/di": "~2.0"
},
"require-dev": {
"phpunit/phpunit": "^6.0",
"squizlabs/php_codesniffer": "^3.3"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-3.x": "3.x-dev",
"dev-2.x": "2.x-dev",
"dev-1.x": "1.x-dev"
}
},
"autoload": {
"psr-4": {
"League\\Container\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Phil Bennett",
"email": "philipobenito@gmail.com",
"homepage": "http://www.philipobenito.com",
"role": "Developer"
}
],
"description": "A fast and intuitive dependency injection container.",
"homepage": "https://github.com/thephpleague/container",
"keywords": [
"container",
"dependency",
"di",
"injection",
"league",
"provider",
"service"
],
"funding": [
{
"url": "https://github.com/philipobenito",
"type": "github"
}
],
"time": "2020-09-28T13:38:44+00:00"
},
{
"name": "maxmind-db/reader",
@ -380,22 +471,27 @@
},
{
"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\\": ""
@ -409,14 +505,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"
@ -424,21 +520,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",
@ -477,26 +562,27 @@
},
{
"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": {
@ -507,9 +593,6 @@
}
},
"autoload": {
"classmap": [
"includes/"
],
"psr-4": {
"Automattic\\WooCommerce\\Admin\\": "src/"
}
@ -520,29 +603,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": {
@ -567,7 +654,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": [
@ -616,78 +707,6 @@
"tool"
],
"time": "2020-05-03T08:27:20+00:00"
},
{
"name": "league/container",
"version": "3.3.3",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/container.git",
"reference": "7dc67bdf89efc338e674863c0ea70a63efe4de05"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/container/zipball/7dc67bdf89efc338e674863c0ea70a63efe4de05",
"reference": "7dc67bdf89efc338e674863c0ea70a63efe4de05",
"shasum": ""
},
"require": {
"php": "^7.0 || ^8.0",
"psr/container": "^1.0"
},
"provide": {
"psr/container-implementation": "^1.0"
},
"replace": {
"orno/di": "~2.0"
},
"require-dev": {
"phpunit/phpunit": "^6.0",
"squizlabs/php_codesniffer": "^3.3"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-3.x": "3.x-dev",
"dev-2.x": "2.x-dev",
"dev-1.x": "1.x-dev"
}
},
"autoload": {
"psr-4": {
"League\\Container\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Phil Bennett",
"email": "philipobenito@gmail.com",
"homepage": "http://www.philipobenito.com",
"role": "Developer"
}
],
"description": "A fast and intuitive dependency injection container.",
"homepage": "https://github.com/thephpleague/container",
"keywords": [
"container",
"dependency",
"di",
"injection",
"league",
"provider",
"service"
],
"funding": [
{
"url": "https://github.com/philipobenito",
"type": "github"
}
],
"time": "2020-09-28T13:38:44+00:00"
}
],
"aliases": [],
@ -700,7 +719,7 @@
},
"platform-dev": [],
"platform-overrides": {
"php": "7.1"
"php": "7.0"
},
"plugin-api-version": "1.1.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

@ -1738,7 +1738,7 @@ class WC_Product extends WC_Abstract_Legacy_Product {
* @return boolean
*/
public function has_options() {
return false;
return apply_filters( 'woocommerce_product_has_options', false, $this );
}
/*

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

@ -57,6 +57,10 @@ class WC_Helper_Updater {
'upgrade_notice' => $data['upgrade_notice'],
);
if ( isset( $data['requires_php'] ) ) {
$item['requires_php'] = $data['requires_php'];
}
// We don't want to deliver a valid upgrade package when their subscription has expired.
// To avoid the generic "no_package" error that empty strings give, we will store an
// indication of expiration for the `upgrader_pre_download` filter to error on.

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

@ -885,10 +885,12 @@ class WC_Form_Handler {
* @return bool success or not
*/
private static function add_to_cart_handler_variable( $product_id ) {
$variation_id = empty( $_REQUEST['variation_id'] ) ? '' : absint( wp_unslash( $_REQUEST['variation_id'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$quantity = empty( $_REQUEST['quantity'] ) ? 1 : wc_stock_amount( wp_unslash( $_REQUEST['quantity'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$variation_id = empty( $_REQUEST['variation_id'] ) ? '' : absint( wp_unslash( $_REQUEST['variation_id'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$quantity = empty( $_REQUEST['quantity'] ) ? 1 : wc_stock_amount( wp_unslash( $_REQUEST['quantity'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$variations = array();
$product = wc_get_product( $product_id );
foreach ( $_REQUEST as $key => $value ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( 'attribute_' !== substr( $key, 0, 10 ) ) {
continue;
@ -899,7 +901,19 @@ class WC_Form_Handler {
$passed_validation = apply_filters( 'woocommerce_add_to_cart_validation', true, $product_id, $quantity, $variation_id, $variations );
if ( $passed_validation && false !== WC()->cart->add_to_cart( $product_id, $quantity, $variation_id, $variations ) ) {
if ( ! $passed_validation ) {
return false;
}
// Prevent parent variable product from being added to cart.
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;
}
if ( false !== WC()->cart->add_to_cart( $product_id, $quantity, $variation_id, $variations ) ) {
wc_add_to_cart_message( array( $product_id => $quantity ), true );
return true;
}

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

@ -590,7 +590,7 @@ class WC_Product_Variable extends WC_Product {
* @return boolean
*/
public function has_options() {
return true;
return apply_filters( 'woocommerce_product_has_options', true, $this );
}

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>';
}
@ -565,7 +565,7 @@ class WC_Shortcodes {
$single_product = new WP_Query( $args );
?>
<script type="text/javascript">
jQuery( document ).ready( function( $ ) {
jQuery( function( $ ) {
var $variations_form = $( '[data-product-page-preselected-id="<?php echo esc_attr( $preselected_id ); ?>"]' ).find( 'form.variations_form' );
<?php foreach ( $attributes as $attr => $value ) { ?>

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

@ -102,7 +102,7 @@ class WC_Shop_Customizer {
$max_notice = __( 'The maximum allowed setting is %d', 'woocommerce' );
?>
<script type="text/javascript">
jQuery( document ).ready( function( $ ) {
jQuery( function( $ ) {
$( document.body ).on( 'change', '.woocommerce-cropping-control input[type="radio"]', function() {
var $wrapper = $( this ).closest( '.woocommerce-cropping-control' ),
value = $wrapper.find( 'input:checked' ).val();

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

@ -66,7 +66,10 @@ abstract class WC_Gateway_Paypal_Response {
if ( ! $order->has_status( array( 'processing', 'completed' ) ) ) {
$order->add_order_note( $note );
$order->payment_complete( $txn_id );
WC()->cart->empty_cart();
if ( isset( WC()->cart ) ) {
WC()->cart->empty_cart();
}
}
}
@ -78,6 +81,9 @@ abstract class WC_Gateway_Paypal_Response {
*/
protected function payment_on_hold( $order, $reason = '' ) {
$order->update_status( 'on-hold', $reason );
WC()->cart->empty_cart();
if ( isset( WC()->cart ) ) {
WC()->cart->empty_cart();
}
}
}

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

@ -316,15 +316,16 @@ function wc_cart_totals_order_total_html() {
if ( ! empty( $tax_string_array ) ) {
$taxable_address = WC()->customer->get_taxable_address();
/* translators: %s: country name */
$estimated_text = WC()->customer->is_customer_outside_base() && ! WC()->customer->has_calculated_shipping() ? sprintf( ' ' . __( 'estimated for %s', 'woocommerce' ), WC()->countries->estimated_for_prefix( $taxable_address[0] ) . WC()->countries->countries[ $taxable_address[0] ] ) : '';
$value .= '<small class="includes_tax">('
/* translators: includes tax information */
. esc_html__( 'includes', 'woocommerce' )
. ' '
. wp_kses_post( implode( ', ', $tax_string_array ) )
. esc_html( $estimated_text )
. ')</small>';
if ( WC()->customer->is_customer_outside_base() && ! WC()->customer->has_calculated_shipping() ) {
$country = WC()->countries->estimated_for_prefix( $taxable_address[0] ) . WC()->countries->countries[ $taxable_address[0] ];
/* translators: 1: tax amount 2: country name */
$tax_text = wp_kses_post( sprintf( __( '(includes %1$s estimated for %2$s)', 'woocommerce' ), implode( ', ', $tax_string_array ), $country ) );
} else {
/* translators: %s: tax amount */
$tax_text = wp_kses_post( sprintf( __( '(includes %s)', 'woocommerce' ), implode( ', ', $tax_string_array ) ) );
}
$value .= '<small class="includes_tax">' . $tax_text . '</small>';
}
}

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

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

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