This commit is contained in:
vedanshujain 2020-07-30 17:55:11 +05:30
commit 3a63d75e0f
110 changed files with 5535 additions and 851 deletions

View File

@ -7268,3 +7268,8 @@ table.bar_chart {
}
}
}
.wc-quick-edit-warning {
color: darkred;
font-weight: bold;
}

View File

@ -1,135 +1,166 @@
/*global inlineEditPost, woocommerce_admin, woocommerce_quick_edit */
jQuery(function( $ ) {
$( '#the-list' ).on( 'click', '.editinline', function() {
jQuery(
function( $ ) {
$( '#the-list' ).on(
'click',
'.editinline',
function() {
inlineEditPost.revert();
inlineEditPost.revert();
var post_id = $( this ).closest( 'tr' ).attr( 'id' );
var post_id = $( this ).closest( 'tr' ).attr( 'id' );
post_id = post_id.replace( 'post-', '' );
post_id = post_id.replace( 'post-', '' );
var $wc_inline_data = $( '#woocommerce_inline_' + post_id );
var $wc_inline_data = $( '#woocommerce_inline_' + post_id );
var sku = $wc_inline_data.find( '.sku' ).text(),
regular_price = $wc_inline_data.find( '.regular_price' ).text(),
sale_price = $wc_inline_data.find( '.sale_price ').text(),
weight = $wc_inline_data.find( '.weight' ).text(),
length = $wc_inline_data.find( '.length' ).text(),
width = $wc_inline_data.find( '.width' ).text(),
height = $wc_inline_data.find( '.height' ).text(),
shipping_class = $wc_inline_data.find( '.shipping_class' ).text(),
visibility = $wc_inline_data.find( '.visibility' ).text(),
stock_status = $wc_inline_data.find( '.stock_status' ).text(),
stock = $wc_inline_data.find( '.stock' ).text(),
featured = $wc_inline_data.find( '.featured' ).text(),
manage_stock = $wc_inline_data.find( '.manage_stock' ).text(),
menu_order = $wc_inline_data.find( '.menu_order' ).text(),
tax_status = $wc_inline_data.find( '.tax_status' ).text(),
tax_class = $wc_inline_data.find( '.tax_class' ).text(),
backorders = $wc_inline_data.find( '.backorders' ).text();
var sku = $wc_inline_data.find( '.sku' ).text(),
regular_price = $wc_inline_data.find( '.regular_price' ).text(),
sale_price = $wc_inline_data.find( '.sale_price ' ).text(),
weight = $wc_inline_data.find( '.weight' ).text(),
length = $wc_inline_data.find( '.length' ).text(),
width = $wc_inline_data.find( '.width' ).text(),
height = $wc_inline_data.find( '.height' ).text(),
shipping_class = $wc_inline_data.find( '.shipping_class' ).text(),
visibility = $wc_inline_data.find( '.visibility' ).text(),
stock_status = $wc_inline_data.find( '.stock_status' ).text(),
stock = $wc_inline_data.find( '.stock' ).text(),
featured = $wc_inline_data.find( '.featured' ).text(),
manage_stock = $wc_inline_data.find( '.manage_stock' ).text(),
menu_order = $wc_inline_data.find( '.menu_order' ).text(),
tax_status = $wc_inline_data.find( '.tax_status' ).text(),
tax_class = $wc_inline_data.find( '.tax_class' ).text(),
backorders = $wc_inline_data.find( '.backorders' ).text(),
product_type = $wc_inline_data.find( '.product_type' ).text();
var formatted_regular_price = regular_price.replace('.', woocommerce_admin.mon_decimal_point ),
formatted_sale_price = sale_price.replace('.', woocommerce_admin.mon_decimal_point );
var formatted_regular_price = regular_price.replace( '.', woocommerce_admin.mon_decimal_point ),
formatted_sale_price = sale_price.replace( '.', woocommerce_admin.mon_decimal_point );
$( 'input[name="_sku"]', '.inline-edit-row' ).val( sku );
$( 'input[name="_regular_price"]', '.inline-edit-row' ).val( formatted_regular_price );
$( 'input[name="_sale_price"]', '.inline-edit-row' ).val( formatted_sale_price );
$( 'input[name="_weight"]', '.inline-edit-row' ).val( weight );
$( 'input[name="_length"]', '.inline-edit-row' ).val( length );
$( 'input[name="_width"]', '.inline-edit-row' ).val( width );
$( 'input[name="_height"]', '.inline-edit-row' ).val( height );
$( 'input[name="_sku"]', '.inline-edit-row' ).val( sku );
$( 'input[name="_regular_price"]', '.inline-edit-row' ).val( formatted_regular_price );
$( 'input[name="_sale_price"]', '.inline-edit-row' ).val( formatted_sale_price );
$( 'input[name="_weight"]', '.inline-edit-row' ).val( weight );
$( 'input[name="_length"]', '.inline-edit-row' ).val( length );
$( 'input[name="_width"]', '.inline-edit-row' ).val( width );
$( 'input[name="_height"]', '.inline-edit-row' ).val( height );
$( 'select[name="_shipping_class"] option:selected', '.inline-edit-row' ).attr( 'selected', false ).change();
$( 'select[name="_shipping_class"] option[value="' + shipping_class + '"]' ).attr( 'selected', 'selected' ).change();
$( 'select[name="_shipping_class"] option:selected', '.inline-edit-row' ).attr( 'selected', false ).change();
$( 'select[name="_shipping_class"] option[value="' + shipping_class + '"]' ).attr( 'selected', 'selected' ).change();
$( 'input[name="_stock"]', '.inline-edit-row' ).val( stock );
$( 'input[name="menu_order"]', '.inline-edit-row' ).val( menu_order );
$( 'input[name="_stock"]', '.inline-edit-row' ).val( stock );
$( 'input[name="menu_order"]', '.inline-edit-row' ).val( menu_order );
// eslint-disable-next-line max-len
$( 'select[name="_tax_status"] option, select[name="_tax_class"] option, select[name="_visibility"] option, select[name="_stock_status"] option, select[name="_backorders"] option' ).removeAttr( 'selected' );
$(
'select[name="_tax_status"] option, ' +
'select[name="_tax_class"] option, ' +
'select[name="_visibility"] option, ' +
'select[name="_stock_status"] option, ' +
'select[name="_backorders"] option'
).removeAttr( 'selected' );
$( 'select[name="_tax_status"] option[value="' + tax_status + '"]', '.inline-edit-row' ).attr( 'selected', 'selected' );
$( 'select[name="_tax_class"] option[value="' + tax_class + '"]', '.inline-edit-row' ).attr( 'selected', 'selected' );
$( 'select[name="_visibility"] option[value="' + visibility + '"]', '.inline-edit-row' ).attr( 'selected', 'selected' );
$( 'select[name="_stock_status"] option[value="' + stock_status + '"]', '.inline-edit-row' ).attr( 'selected', 'selected' );
$( 'select[name="_backorders"] option[value="' + backorders + '"]', '.inline-edit-row' ).attr( 'selected', 'selected' );
var is_variable_product = 'variable' === product_type;
$( 'select[name="_stock_status"] ~ .wc-quick-edit-warning', '.inline-edit-row' ).toggle( is_variable_product );
$( 'select[name="_stock_status"] option[value="' + (is_variable_product ? '' : stock_status) + '"]', '.inline-edit-row' )
.attr( 'selected', 'selected' );
if ( 'yes' === featured ) {
$( 'input[name="_featured"]', '.inline-edit-row' ).attr( 'checked', 'checked' );
} else {
$( 'input[name="_featured"]', '.inline-edit-row' ).removeAttr( 'checked' );
}
$( 'select[name="_tax_status"] option[value="' + tax_status + '"]', '.inline-edit-row' ).attr( 'selected', 'selected' );
$( 'select[name="_tax_class"] option[value="' + tax_class + '"]', '.inline-edit-row' ).attr( 'selected', 'selected' );
$( 'select[name="_visibility"] option[value="' + visibility + '"]', '.inline-edit-row' ).attr( 'selected', 'selected' );
$( 'select[name="_backorders"] option[value="' + backorders + '"]', '.inline-edit-row' ).attr( 'selected', 'selected' );
// Conditional display
var product_type = $wc_inline_data.find( '.product_type' ).text(),
product_is_virtual = $wc_inline_data.find( '.product_is_virtual' ).text();
if ( 'yes' === featured ) {
$( 'input[name="_featured"]', '.inline-edit-row' ).attr( 'checked', 'checked' );
} else {
$( 'input[name="_featured"]', '.inline-edit-row' ).removeAttr( 'checked' );
}
var product_supports_stock_status = 'external' !== product_type;
var product_supports_stock_fields = 'external' !== product_type && 'grouped' !== product_type;
// Conditional display.
var product_is_virtual = $wc_inline_data.find( '.product_is_virtual' ).text();
$( '.stock_fields, .manage_stock_field, .stock_status_field, .backorder_field' ).show();
var product_supports_stock_status = 'external' !== product_type;
var product_supports_stock_fields = 'external' !== product_type && 'grouped' !== product_type;
if ( product_supports_stock_fields ) {
if ( 'yes' === manage_stock ) {
$( '.stock_qty_field, .backorder_field', '.inline-edit-row' ).show().removeAttr( 'style' );
$( '.stock_status_field' ).hide();
$( '.manage_stock_field input' ).prop( 'checked', true );
} else {
$( '.stock_qty_field, .backorder_field', '.inline-edit-row' ).hide();
$( '.stock_status_field' ).show().removeAttr( 'style' );
$( '.manage_stock_field input' ).prop( 'checked', false );
$( '.stock_fields, .manage_stock_field, .stock_status_field, .backorder_field' ).show();
if ( product_supports_stock_fields ) {
if ( 'yes' === manage_stock ) {
$( '.stock_qty_field, .backorder_field', '.inline-edit-row' ).show().removeAttr( 'style' );
$( '.stock_status_field' ).hide();
$( '.manage_stock_field input' ).prop( 'checked', true );
} else {
$( '.stock_qty_field, .backorder_field', '.inline-edit-row' ).hide();
$( '.stock_status_field' ).show().removeAttr( 'style' );
$( '.manage_stock_field input' ).prop( 'checked', false );
}
} else if ( product_supports_stock_status ) {
$( '.stock_fields, .manage_stock_field, .backorder_field' ).hide();
} else {
$( '.stock_fields, .manage_stock_field, .stock_status_field, .backorder_field' ).hide();
}
if ( 'simple' === product_type || 'external' === product_type ) {
$( '.price_fields', '.inline-edit-row' ).show().removeAttr( 'style' );
} else {
$( '.price_fields', '.inline-edit-row' ).hide();
}
if ( 'yes' === product_is_virtual ) {
$( '.dimension_fields', '.inline-edit-row' ).hide();
} else {
$( '.dimension_fields', '.inline-edit-row' ).show().removeAttr( 'style' );
}
// Rename core strings.
$( 'input[name="comment_status"]' ).parent().find( '.checkbox-title' ).text( woocommerce_quick_edit.strings.allow_reviews );
}
} else if ( product_supports_stock_status ) {
$( '.stock_fields, .manage_stock_field, .backorder_field' ).hide();
} else {
$( '.stock_fields, .manage_stock_field, .stock_status_field, .backorder_field' ).hide();
}
);
if ( 'simple' === product_type || 'external' === product_type ) {
$( '.price_fields', '.inline-edit-row' ).show().removeAttr( 'style' );
} else {
$( '.price_fields', '.inline-edit-row' ).hide();
}
$( '#the-list' ).on(
'change',
'.inline-edit-row input[name="_manage_stock"]',
function() {
if ( 'yes' === product_is_virtual ) {
$( '.dimension_fields', '.inline-edit-row' ).hide();
} else {
$( '.dimension_fields', '.inline-edit-row' ).show().removeAttr( 'style' );
}
if ( $( this ).is( ':checked' ) ) {
$( '.stock_qty_field, .backorder_field', '.inline-edit-row' ).show().removeAttr( 'style' );
$( '.stock_status_field' ).hide();
} else {
$( '.stock_qty_field, .backorder_field', '.inline-edit-row' ).hide();
$( '.stock_status_field' ).show().removeAttr( 'style' );
}
// Rename core strings
$( 'input[name="comment_status"]' ).parent().find( '.checkbox-title' ).text( woocommerce_quick_edit.strings.allow_reviews );
});
}
);
$( '#the-list' ).on( 'change', '.inline-edit-row input[name="_manage_stock"]', function() {
$( '#wpbody' ).on(
'click',
'#doaction, #doaction2',
function() {
$( 'input.text', '.inline-edit-row' ).val( '' );
$( '#woocommerce-fields' ).find( 'select' ).prop( 'selectedIndex', 0 );
$( '#woocommerce-fields-bulk' ).find( '.inline-edit-group .change-input' ).hide();
}
);
if ( $( this ).is( ':checked' ) ) {
$( '.stock_qty_field, .backorder_field', '.inline-edit-row' ).show().removeAttr( 'style' );
$( '.stock_status_field' ).hide();
} else {
$( '.stock_qty_field, .backorder_field', '.inline-edit-row' ).hide();
$( '.stock_status_field' ).show().removeAttr( 'style' );
}
$( '#wpbody' ).on(
'change',
'#woocommerce-fields-bulk .inline-edit-group .change_to',
function() {
});
if ( 0 < $( this ).val() ) {
$( this ).closest( 'div' ).find( '.change-input' ).show();
} else {
$( this ).closest( 'div' ).find( '.change-input' ).hide();
}
$( '#wpbody' ).on( 'click', '#doaction, #doaction2', function() {
$( 'input.text', '.inline-edit-row' ).val( '' );
$( '#woocommerce-fields' ).find( 'select' ).prop( 'selectedIndex', 0 );
$( '#woocommerce-fields-bulk' ).find( '.inline-edit-group .change-input' ).hide();
});
}
);
$( '#wpbody' ).on( 'change', '#woocommerce-fields-bulk .inline-edit-group .change_to', function() {
if ( 0 < $( this ).val() ) {
$( this ).closest( 'div' ).find( '.change-input' ).show();
} else {
$( this ).closest( 'div' ).find( '.change-input' ).hide();
}
});
$( '#wpbody' ).on( 'click', '.trash-product', function() {
return window.confirm( woocommerce_admin.i18n_delete_product_notice );
});
});
$( '#wpbody' ).on(
'click',
'.trash-product',
function() {
return window.confirm( woocommerce_admin.i18n_delete_product_notice );
}
);
}
);

View File

@ -273,7 +273,9 @@ jQuery( function( $ ) {
var large_image_src = img.attr( 'data-large_image' ),
large_image_w = img.attr( 'data-large_image_width' ),
large_image_h = img.attr( 'data-large_image_height' ),
alt = img.attr( 'alt' ),
item = {
alt : alt,
src : large_image_src,
w : large_image_w,
h : large_image_h,

View File

@ -2861,6 +2861,7 @@ var _getItemAt,
};
img.src = item.src;// + '?a=' + Math.random();
img.alt = item.alt || '';
return img;
},

File diff suppressed because one or more lines are too long

View File

@ -100,6 +100,7 @@
var srcElement = source.querySelector('img');
if (srcElement) {
settings.url = srcElement.getAttribute('data-src') || srcElement.currentSrc || srcElement.src;
settings.alt = srcElement.getAttribute('data-alt') || srcElement.alt;
}
if (!settings.url) {
return;
@ -227,7 +228,7 @@
};
img.setAttribute('role', 'presentation');
img.alt = '';
img.alt = settings.alt || '';
img.src = settings.url;
});
};

View File

@ -3,4 +3,4 @@
license: MIT
http://www.jacklmoore.com/zoom
*/
(function(o){var t={url:!1,callback:!1,target:!1,duration:120,on:"mouseover",touch:!0,onZoomIn:!1,onZoomOut:!1,magnify:1};o.zoom=function(t,n,e,i){var u,c,a,r,m,l,s,f=o(t),h=f.css("position"),d=o(n);return t.style.position=/(absolute|fixed)/.test(h)?h:"relative",t.style.overflow="hidden",e.style.width=e.style.height="",o(e).addClass("zoomImg").css({position:"absolute",top:0,left:0,opacity:0,width:e.width*i,height:e.height*i,border:"none",maxWidth:"none",maxHeight:"none"}).appendTo(t),{init:function(){c=f.outerWidth(),u=f.outerHeight(),n===t?(r=c,a=u):(r=d.outerWidth(),a=d.outerHeight()),m=(e.width-c)/r,l=(e.height-u)/a,s=d.offset()},move:function(o){var t=o.pageX-s.left,n=o.pageY-s.top;n=Math.max(Math.min(n,a),0),t=Math.max(Math.min(t,r),0),e.style.left=t*-m+"px",e.style.top=n*-l+"px"}}},o.fn.zoom=function(n){return this.each(function(){var e=o.extend({},t,n||{}),i=e.target&&o(e.target)[0]||this,u=this,c=o(u),a=document.createElement("img"),r=o(a),m="mousemove.zoom",l=!1,s=!1;if(!e.url){var f=u.querySelector("img");if(f&&(e.url=f.getAttribute("data-src")||f.currentSrc||f.src),!e.url)return}c.one("zoom.destroy",function(o,t){c.off(".zoom"),i.style.position=o,i.style.overflow=t,a.onload=null,r.remove()}.bind(this,i.style.position,i.style.overflow)),a.onload=function(){function t(t){f.init(),f.move(t),r.stop().fadeTo(o.support.opacity?e.duration:0,1,o.isFunction(e.onZoomIn)?e.onZoomIn.call(a):!1)}function n(){r.stop().fadeTo(e.duration,0,o.isFunction(e.onZoomOut)?e.onZoomOut.call(a):!1)}var f=o.zoom(i,u,a,e.magnify);"grab"===e.on?c.on("mousedown.zoom",function(e){1===e.which&&(o(document).one("mouseup.zoom",function(){n(),o(document).off(m,f.move)}),t(e),o(document).on(m,f.move),e.preventDefault())}):"click"===e.on?c.on("click.zoom",function(e){return l?void 0:(l=!0,t(e),o(document).on(m,f.move),o(document).one("click.zoom",function(){n(),l=!1,o(document).off(m,f.move)}),!1)}):"toggle"===e.on?c.on("click.zoom",function(o){l?n():t(o),l=!l}):"mouseover"===e.on&&(f.init(),c.on("mouseenter.zoom",t).on("mouseleave.zoom",n).on(m,f.move)),e.touch&&c.on("touchstart.zoom",function(o){o.preventDefault(),s?(s=!1,n()):(s=!0,t(o.originalEvent.touches[0]||o.originalEvent.changedTouches[0]))}).on("touchmove.zoom",function(o){o.preventDefault(),f.move(o.originalEvent.touches[0]||o.originalEvent.changedTouches[0])}).on("touchend.zoom",function(o){o.preventDefault(),s&&(s=!1,n())}),o.isFunction(e.callback)&&e.callback.call(a)},a.setAttribute("role","presentation"),a.alt="",a.src=e.url})},o.fn.zoom.defaults=t})(window.jQuery);
!function(d){var n={url:!1,callback:!1,target:!1,duration:120,on:"mouseover",touch:!0,onZoomIn:!1,onZoomOut:!1,magnify:1};d.zoom=function(o,t,n,e){var i,u,a,c,r,l,m,s=d(o),f=s.css("position"),h=d(t);return o.style.position=/(absolute|fixed)/.test(f)?f:"relative",o.style.overflow="hidden",n.style.width=n.style.height="",d(n).addClass("zoomImg").css({position:"absolute",top:0,left:0,opacity:0,width:n.width*e,height:n.height*e,border:"none",maxWidth:"none",maxHeight:"none"}).appendTo(o),{init:function(){u=s.outerWidth(),i=s.outerHeight(),a=t===o?(c=u,i):(c=h.outerWidth(),h.outerHeight()),r=(n.width-u)/c,l=(n.height-i)/a,m=h.offset()},move:function(o){var t=o.pageX-m.left,e=o.pageY-m.top;e=Math.max(Math.min(e,a),0),t=Math.max(Math.min(t,c),0),n.style.left=t*-r+"px",n.style.top=e*-l+"px"}}},d.fn.zoom=function(e){return this.each(function(){var i=d.extend({},n,e||{}),u=i.target&&d(i.target)[0]||this,o=this,a=d(o),c=document.createElement("img"),r=d(c),l="mousemove.zoom",m=!1,s=!1;if(!i.url){var t=o.querySelector("img");if(t&&(i.url=t.getAttribute("data-src")||t.currentSrc||t.src,i.alt=t.getAttribute("data-alt")||t.alt),!i.url)return}a.one("zoom.destroy",function(o,t){a.off(".zoom"),u.style.position=o,u.style.overflow=t,c.onload=null,r.remove()}.bind(this,u.style.position,u.style.overflow)),c.onload=function(){var t=d.zoom(u,o,c,i.magnify);function e(o){t.init(),t.move(o),r.stop().fadeTo(d.support.opacity?i.duration:0,1,!!d.isFunction(i.onZoomIn)&&i.onZoomIn.call(c))}function n(){r.stop().fadeTo(i.duration,0,!!d.isFunction(i.onZoomOut)&&i.onZoomOut.call(c))}"grab"===i.on?a.on("mousedown.zoom",function(o){1===o.which&&(d(document).one("mouseup.zoom",function(){n(),d(document).off(l,t.move)}),e(o),d(document).on(l,t.move),o.preventDefault())}):"click"===i.on?a.on("click.zoom",function(o){return m?void 0:(m=!0,e(o),d(document).on(l,t.move),d(document).one("click.zoom",function(){n(),m=!1,d(document).off(l,t.move)}),!1)}):"toggle"===i.on?a.on("click.zoom",function(o){m?n():e(o),m=!m}):"mouseover"===i.on&&(t.init(),a.on("mouseenter.zoom",e).on("mouseleave.zoom",n).on(l,t.move)),i.touch&&a.on("touchstart.zoom",function(o){o.preventDefault(),s?(s=!1,n()):(s=!0,e(o.originalEvent.touches[0]||o.originalEvent.changedTouches[0]))}).on("touchmove.zoom",function(o){o.preventDefault(),t.move(o.originalEvent.touches[0]||o.originalEvent.changedTouches[0])}).on("touchend.zoom",function(o){o.preventDefault(),s&&(s=!1,n())}),d.isFunction(i.callback)&&i.callback.call(c)},c.setAttribute("role","presentation"),c.alt=i.alt||"",c.src=i.url})},d.fn.zoom.defaults=n}(window.jQuery);

View File

@ -8,15 +8,16 @@
"minimum-stability": "dev",
"require": {
"php": ">=7.0",
"automattic/jetpack-autoloader": "^1.7.0",
"automattic/jetpack-autoloader": "^2.0.2",
"automattic/jetpack-constants": "^1.1",
"composer/installers": "1.7.0",
"league/container": "^3.3",
"maxmind-db/reader": "1.6.0",
"pelago/emogrifier": "^3.1",
"woocommerce/action-scheduler": "3.1.6",
"woocommerce/woocommerce-admin": "1.3.1",
"woocommerce/woocommerce-blocks": "2.7.1",
"woocommerce/woocommerce-rest-api": "1.0.10"
"woocommerce/woocommerce-admin": "1.4.0-beta.2",
"woocommerce/woocommerce-blocks": "3.0.0",
"woocommerce/woocommerce-rest-api": "1.0.11"
},
"require-dev": {
"phpunit/phpunit": "7.5.20",

206
composer.lock generated
View File

@ -4,24 +4,24 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "313a6737a05bfc27777a05bc22bb7bbe",
"content-hash": "877625af18978cccd2d02780fbd11842",
"packages": [
{
"name": "automattic/jetpack-autoloader",
"version": "v1.7.0",
"version": "v2.0.2",
"source": {
"type": "git",
"url": "https://github.com/Automattic/jetpack-autoloader.git",
"reference": "7c6736eeee0f9fc49fa691fe3e958725efb27ca0"
"reference": "4502da4b2443fc1b61389cacc94c34876aca2b3d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Automattic/jetpack-autoloader/zipball/7c6736eeee0f9fc49fa691fe3e958725efb27ca0",
"reference": "7c6736eeee0f9fc49fa691fe3e958725efb27ca0",
"url": "https://api.github.com/repos/Automattic/jetpack-autoloader/zipball/4502da4b2443fc1b61389cacc94c34876aca2b3d",
"reference": "4502da4b2443fc1b61389cacc94c34876aca2b3d",
"shasum": ""
},
"require": {
"composer-plugin-api": "^1.1"
"composer-plugin-api": "^1.1 || ^2.0"
},
"require-dev": {
"phpunit/phpunit": "^5.7 || ^6.5 || ^7.5"
@ -40,7 +40,7 @@
"GPL-2.0-or-later"
],
"description": "Creates a custom autoloader for a plugin or theme.",
"time": "2020-04-23T02:28:37+00:00"
"time": "2020-07-09T13:18:38+00:00"
},
{
"name": "automattic/jetpack-constants",
@ -195,6 +195,78 @@
],
"time": "2019-08-12T15:00:31+00:00"
},
{
"name": "league/container",
"version": "3.3.1",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/container.git",
"reference": "93238f74ff5964aee27a78508cdfbdba1cd338f6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/container/zipball/93238f74ff5964aee27a78508cdfbdba1cd338f6",
"reference": "93238f74ff5964aee27a78508cdfbdba1cd338f6",
"shasum": ""
},
"require": {
"php": "^7.0",
"psr/container": "^1.0"
},
"provide": {
"psr/container-implementation": "^1.0"
},
"replace": {
"orno/di": "~2.0"
},
"require-dev": {
"phpunit/phpunit": "^6.0",
"squizlabs/php_codesniffer": "^3.3"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-3.x": "3.x-dev",
"dev-2.x": "2.x-dev",
"dev-1.x": "1.x-dev"
}
},
"autoload": {
"psr-4": {
"League\\Container\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Phil Bennett",
"email": "philipobenito@gmail.com",
"homepage": "http://www.philipobenito.com",
"role": "Developer"
}
],
"description": "A fast and intuitive dependency injection container.",
"homepage": "https://github.com/thephpleague/container",
"keywords": [
"container",
"dependency",
"di",
"injection",
"league",
"provider",
"service"
],
"funding": [
{
"url": "https://github.com/philipobenito",
"type": "github"
}
],
"time": "2020-05-18T08:20:23+00:00"
},
{
"name": "maxmind-db/reader",
"version": "v1.6.0",
@ -329,9 +401,58 @@
],
"time": "2019-12-26T19:37:31+00:00"
},
{
"name": "psr/container",
"version": "1.0.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/container.git",
"reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f",
"reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Container\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
}
],
"description": "Common Container Interface (PHP FIG PSR-11)",
"homepage": "https://github.com/php-fig/container",
"keywords": [
"PSR-11",
"container",
"container-interface",
"container-interop",
"psr"
],
"time": "2017-02-14T16:28:37+00:00"
},
{
"name": "symfony/css-selector",
"version": "v3.4.42",
"version": "v3.4.43",
"source": {
"type": "git",
"url": "https://github.com/symfony/css-selector.git",
@ -380,6 +501,20 @@
],
"description": "Symfony CssSelector Component",
"homepage": "https://symfony.com",
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2020-03-16T08:31:04+00:00"
},
{
@ -419,20 +554,20 @@
},
{
"name": "woocommerce/woocommerce-admin",
"version": "v1.3.1",
"version": "v1.4.0-beta.2",
"source": {
"type": "git",
"url": "https://github.com/woocommerce/woocommerce-admin.git",
"reference": "337036202a29078aed827f8e5dec566a9c57929a"
"reference": "d56ac35bbb62bda2c981443932e7f90b0f6dbe99"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/woocommerce/woocommerce-admin/zipball/337036202a29078aed827f8e5dec566a9c57929a",
"reference": "337036202a29078aed827f8e5dec566a9c57929a",
"url": "https://api.github.com/repos/woocommerce/woocommerce-admin/zipball/d56ac35bbb62bda2c981443932e7f90b0f6dbe99",
"reference": "d56ac35bbb62bda2c981443932e7f90b0f6dbe99",
"shasum": ""
},
"require": {
"automattic/jetpack-autoloader": "^1.6.0",
"automattic/jetpack-autoloader": "^2.0.0",
"composer/installers": "1.7.0",
"php": ">=5.6|>=7.0"
},
@ -462,24 +597,24 @@
],
"description": "A modern, javascript-driven WooCommerce Admin experience.",
"homepage": "https://github.com/woocommerce/woocommerce-admin",
"time": "2020-07-20T22:33:15+00:00"
"time": "2020-07-28T00:28:40+00:00"
},
{
"name": "woocommerce/woocommerce-blocks",
"version": "v2.7.1",
"version": "v3.0.0",
"source": {
"type": "git",
"url": "https://github.com/woocommerce/woocommerce-gutenberg-products-block.git",
"reference": "0025c5cda83892c6f566fffd05197006f230d16c"
"reference": "cc00da60f21a7219e7e5ef5599996c68fee7b2be"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/woocommerce/woocommerce-gutenberg-products-block/zipball/0025c5cda83892c6f566fffd05197006f230d16c",
"reference": "0025c5cda83892c6f566fffd05197006f230d16c",
"url": "https://api.github.com/repos/woocommerce/woocommerce-gutenberg-products-block/zipball/cc00da60f21a7219e7e5ef5599996c68fee7b2be",
"reference": "cc00da60f21a7219e7e5ef5599996c68fee7b2be",
"shasum": ""
},
"require": {
"automattic/jetpack-autoloader": "^1.6.0",
"automattic/jetpack-autoloader": "^2.0.0",
"composer/installers": "1.7.0"
},
"require-dev": {
@ -509,24 +644,24 @@
"gutenberg",
"woocommerce"
],
"time": "2020-06-16T13:34:29+00:00"
"time": "2020-07-22T13:34:19+00:00"
},
{
"name": "woocommerce/woocommerce-rest-api",
"version": "1.0.10",
"version": "1.0.11",
"source": {
"type": "git",
"url": "https://github.com/woocommerce/woocommerce-rest-api.git",
"reference": "fdcb116b4f5b699b942c01b46fd863c7da8b4b7c"
"reference": "304bb95cb4b95f182f09d56153d5ac254d5fe60a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/woocommerce/woocommerce-rest-api/zipball/fdcb116b4f5b699b942c01b46fd863c7da8b4b7c",
"reference": "fdcb116b4f5b699b942c01b46fd863c7da8b4b7c",
"url": "https://api.github.com/repos/woocommerce/woocommerce-rest-api/zipball/304bb95cb4b95f182f09d56153d5ac254d5fe60a",
"reference": "304bb95cb4b95f182f09d56153d5ac254d5fe60a",
"shasum": ""
},
"require": {
"automattic/jetpack-autoloader": "^1.2.0"
"automattic/jetpack-autoloader": "^2.0.0"
},
"require-dev": {
"phpunit/phpunit": "6.5.14",
@ -549,7 +684,7 @@
],
"description": "The WooCommerce core REST API.",
"homepage": "https://github.com/woocommerce/woocommerce-rest-api",
"time": "2020-06-16T09:51:51+00:00"
"time": "2020-07-24T13:38:16+00:00"
}
],
"packages-dev": [
@ -2414,7 +2549,7 @@
},
{
"name": "symfony/finder",
"version": "v3.4.42",
"version": "v3.4.43",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
@ -2459,6 +2594,20 @@
],
"description": "Symfony Finder Component",
"homepage": "https://symfony.com",
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2020-02-14T07:34:21+00:00"
},
{
@ -2926,5 +3075,6 @@
"platform-dev": [],
"platform-overrides": {
"php": "7.1"
}
},
"plugin-api-version": "1.1.0"
}

34
includes/README.md Normal file
View File

@ -0,0 +1,34 @@
# WooCommerce `includes` files
This directory contains WooCommerce legacy code. Ideally, the code in this folder should only get the minimum required changes for bug fixing, and any new code should go in [the `src` directory](https://github.com/woocommerce/woocommerce/tree/master/src/README.md) instead.
## Interacting with the `src` folder
Whenever you need to get an instance of a class from the `src` directory, please don't instantiate it directly, but instead use [the container](https://github.com/woocommerce/woocommerce/tree/master/src/README.md#the-container). To get an instance of the container itself you can use the `wc_get_container` function, for example:
```php
$container = wc_get_container();
$service = $container->get( \Automattic\WooCommerce\TheNamespace\TheService::class );
$service->do_something();
```
The exception to this rule might be data-only classes that could be created the old way (using a plain `new` statement); but in general, all classes in the `src` directory are registered in the container and should be resolved using it.
## Adding new actions and filters
Please take a look at [the considerations for creation new hooks in `src` code](https://github.com/woocommerce/woocommerce/tree/master/src/README.md#defining-new-actions-and-filters), as they apply for `includes` code as well. The short version is that **new hooks should be introduced only if they provide a valuable extension point for plugins**, and not with the purpose of driving WooCommerce's internal logic.
## Writing unit tests
[As it's the case for the `src` folder](https://github.com/woocommerce/woocommerce/tree/master/src/README.md#writing-unit-tests), writing unit tests is generally mandatory if you are a WooCommerce team member or a contributor from another Automattic team, and encouraged if you are an external contributor. Tests should cover any new code (although as mentioned, adding new code in `includes` should be rare) and any modifications to existing code.
In order to make it easier to write unit tests, there are a couple of mechanisms in place that you can use:
* [The code hacker](https://github.com/woocommerce/woocommerce/blob/master/tests/Tools/CodeHacking/README.md). Pros: you don't need to do any special changes to your code to make it testable. Cons: it's a hack, the tested code is being actually modified while being loaded by the PHP engine, so not an ideal solution.
* [The legacy proxy and the related helper methods in WC_Unit_Test_case](https://github.com/woocommerce/woocommerce/tree/master/src/README.md#interacting-with-legacy-code): although these are intended in principle for writing tests for code in the `src` directory, they can be used for `includes` code as well. Pros: a clean approach, no hacks involved. Cons: you need to modify your code to use the proxy whenever you need to call a function or static method that makes the code difficult to test.
It's up to you as a contributor to decide which mechanism to use in each case. Choose wisely.

View File

@ -462,7 +462,7 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
$tax_totals[ $code ]->is_compound = $tax->is_compound();
$tax_totals[ $code ]->label = $tax->get_label();
$tax_totals[ $code ]->amount += (float) $tax->get_tax_total() + (float) $tax->get_shipping_tax_total();
$tax_totals[ $code ]->formatted_amount = wc_price( wc_round_tax_total( $tax_totals[ $code ]->amount ), array( 'currency' => $this->get_currency() ) );
$tax_totals[ $code ]->formatted_amount = wc_price( $tax_totals[ $code ]->amount, array( 'currency' => $this->get_currency() ) );
}
if ( apply_filters( 'woocommerce_order_hide_zero_taxes', true ) ) {
@ -672,7 +672,7 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
*/
protected function set_total_tax( $value ) {
// We round here because this is a total entry, as opposed to line items in other setters.
$this->set_prop( 'total_tax', wc_format_decimal( wc_round_tax_total( $value ) ) );
$this->set_prop( 'total_tax', wc_format_decimal( round( $value, wc_get_price_decimals() ) ) );
}
/**

View File

@ -1501,6 +1501,16 @@ class WC_Product extends WC_Abstract_Legacy_Product {
* @return bool
*/
public function is_visible() {
$visible = $this->is_visible_core();
return apply_filters( 'woocommerce_product_is_visible', $visible, $this->get_id() );
}
/**
* Returns whether or not the product is visible in the catalog (doesn't trigger filters).
*
* @return bool
*/
protected function is_visible_core() {
$visible = 'visible' === $this->get_catalog_visibility() || ( is_search() && 'search' === $this->get_catalog_visibility() ) || ( ! is_search() && 'catalog' === $this->get_catalog_visibility() );
if ( 'trash' === $this->get_status() ) {
@ -1521,7 +1531,7 @@ class WC_Product extends WC_Abstract_Legacy_Product {
$visible = false;
}
return apply_filters( 'woocommerce_product_is_visible', $visible, $this->get_id() );
return $visible;
}
/**

View File

@ -79,6 +79,8 @@ class WC_Admin_Post_Types {
public function setup_screen() {
global $wc_list_table;
$request_data = $this->request_data();
$screen_id = false;
if ( function_exists( 'get_current_screen' ) ) {
@ -86,8 +88,8 @@ class WC_Admin_Post_Types {
$screen_id = isset( $screen, $screen->id ) ? $screen->id : '';
}
if ( ! empty( $_REQUEST['screen'] ) ) { // WPCS: input var ok.
$screen_id = wc_clean( wp_unslash( $_REQUEST['screen'] ) ); // WPCS: input var ok, sanitization ok.
if ( ! empty( $request_data['screen'] ) ) {
$screen_id = wc_clean( wp_unslash( $request_data['screen'] ) );
}
switch ( $screen_id ) {
@ -296,6 +298,8 @@ class WC_Admin_Post_Types {
* @return int
*/
public function bulk_and_quick_edit_save_post( $post_id, $post ) {
$request_data = $this->request_data();
// If this is an autosave, our form has not been submitted, so we don't want to do anything.
if ( Constants::is_true( 'DOING_AUTOSAVE' ) ) {
return $post_id;
@ -307,14 +311,15 @@ class WC_Admin_Post_Types {
}
// Check nonce.
if ( ! isset( $_REQUEST['woocommerce_quick_edit_nonce'] ) || ! wp_verify_nonce( $_REQUEST['woocommerce_quick_edit_nonce'], 'woocommerce_quick_edit_nonce' ) ) { // WPCS: input var ok, sanitization ok.
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
if ( ! isset( $request_data['woocommerce_quick_edit_nonce'] ) || ! wp_verify_nonce( $request_data['woocommerce_quick_edit_nonce'], 'woocommerce_quick_edit_nonce' ) ) {
return $post_id;
}
// Get the product and save.
$product = wc_get_product( $post );
if ( ! empty( $_REQUEST['woocommerce_quick_edit'] ) ) { // WPCS: input var ok.
if ( ! empty( $request_data['woocommerce_quick_edit'] ) ) { // WPCS: input var ok.
$this->quick_edit_save( $post_id, $product );
} else {
$this->bulk_edit_save( $post_id, $product );
@ -330,6 +335,8 @@ class WC_Admin_Post_Types {
* @param WC_Product $product Product object.
*/
private function quick_edit_save( $post_id, $product ) {
$request_data = $this->request_data();
$data_store = $product->get_data_store();
$old_regular_price = $product->get_regular_price();
$old_sale_price = $product->get_sale_price();
@ -344,14 +351,15 @@ class WC_Admin_Post_Types {
);
foreach ( $input_to_props as $input_var => $prop ) {
if ( isset( $_REQUEST[ $input_var ] ) ) { // WPCS: input var ok, sanitization ok.
$product->{"set_{$prop}"}( wc_clean( wp_unslash( $_REQUEST[ $input_var ] ) ) ); // WPCS: input var ok, sanitization ok.
if ( isset( $request_data[ $input_var ] ) ) {
$product->{"set_{$prop}"}( wc_clean( wp_unslash( $request_data[ $input_var ] ) ) );
}
}
if ( isset( $_REQUEST['_sku'] ) ) { // WPCS: input var ok, sanitization ok.
$sku = $product->get_sku();
$new_sku = (string) wc_clean( $_REQUEST['_sku'] ); // WPCS: input var ok, sanitization ok.
if ( isset( $request_data['_sku'] ) ) {
$sku = $product->get_sku();
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
$new_sku = (string) wc_clean( $request_data['_sku'] );
if ( $new_sku !== $sku ) {
if ( ! empty( $new_sku ) ) {
@ -365,27 +373,30 @@ class WC_Admin_Post_Types {
}
}
if ( ! empty( $_REQUEST['_shipping_class'] ) ) { // WPCS: input var ok, sanitization ok.
if ( '_no_shipping_class' === $_REQUEST['_shipping_class'] ) { // WPCS: input var ok, sanitization ok.
if ( ! empty( $request_data['_shipping_class'] ) ) {
if ( '_no_shipping_class' === $request_data['_shipping_class'] ) {
$product->set_shipping_class_id( 0 );
} else {
$shipping_class_id = $data_store->get_shipping_class_id_by_slug( wc_clean( $_REQUEST['_shipping_class'] ) ); // WPCS: input var ok, sanitization ok.
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
$shipping_class_id = $data_store->get_shipping_class_id_by_slug( wc_clean( $request_data['_shipping_class'] ) );
$product->set_shipping_class_id( $shipping_class_id );
}
}
$product->set_featured( isset( $_REQUEST['_featured'] ) ); // WPCS: input var ok, sanitization ok.
$product->set_featured( isset( $request_data['_featured'] ) );
if ( $product->is_type( 'simple' ) || $product->is_type( 'external' ) ) {
if ( isset( $_REQUEST['_regular_price'] ) ) { // WPCS: input var ok, sanitization ok.
$new_regular_price = ( '' === $_REQUEST['_regular_price'] ) ? '' : wc_format_decimal( $_REQUEST['_regular_price'] ); // WPCS: input var ok, sanitization ok.
if ( isset( $request_data['_regular_price'] ) ) {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
$new_regular_price = ( '' === $request_data['_regular_price'] ) ? '' : wc_format_decimal( $request_data['_regular_price'] );
$product->set_regular_price( $new_regular_price );
} else {
$new_regular_price = null;
}
if ( isset( $_REQUEST['_sale_price'] ) ) { // WPCS: input var ok, sanitization ok.
$new_sale_price = ( '' === $_REQUEST['_sale_price'] ) ? '' : wc_format_decimal( $_REQUEST['_sale_price'] ); // WPCS: input var ok, sanitization ok.
if ( isset( $request_data['_sale_price'] ) ) {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
$new_sale_price = ( '' === $request_data['_sale_price'] ) ? '' : wc_format_decimal( $request_data['_sale_price'] );
$product->set_sale_price( $new_sale_price );
} else {
$new_sale_price = null;
@ -407,35 +418,25 @@ class WC_Admin_Post_Types {
}
// Handle Stock Data.
$manage_stock = ! empty( $_REQUEST['_manage_stock'] ) && 'grouped' !== $product->get_type() ? 'yes' : 'no'; // WPCS: input var ok, sanitization ok.
$backorders = ! empty( $_REQUEST['_backorders'] ) ? wc_clean( $_REQUEST['_backorders'] ) : 'no'; // WPCS: input var ok, sanitization ok.
$stock_status = ! empty( $_REQUEST['_stock_status'] ) ? wc_clean( $_REQUEST['_stock_status'] ) : 'instock'; // WPCS: input var ok, sanitization ok.
$stock_amount = 'yes' === $manage_stock && isset( $_REQUEST['_stock'] ) && is_numeric( wp_unslash( $_REQUEST['_stock'] ) ) ? wc_stock_amount( wp_unslash( $_REQUEST['_stock'] ) ) : ''; // WPCS: input var ok, sanitization ok.
// phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$manage_stock = ! empty( $request_data['_manage_stock'] ) && 'grouped' !== $product->get_type() ? 'yes' : 'no';
$backorders = ! empty( $request_data['_backorders'] ) ? wc_clean( $request_data['_backorders'] ) : 'no';
if ( ! empty( $request_data['_stock_status'] ) ) {
$stock_status = wc_clean( $request_data['_stock_status'] );
} else {
$stock_status = $product->is_type( 'variable' ) ? null : 'instock';
}
// phpcs:enable WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$product->set_manage_stock( $manage_stock );
$product->set_backorders( $backorders );
if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) {
$stock_amount = 'yes' === $manage_stock && isset( $request_data['_stock'] ) && is_numeric( wp_unslash( $request_data['_stock'] ) ) ? wc_stock_amount( wp_unslash( $request_data['_stock'] ) ) : '';
$product->set_stock_quantity( $stock_amount );
}
// Apply product type constraints to stock status.
if ( $product->is_type( 'external' ) ) {
// External products are always in stock.
$product->set_stock_status( 'instock' );
} elseif ( $product->is_type( 'variable' ) && ! $product->get_manage_stock() ) {
// Stock status is determined by children.
foreach ( $product->get_children() as $child_id ) {
$child = wc_get_product( $child_id );
if ( ! $product->get_manage_stock() ) {
$child->set_stock_status( $stock_status );
$child->save();
}
}
$product = WC_Product_Variable::sync( $product, false );
} else {
$product->set_stock_status( $stock_status );
}
$product = $this->maybe_update_stock_status( $product, $stock_status );
$product->save();
@ -449,58 +450,61 @@ class WC_Admin_Post_Types {
* @param WC_Product $product Product object.
*/
public function bulk_edit_save( $post_id, $product ) {
$data_store = $product->get_data_store();
$old_regular_price = $product->get_regular_price();
$old_sale_price = $product->get_sale_price();
$data = wp_unslash( $_REQUEST ); // WPCS: input var ok, CSRF ok.
// phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash
if ( ! empty( $_REQUEST['change_weight'] ) && isset( $_REQUEST['_weight'] ) ) { // WPCS: input var ok, sanitization ok.
$product->set_weight( wc_clean( wp_unslash( $_REQUEST['_weight'] ) ) ); // WPCS: input var ok, sanitization ok.
$request_data = $this->request_data();
$data_store = $product->get_data_store();
if ( ! empty( $request_data['change_weight'] ) && isset( $request_data['_weight'] ) ) {
$product->set_weight( wc_clean( wp_unslash( $request_data['_weight'] ) ) );
}
if ( ! empty( $_REQUEST['change_dimensions'] ) ) { // WPCS: input var ok, sanitization ok.
if ( isset( $_REQUEST['_length'] ) ) { // WPCS: input var ok, sanitization ok.
$product->set_length( wc_clean( wp_unslash( $_REQUEST['_length'] ) ) ); // WPCS: input var ok, sanitization ok.
if ( ! empty( $request_data['change_dimensions'] ) ) {
if ( isset( $request_data['_length'] ) ) {
$product->set_length( wc_clean( wp_unslash( $request_data['_length'] ) ) );
}
if ( isset( $_REQUEST['_width'] ) ) { // WPCS: input var ok, sanitization ok.
$product->set_width( wc_clean( wp_unslash( $_REQUEST['_width'] ) ) ); // WPCS: input var ok, sanitization ok.
if ( isset( $request_data['_width'] ) ) {
$product->set_width( wc_clean( wp_unslash( $request_data['_width'] ) ) );
}
if ( isset( $_REQUEST['_height'] ) ) { // WPCS: input var ok, sanitization ok.
$product->set_height( wc_clean( wp_unslash( $_REQUEST['_height'] ) ) ); // WPCS: input var ok, sanitization ok.
if ( isset( $request_data['_height'] ) ) {
$product->set_height( wc_clean( wp_unslash( $request_data['_height'] ) ) );
}
}
if ( ! empty( $_REQUEST['_tax_status'] ) ) { // WPCS: input var ok, sanitization ok.
$product->set_tax_status( wc_clean( $_REQUEST['_tax_status'] ) ); // WPCS: input var ok, sanitization ok.
if ( ! empty( $request_data['_tax_status'] ) ) {
$product->set_tax_status( wc_clean( $request_data['_tax_status'] ) );
}
if ( ! empty( $_REQUEST['_tax_class'] ) ) { // WPCS: input var ok, sanitization ok.
$tax_class = wc_clean( wp_unslash( $_REQUEST['_tax_class'] ) ); // WPCS: input var ok, sanitization ok.
if ( ! empty( $request_data['_tax_class'] ) ) {
$tax_class = wc_clean( wp_unslash( $request_data['_tax_class'] ) );
if ( 'standard' === $tax_class ) {
$tax_class = '';
}
$product->set_tax_class( $tax_class );
}
if ( ! empty( $_REQUEST['_shipping_class'] ) ) { // WPCS: input var ok, sanitization ok.
if ( '_no_shipping_class' === $_REQUEST['_shipping_class'] ) { // WPCS: input var ok, sanitization ok.
if ( ! empty( $request_data['_shipping_class'] ) ) {
if ( '_no_shipping_class' === $request_data['_shipping_class'] ) {
$product->set_shipping_class_id( 0 );
} else {
$shipping_class_id = $data_store->get_shipping_class_id_by_slug( wc_clean( $_REQUEST['_shipping_class'] ) ); // WPCS: input var ok, sanitization ok.
$shipping_class_id = $data_store->get_shipping_class_id_by_slug( wc_clean( $request_data['_shipping_class'] ) );
$product->set_shipping_class_id( $shipping_class_id );
}
}
if ( ! empty( $_REQUEST['_visibility'] ) ) { // WPCS: input var ok, sanitization ok.
$product->set_catalog_visibility( wc_clean( $_REQUEST['_visibility'] ) ); // WPCS: input var ok, sanitization ok.
if ( ! empty( $request_data['_visibility'] ) ) {
$product->set_catalog_visibility( wc_clean( $request_data['_visibility'] ) );
}
if ( ! empty( $_REQUEST['_featured'] ) ) { // WPCS: input var ok, sanitization ok.
$product->set_featured( wp_unslash( $_REQUEST['_featured'] ) ); // WPCS: input var ok, sanitization ok.
if ( ! empty( $request_data['_featured'] ) ) {
// phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$product->set_featured( wp_unslash( $request_data['_featured'] ) );
// phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
}
if ( ! empty( $_REQUEST['_sold_individually'] ) ) { // WPCS: input var ok, sanitization ok.
if ( 'yes' === $_REQUEST['_sold_individually'] ) { // WPCS: input var ok, sanitization ok.
if ( ! empty( $request_data['_sold_individually'] ) ) {
if ( 'yes' === $request_data['_sold_individually'] ) {
$product->set_sold_individually( 'yes' );
} else {
$product->set_sold_individually( '' );
@ -518,93 +522,10 @@ class WC_Admin_Post_Types {
}
if ( $can_product_type_change_price ) {
$price_changed = false;
$regular_price_changed = $this->set_new_price( $product, 'regular' );
$sale_price_changed = $this->set_new_price( $product, 'sale' );
if ( ! empty( $_REQUEST['change_regular_price'] ) && isset( $_REQUEST['_regular_price'] ) ) { // WPCS: input var ok, sanitization ok.
$change_regular_price = absint( $_REQUEST['change_regular_price'] ); // WPCS: input var ok, sanitization ok.
$raw_regular_price = wc_clean( wp_unslash( $_REQUEST['_regular_price'] ) ); // WPCS: input var ok, sanitization ok.
$is_percentage = (bool) strstr( $raw_regular_price, '%' );
$regular_price = wc_format_decimal( $raw_regular_price );
switch ( $change_regular_price ) {
case 1:
$new_price = $regular_price;
break;
case 2:
if ( $is_percentage ) {
$percent = $regular_price / 100;
$new_price = $old_regular_price + ( round( $old_regular_price * $percent, wc_get_price_decimals() ) );
} else {
$new_price = $old_regular_price + $regular_price;
}
break;
case 3:
if ( $is_percentage ) {
$percent = $regular_price / 100;
$new_price = max( 0, $old_regular_price - ( round( $old_regular_price * $percent, wc_get_price_decimals() ) ) );
} else {
$new_price = max( 0, $old_regular_price - $regular_price );
}
break;
default:
break;
}
if ( isset( $new_price ) && $new_price !== $old_regular_price ) {
$price_changed = true;
$new_price = round( $new_price, wc_get_price_decimals() );
$product->set_regular_price( $new_price );
}
}
if ( ! empty( $_REQUEST['change_sale_price'] ) && isset( $_REQUEST['_sale_price'] ) ) { // WPCS: input var ok, sanitization ok.
$change_sale_price = absint( $_REQUEST['change_sale_price'] ); // WPCS: input var ok, sanitization ok.
$raw_sale_price = wc_clean( wp_unslash( $_REQUEST['_sale_price'] ) ); // WPCS: input var ok, sanitization ok.
$is_percentage = (bool) strstr( $raw_sale_price, '%' );
$sale_price = wc_format_decimal( $raw_sale_price );
switch ( $change_sale_price ) {
case 1:
$new_price = $sale_price;
break;
case 2:
if ( $is_percentage ) {
$percent = $sale_price / 100;
$new_price = $old_sale_price + ( $old_sale_price * $percent );
} else {
$new_price = $old_sale_price + $sale_price;
}
break;
case 3:
if ( $is_percentage ) {
$percent = $sale_price / 100;
$new_price = max( 0, $old_sale_price - ( $old_sale_price * $percent ) );
} else {
$new_price = max( 0, $old_sale_price - $sale_price );
}
break;
case 4:
if ( $is_percentage ) {
$percent = $sale_price / 100;
$new_price = max( 0, $product->regular_price - ( $product->regular_price * $percent ) );
} else {
$new_price = max( 0, $product->regular_price - $sale_price );
}
break;
default:
break;
}
if ( isset( $new_price ) && $new_price !== $old_sale_price ) {
$price_changed = true;
$new_price = ! empty( $new_price ) || '0' === $new_price ? round( $new_price, wc_get_price_decimals() ) : '';
$product->set_sale_price( $new_price );
}
}
if ( $price_changed ) {
if ( $regular_price_changed || $sale_price_changed ) {
$product->set_date_on_sale_to( '' );
$product->set_date_on_sale_from( '' );
@ -616,24 +537,22 @@ class WC_Admin_Post_Types {
// Handle Stock Data.
$was_managing_stock = $product->get_manage_stock() ? 'yes' : 'no';
$stock_status = $product->get_stock_status();
$backorders = $product->get_backorders();
$backorders = ! empty( $_REQUEST['_backorders'] ) ? wc_clean( $_REQUEST['_backorders'] ) : $backorders; // WPCS: input var ok, sanitization ok.
$stock_status = ! empty( $_REQUEST['_stock_status'] ) ? wc_clean( $_REQUEST['_stock_status'] ) : $stock_status; // WPCS: input var ok, sanitization ok.
$backorders = ! empty( $request_data['_backorders'] ) ? wc_clean( $request_data['_backorders'] ) : $backorders;
if ( ! empty( $_REQUEST['_manage_stock'] ) ) { // WPCS: input var ok, sanitization ok.
$manage_stock = 'yes' === wc_clean( $_REQUEST['_manage_stock'] ) && 'grouped' !== $product->get_type() ? 'yes' : 'no'; // WPCS: input var ok, sanitization ok.
if ( ! empty( $request_data['_manage_stock'] ) ) {
$manage_stock = 'yes' === wc_clean( $request_data['_manage_stock'] ) && 'grouped' !== $product->get_type() ? 'yes' : 'no';
} else {
$manage_stock = $was_managing_stock;
}
$stock_amount = 'yes' === $manage_stock && ! empty( $_REQUEST['change_stock'] ) && isset( $_REQUEST['_stock'] ) ? wc_stock_amount( $_REQUEST['_stock'] ) : $product->get_stock_quantity(); // WPCS: input var ok, sanitization ok.
$stock_amount = 'yes' === $manage_stock && ! empty( $request_data['change_stock'] ) && isset( $request_data['_stock'] ) ? wc_stock_amount( $request_data['_stock'] ) : $product->get_stock_quantity();
$product->set_manage_stock( $manage_stock );
$product->set_backorders( $backorders );
if ( 'yes' === get_option( 'woocommerce_manage_stock' ) ) {
$change_stock = absint( $_REQUEST['change_stock'] );
$change_stock = absint( $request_data['change_stock'] );
switch ( $change_stock ) {
case 2:
wc_update_product_stock( $product, $stock_amount, 'increase', true );
@ -651,27 +570,14 @@ class WC_Admin_Post_Types {
$product->set_manage_stock( 'no' );
}
// Apply product type constraints to stock status.
if ( $product->is_type( 'external' ) ) {
// External products are always in stock.
$product->set_stock_status( 'instock' );
} elseif ( $product->is_type( 'variable' ) && ! $product->get_manage_stock() ) {
// Stock status is determined by children.
foreach ( $product->get_children() as $child_id ) {
$child = wc_get_product( $child_id );
if ( ! $product->get_manage_stock() ) {
$child->set_stock_status( $stock_status );
$child->save();
}
}
$product = WC_Product_Variable::sync( $product, false );
} else {
$product->set_stock_status( $stock_status );
}
$stock_status = empty( $request_data['_stock_status'] ) ? null : wc_clean( $request_data['_stock_status'] );
$product = $this->maybe_update_stock_status( $product, $stock_status );
$product->save();
do_action( 'woocommerce_product_bulk_edit_save', $product );
// phpcs:enable WordPress.Security.ValidatedSanitizedInput.MissingUnslash
}
/**
@ -719,11 +625,13 @@ class WC_Admin_Post_Types {
* @param WP_Post $post Current post object.
*/
public function edit_form_after_title( $post ) {
// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
if ( 'shop_coupon' === $post->post_type ) {
?>
<textarea id="woocommerce-coupon-description" name="excerpt" cols="5" rows="2" placeholder="<?php esc_attr_e( 'Description (optional)', 'woocommerce' ); ?>"><?php echo $post->post_excerpt; // WPCS: XSS ok. ?></textarea>
<textarea id="woocommerce-coupon-description" name="excerpt" cols="5" rows="2" placeholder="<?php esc_attr_e( 'Description (optional)', 'woocommerce' ); ?>"><?php echo $post->post_excerpt; ?></textarea>
<?php
}
// phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped
}
/**
@ -802,7 +710,8 @@ class WC_Admin_Post_Types {
* @return array
*/
public function upload_dir( $pathdata ) {
if ( isset( $_POST['type'] ) && 'downloadable_product' === $_POST['type'] ) { // WPCS: CSRF ok, input var ok.
// phpcs:disable WordPress.Security.NonceVerification.Missing
if ( isset( $_POST['type'] ) && 'downloadable_product' === $_POST['type'] ) {
if ( empty( $pathdata['subdir'] ) ) {
$pathdata['path'] = $pathdata['path'] . '/woocommerce_uploads';
@ -817,6 +726,7 @@ class WC_Admin_Post_Types {
}
}
return $pathdata;
// phpcs:enable WordPress.Security.NonceVerification.Missing
}
/**
@ -830,7 +740,8 @@ class WC_Admin_Post_Types {
* @since 4.0
*/
public function update_filename( $full_filename, $ext, $dir ) {
if ( ! isset( $_POST['type'] ) || ! 'downloadable_product' === $_POST['type'] ) { // WPCS: CSRF ok, input var ok.
// phpcs:disable WordPress.Security.NonceVerification.Missing
if ( ! isset( $_POST['type'] ) || ! 'downloadable_product' === $_POST['type'] ) {
return $full_filename;
}
@ -843,6 +754,7 @@ class WC_Admin_Post_Types {
}
return $this->unique_filename( $full_filename, $ext );
// phpcs:enable WordPress.Security.NonceVerification.Missing
}
/**
@ -862,11 +774,11 @@ class WC_Admin_Post_Types {
return $full_filename;
}
$suffix = strtolower( wp_generate_password( $length_to_prepend, false, false ) );
$suffix = strtolower( wp_generate_password( $length_to_prepend, false, false ) );
$filename = $full_filename;
if ( strlen( $ext ) > 0 ) {
$filename = substr( $filename, 0, strlen( $filename ) - strlen( $ext ) );
$filename = substr( $filename, 0, strlen( $filename ) - strlen( $ext ) );
}
$full_filename = str_replace(
@ -892,9 +804,10 @@ class WC_Admin_Post_Types {
* @param int $product_id product identifier.
* @param int $variation_id optional product variation identifier.
* @param array $downloadable_files newly set files.
* @deprecated and moved to post-data class.
* @deprecated 3.3.0 and moved to post-data class.
*/
public function process_product_file_download_paths( $product_id, $variation_id, $downloadable_files ) {
wc_deprecated_function( 'WC_Admin_Post_Types::process_product_file_download_paths', '3.3', '' );
WC_Post_Data::process_product_file_download_paths( $product_id, $variation_id, $downloadable_files );
}
@ -961,6 +874,118 @@ class WC_Admin_Post_Types {
return $post_states;
}
/**
* Apply product type constraints to stock status.
*
* @param WC_Product $product The product whose stock status will be adjusted.
* @param string|null $stock_status The stock status to use for adjustment, or null if no new stock status has been supplied in the request.
* @return WC_Product The supplied product, or the synced product if it was a variable product.
*/
private function maybe_update_stock_status( $product, $stock_status ) {
if ( $product->is_type( 'external' ) ) {
// External products are always in stock.
$product->set_stock_status( 'instock' );
} elseif ( isset( $stock_status ) ) {
if ( $product->is_type( 'variable' ) && ! $product->get_manage_stock() ) {
// Stock status is determined by children.
foreach ( $product->get_children() as $child_id ) {
$child = wc_get_product( $child_id );
if ( ! $product->get_manage_stock() ) {
$child->set_stock_status( $stock_status );
$child->save();
}
}
$product = WC_Product_Variable::sync( $product, false );
} else {
$product->set_stock_status( $stock_status );
}
}
return $product;
}
/**
* Set the new regular or sale price if requested.
*
* @param WC_Product $product The product to set the new price for.
* @param string $price_type 'regular' or 'sale'.
* @return bool true if a new price has been set, false otherwise.
*/
private function set_new_price( $product, $price_type ) {
// phpcs:disable WordPress.Security.NonceVerification.Recommended
$request_data = $this->request_data();
if ( empty( $request_data[ "change_{$price_type}_price" ] ) || ! isset( $request_data[ "_{$price_type}_price" ] ) ) {
return false;
}
$old_price = $product->{"get_{$price_type}_price"}();
$price_changed = false;
$change_price = absint( $request_data[ "change_{$price_type}_price" ] );
$raw_price = wc_clean( wp_unslash( $request_data[ "_{$price_type}_price" ] ) );
$is_percentage = (bool) strstr( $raw_price, '%' );
$price = wc_format_decimal( $raw_price );
switch ( $change_price ) {
case 1:
$new_price = $price;
break;
case 2:
if ( $is_percentage ) {
$percent = $price / 100;
$new_price = $old_price + ( $old_price * $percent );
} else {
$new_price = $old_price + $price;
}
break;
case 3:
if ( $is_percentage ) {
$percent = $price / 100;
$new_price = max( 0, $old_price - ( $old_price * $percent ) );
} else {
$new_price = max( 0, $old_price - $price );
}
break;
case 4:
if ( 'sale' !== $price_type ) {
break;
}
$regular_price = $product->get_regular_price();
if ( $is_percentage ) {
$percent = $price / 100;
$new_price = max( 0, $regular_price - ( round( $regular_price * $percent, wc_get_price_decimals() ) ) );
} else {
$new_price = max( 0, $regular_price - $price );
}
break;
default:
break;
}
if ( isset( $new_price ) && $new_price !== $old_price ) {
$price_changed = true;
$new_price = round( $new_price, wc_get_price_decimals() );
$product->{"set_{$price_type}_price"}( $new_price );
}
return $price_changed;
// phpcs:disable WordPress.Security.NonceVerification.Recommended
}
/**
* Get the current request data ($_REQUEST superglobal).
* This method is added to ease unit testing.
*
* @return array The $_REQUEST superglobal.
*/
protected function request_data() {
return $_REQUEST;
}
}
new WC_Admin_Post_Types();

View File

@ -73,6 +73,9 @@ class WC_Helper_Updater {
}
}
$translations = self::get_translations_update_data();
$transient->translations = array_merge( isset( $transient->translations ) ? $transient->translations : array(), $translations );
return $transient;
}
@ -162,6 +165,123 @@ class WC_Helper_Updater {
return self::_update_check( $payload );
}
/**
* Get translations updates informations.
*
* Scans through all subscriptions for the connected user, as well
* as all Woo extensions without a subscription, and obtains update
* data for each product.
*
* @return array Update data {product_id => data}
*/
public static function get_translations_update_data() {
$payload = array();
$installed_translations = wp_get_installed_translations( 'plugins' );
$locales = array_values( get_available_languages() );
/**
* Filters the locales requested for plugin translations.
*
* @since 3.7.0
* @since 4.5.0 The default value of the `$locales` parameter changed to include all locales.
*
* @param array $locales Plugin locales. Default is all available locales of the site.
*/
$locales = apply_filters( 'plugins_update_check_locales', $locales );
$locales = array_unique( $locales );
// No locales, the respone will be empty, we can return now.
if ( empty( $locales ) ) {
return array();
}
// Scan local plugins which may or may not have a subscription.
$plugins = WC_Helper::get_local_woo_plugins();
$active_woo_plugins = array_intersect( array_keys( $plugins ), get_option( 'active_plugins', array() ) );
/*
* Use only plugins that are subscribed to the automatic translations updates.
*/
$active_for_translations = array_filter(
$active_woo_plugins,
function( $plugin ) use ( $plugins ) {
return apply_filters( 'woocommerce_translations_updates_for_' . $plugins[ $plugin ]['slug'], false );
}
);
// Nothing to check for, exit.
if ( empty( $active_for_translations ) ) {
return array();
}
if ( wp_doing_cron() ) {
$timeout = 30;
} else {
// Three seconds, plus one extra second for every 10 plugins.
$timeout = 3 + (int) ( count( $active_for_translations ) / 10 );
}
$request_body = array(
'locales' => $locales,
'plugins' => array(),
);
foreach ( $active_for_translations as $active_plugin ) {
$plugin = $plugins[ $active_plugin ];
$request_body['plugins'][ $plugin['slug'] ] = array( 'version' => $plugin['Version'] );
}
$raw_response = wp_remote_post(
'https://translate.wordpress.com/api/translations-updates/woocommerce',
array(
'body' => json_encode( $request_body ),
'headers' => array( 'Content-Type: application/json' ),
'timeout' => $timeout,
)
);
// Something wrong happened on the translate server side.
$response_code = wp_remote_retrieve_response_code( $raw_response );
if ( 200 !== $response_code ) {
return array();
}
$response = json_decode( wp_remote_retrieve_body( $raw_response ), true );
// API error, api returned but something was wrong.
if ( array_key_exists( 'success', $response ) && false === $response['success'] ) {
return array();
}
$translations = array();
foreach ( $response['data'] as $plugin_name => $language_packs ) {
foreach ( $language_packs as $language_pack ) {
// Maybe we have this language pack already installed so lets check revision date.
if ( array_key_exists( $plugin_name, $installed_translations ) && array_key_exists( $language_pack['wp_locale'], $installed_translations[ $plugin_name ] ) ) {
$installed_translation_revision_time = new DateTime( $installed_translations[ $plugin_name ][ $language_pack['wp_locale'] ]['PO-Revision-Date'] );
$new_translation_revision_time = new DateTime( $language_pack['last_modified'] );
// Skip if translation language pack is not newer than what is installed already.
if ( $new_translation_revision_time <= $installed_translation_revision_time ) {
continue;
}
}
$translations[] = array(
'type' => 'plugin',
'slug' => $plugin_name,
'language' => $language_pack['wp_locale'],
'version' => $language_pack['version'],
'updated' => $language_pack['last_modified'],
'package' => $language_pack['package'],
'autoupdate' => true,
);
}
}
return $translations;
}
/**
* Run an update check API call.
*

View File

@ -395,11 +395,12 @@ class WC_Admin_List_Table_Products extends WC_Admin_List_Table {
/**
* Search by SKU or ID for products.
*
* @deprecated Logic moved to query_filters.
* @deprecated 4.4.0 Logic moved to query_filters.
* @param string $where Where clause SQL.
* @return string
*/
public function sku_search( $where ) {
wc_deprecated_function( 'WC_Admin_List_Table_Products::sku_search', '4.4.0', 'Logic moved to query_filters.' );
return $where;
}

View File

@ -2,9 +2,14 @@
/**
* Shows a shipping line
*
* @package WooCommerce/Admin
*
* @var object $item The item being displayed
* @var int $item_id The id of the item being displayed
*
* @package WooCommerce/Admin/Views
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
@ -58,10 +63,10 @@ if ( ! defined( 'ABSPATH' ) ) {
<td class="line_cost" width="1%">
<div class="view">
<?php
echo wc_price( $item->get_total(), array( 'currency' => $order->get_currency() ) );
echo wp_kses_post( wc_price( $item->get_total(), array( 'currency' => $order->get_currency() ) ) );
$refunded = $order->get_total_refunded_for_item( $item_id, 'shipping' );
if ( $refunded ) {
echo '<small class="refunded">-' . wc_price( $refunded, array( 'currency' => $order->get_currency() ) ) . '</small>';
echo wp_kses_post( '<small class="refunded">-' . wc_price( $refunded, array( 'currency' => $order->get_currency() ) ) . '</small>' );
}
?>
</div>
@ -74,7 +79,8 @@ if ( ! defined( 'ABSPATH' ) ) {
</td>
<?php
if ( ( $tax_data = $item->get_taxes() ) && wc_tax_enabled() ) {
$tax_data = $item->get_taxes();
if ( $tax_data && wc_tax_enabled() ) {
foreach ( $order_taxes as $tax_item ) {
$tax_item_id = $tax_item->get_rate_id();
$tax_item_total = isset( $tax_data['total'][ $tax_item_id ] ) ? $tax_data['total'][ $tax_item_id ] : '';
@ -82,10 +88,10 @@ if ( ! defined( 'ABSPATH' ) ) {
<td class="line_tax" width="1%">
<div class="view">
<?php
echo ( '' !== $tax_item_total ) ? wc_price( wc_round_tax_total( $tax_item_total ), array( 'currency' => $order->get_currency() ) ) : '&ndash;';
echo wp_kses_post( ( '' !== $tax_item_total ) ? wc_price( $tax_item_total, array( 'currency' => $order->get_currency() ) ) : '&ndash;' );
$refunded = $order->get_tax_refunded_for_item( $item_id, $tax_item_id, 'shipping' );
if ( $refunded ) {
echo '<small class="refunded">-' . wc_price( $refunded, array( 'currency' => $order->get_currency() ) ) . '</small>';
echo wp_kses_post( '<small class="refunded">-' . wc_price( $refunded, array( 'currency' => $order->get_currency() ) ) . '</small>' );
}
?>
</div>

View File

@ -6,7 +6,7 @@
* @var int $variation_id
* @var WP_POST $variation
* @var WC_Product_Variation $variation_object
* @var array $variation_data array of variation data @deprecated.
* @var array $variation_data array of variation data @deprecated 4.4.0.
*/
defined( 'ABSPATH' ) || exit;

View File

@ -38,7 +38,7 @@ if ( ! $tab_exists ) {
self::show_messages();
do_action( 'woocommerce_settings_' . $current_tab );
do_action( 'woocommerce_settings_tabs_' . $current_tab ); // @deprecated hook. @todo remove in 4.0.
do_action( 'woocommerce_settings_tabs_' . $current_tab ); // @deprecated 3.4.0 hook.
?>
<p class="submit">
<?php if ( empty( $GLOBALS['hide_save_button'] ) ) : ?>

View File

@ -140,7 +140,8 @@ defined( 'ABSPATH' ) || exit;
<select class="visibility" name="_visibility">
<?php
$options = apply_filters(
'woocommerce_product_visibility_options', array(
'woocommerce_product_visibility_options',
array(
'visible' => __( 'Catalog &amp; search', 'woocommerce' ),
'catalog' => __( 'Catalog', 'woocommerce' ),
'search' => __( 'Search', 'woocommerce' ),
@ -174,11 +175,15 @@ defined( 'ABSPATH' ) || exit;
<span class="input-text-wrap">
<select class="stock_status" name="_stock_status">
<?php
echo '<option value="" id="stock_status_no_change">' . esc_html__( '— No Change —', 'woocommerce' ) . '</option>';
foreach ( wc_get_product_stock_status_options() as $key => $value ) {
echo '<option value="' . esc_attr( $key ) . '">' . esc_html( $value ) . '</option>';
}
?>
</select>
<div class="wc-quick-edit-warning" style="display:none">
<?php echo esc_html__( 'This will change the stock status of all variations.', 'woocommerce' ); ?></p>
</div>
</span>
</label>

View File

@ -861,31 +861,48 @@ class WC_AJAX {
wp_die( -1 );
}
$response = array();
if ( ! isset( $_POST['order_id'] ) ) {
throw new Exception( __( 'Invalid order', 'woocommerce' ) );
}
$order_id = absint( wp_unslash( $_POST['order_id'] ) );
// If we passed through items it means we need to save first before adding a new one.
$items = ( ! empty( $_POST['items'] ) ) ? wp_unslash( $_POST['items'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$items_to_add = isset( $_POST['data'] ) ? array_filter( wp_unslash( (array) $_POST['data'] ) ) : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
try {
if ( ! isset( $_POST['order_id'] ) ) {
throw new Exception( __( 'Invalid order', 'woocommerce' ) );
}
$response = self::maybe_add_order_item( $order_id, $items, $items_to_add );
wp_send_json_success( $response );
} catch ( Exception $e ) {
wp_send_json_error( array( 'error' => $e->getMessage() ) );
}
}
$order_id = absint( wp_unslash( $_POST['order_id'] ) ); // WPCS: input var ok.
$order = wc_get_order( $order_id );
/**
* Add order item via AJAX. This is refactored for better unit testing.
*
* @param int $order_id ID of order to add items to.
* @param string|array $items Existing items in order. Empty string if no items to add.
* @param array $items_to_add Array of items to add.
*
* @return array Fragments to render and notes HTML.
* @throws Exception When unable to add item.
*/
private static function maybe_add_order_item( $order_id, $items, $items_to_add ) {
try {
$order = wc_get_order( $order_id );
if ( ! $order ) {
throw new Exception( __( 'Invalid order', 'woocommerce' ) );
}
// If we passed through items it means we need to save first before adding a new one.
$items = ( ! empty( $_POST['items'] ) ) ? wp_unslash( $_POST['items'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
if ( ! empty( $items ) ) {
$save_items = array();
parse_str( $items, $save_items );
wc_save_order_items( $order->get_id(), $save_items );
}
$items_to_add = isset( $_POST['data'] ) ? array_filter( wp_unslash( (array) $_POST['data'] ) ) : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
// Add items to order.
$order_notes = array();
@ -915,12 +932,7 @@ class WC_AJAX {
$added_items[ $item_id ] = $item;
$order_notes[ $item_id ] = $product->get_formatted_name();
if ( $product->managing_stock() ) {
$new_stock = wc_update_product_stock( $product, $qty, 'decrease' );
$order_notes[ $item_id ] = $product->get_formatted_name() . ' &ndash; ' . ( $new_stock + $qty ) . '&rarr;' . $new_stock;
$item->add_meta_data( '_reduced_stock', $qty, true );
$item->save();
}
// We do not perform any stock operations here because they will be handled when order is moved to a status where stock operations are applied (like processing, completed etc).
do_action( 'woocommerce_ajax_add_order_item_meta', $item_id, $item, $order );
}
@ -942,18 +954,13 @@ class WC_AJAX {
include 'admin/meta-boxes/views/html-order-notes.php';
$notes_html = ob_get_clean();
wp_send_json_success(
array(
'html' => $items_html,
'notes_html' => $notes_html,
)
return array(
'html' => $items_html,
'notes_html' => $notes_html,
);
} catch ( Exception $e ) {
wp_send_json_error( array( 'error' => $e->getMessage() ) );
throw $e; // Forward exception to caller.
}
// wp_send_json_success must be outside the try block not to break phpunit tests.
wp_send_json_success( $response );
}
/**

View File

@ -341,7 +341,8 @@ final class WC_Cart_Totals {
$shipping_line->taxable = true;
$shipping_line->total = wc_add_number_precision_deep( $shipping_object->cost );
$shipping_line->taxes = wc_add_number_precision_deep( $shipping_object->taxes, false );
$shipping_line->total_tax = array_sum( array_map( array( $this, 'round_line_tax' ), $shipping_line->taxes ) );
$shipping_line->taxes = array_map( array( $this, 'round_item_subtotal' ), $shipping_line->taxes );
$shipping_line->total_tax = array_sum( $shipping_line->taxes );
$this->shipping[ $key ] = $shipping_line;
}
@ -858,7 +859,7 @@ final class WC_Cart_Totals {
* @since 3.2.0
*/
protected function calculate_totals() {
$this->set_total( 'total', round( $this->get_total( 'items_total', true ) + $this->get_total( 'fees_total', true ) + $this->get_total( 'shipping_total', true ) + wc_round_tax_total( array_sum( $this->get_merged_taxes( true ) ), 0 ), 0 ) );
$this->set_total( 'total', round( $this->get_total( 'items_total', true ) + $this->get_total( 'fees_total', true ) + $this->get_total( 'shipping_total', true ) + array_sum( $this->get_merged_taxes( true ) ), 0 ) );
$this->cart->set_total_tax( array_sum( $this->get_merged_taxes( false ) ) );
// Allow plugins to hook and alter totals before final total is calculated.

View File

@ -849,7 +849,8 @@ class WC_Cart extends WC_Legacy_Cart {
* @return array
*/
public function get_tax_totals() {
$taxes = $this->get_taxes();
$shipping_taxes = $this->get_shipping_taxes(); // Shipping taxes are rounded differently, so we will subtract from all taxes, then round and then add them back.
$taxes = $this->get_taxes();
$tax_totals = array();
foreach ( $taxes as $key => $tax ) {
@ -860,9 +861,17 @@ class WC_Cart extends WC_Legacy_Cart {
$tax_totals[ $code ] = new stdClass();
$tax_totals[ $code ]->amount = 0;
}
$tax_totals[ $code ]->tax_rate_id = $key;
$tax_totals[ $code ]->is_compound = WC_Tax::is_compound( $key );
$tax_totals[ $code ]->label = WC_Tax::get_rate_label( $key );
$tax_totals[ $code ]->tax_rate_id = $key;
$tax_totals[ $code ]->is_compound = WC_Tax::is_compound( $key );
$tax_totals[ $code ]->label = WC_Tax::get_rate_label( $key );
if ( isset( $shipping_taxes[ $key ] ) ) {
$tax -= $shipping_taxes[ $key ];
$tax = wc_round_tax_total( $tax );
$tax += round( $shipping_taxes[ $key ], wc_get_price_decimals() );
unset( $shipping_taxes[ $key ] );
}
$tax_totals[ $code ]->amount += wc_round_tax_total( $tax );
$tax_totals[ $code ]->formatted_amount = wc_price( $tax_totals[ $code ]->amount );
}
@ -1902,7 +1911,7 @@ class WC_Cart extends WC_Legacy_Cart {
if ( ! $compound && WC_Tax::is_compound( $key ) ) {
continue;
}
$total += wc_round_tax_total( $tax );
$total += $tax;
}
if ( $display ) {
$total = wc_format_decimal( $total, wc_get_price_decimals() );

View File

@ -454,8 +454,8 @@ class WC_Checkout {
*/
$item = apply_filters( 'woocommerce_checkout_create_order_line_item_object', new WC_Order_Item_Product(), $cart_item_key, $values, $order );
$product = $values['data'];
$item->legacy_values = $values; // @deprecated For legacy actions.
$item->legacy_cart_item_key = $cart_item_key; // @deprecated For legacy actions.
$item->legacy_values = $values; // @deprecated 4.4.0 For legacy actions.
$item->legacy_cart_item_key = $cart_item_key; // @deprecated 4.4.0 For legacy actions.
$item->set_props(
array(
'quantity' => $values['quantity'],
@ -502,8 +502,8 @@ class WC_Checkout {
public function create_order_fee_lines( &$order, $cart ) {
foreach ( $cart->get_fees() as $fee_key => $fee ) {
$item = new WC_Order_Item_Fee();
$item->legacy_fee = $fee; // @deprecated For legacy actions.
$item->legacy_fee_key = $fee_key; // @deprecated For legacy actions.
$item->legacy_fee = $fee; // @deprecated 4.4.0 For legacy actions.
$item->legacy_fee_key = $fee_key; // @deprecated 4.4.0 For legacy actions.
$item->set_props(
array(
'name' => $fee->name,
@ -541,7 +541,7 @@ class WC_Checkout {
if ( isset( $chosen_shipping_methods[ $package_key ], $package['rates'][ $chosen_shipping_methods[ $package_key ] ] ) ) {
$shipping_rate = $package['rates'][ $chosen_shipping_methods[ $package_key ] ];
$item = new WC_Order_Item_Shipping();
$item->legacy_package_key = $package_key; // @deprecated For legacy actions.
$item->legacy_package_key = $package_key; // @deprecated 4.4.0 For legacy actions.
$item->set_props(
array(
'method_title' => $shipping_rate->label,

View File

@ -191,10 +191,12 @@ class WC_Download_Handler {
/**
* Count download.
*
* @deprecated unknown
* @deprecated 4.4.0
* @param array $download_data Download data.
*/
public static function count_download( $download_data ) {}
public static function count_download( $download_data ) {
wc_deprecated_function( 'WC_Download_Handler::count_download', '4.4.0', '' );
}
/**
* Download a file - hook into init function.
@ -214,7 +216,14 @@ class WC_Download_Handler {
}
$filename = apply_filters( 'woocommerce_file_download_filename', $filename, $product_id );
$file_download_method = apply_filters( 'woocommerce_file_download_method', get_option( 'woocommerce_file_download_method', 'force' ), $product_id );
/**
* Filter download method.
*
* @param string $method Download method.
* @param int $product_id Product ID.
* @param string $file_path URL to file.
*/
$file_download_method = apply_filters( 'woocommerce_file_download_method', get_option( 'woocommerce_file_download_method', 'force' ), $product_id, $file_path );
// Add action to prevent issues in IE.
add_action( 'nocache_headers', array( __CLASS__, 'ie_nocache_headers_fix' ) );

View File

@ -149,6 +149,10 @@ class WC_Install {
'wc_update_400_reset_action_scheduler_migration_status',
'wc_update_400_db_version',
),
'4.4.0' => array(
'wc_update_440_insert_attribute_terms_for_variable_products',
'wc_update_440_db_version',
),
);
/**
@ -752,14 +756,14 @@ class WC_Install {
AND CONSTRAINT_NAME = 'fk_{$wpdb->prefix}wc_download_log_permission_id'
AND CONSTRAINT_TYPE = 'FOREIGN KEY'
AND TABLE_NAME = '{$wpdb->prefix}wc_download_log'"
); // WPCS: unprepared SQL ok.
);
if ( 0 === (int) $fk_result->fk_count ) {
$wpdb->query(
"ALTER TABLE `{$wpdb->prefix}wc_download_log`
ADD CONSTRAINT `fk_{$wpdb->prefix}wc_download_log_permission_id`
FOREIGN KEY (`permission_id`)
REFERENCES `{$wpdb->prefix}woocommerce_downloadable_product_permissions` (`permission_id`) ON DELETE CASCADE;"
); // WPCS: unprepared SQL ok.
);
}
}

View File

@ -136,11 +136,12 @@ class WC_Order_Item_Coupon extends WC_Order_Item {
/**
* OffsetGet for ArrayAccess/Backwards compatibility.
*
* @deprecated Add deprecation notices in future release.
* @deprecated 4.4.0
* @param string $offset Offset.
* @return mixed
*/
public function offsetGet( $offset ) {
wc_deprecated_function( 'WC_Order_Item_Coupon::offsetGet', '4.4.0', '' );
if ( 'discount_amount' === $offset ) {
$offset = 'discount';
} elseif ( 'discount_amount_tax' === $offset ) {
@ -152,11 +153,12 @@ class WC_Order_Item_Coupon extends WC_Order_Item {
/**
* OffsetSet for ArrayAccess/Backwards compatibility.
*
* @deprecated Add deprecation notices in future release.
* @deprecated 4.4.0
* @param string $offset Offset.
* @param mixed $value Value.
*/
public function offsetSet( $offset, $value ) {
wc_deprecated_function( 'WC_Order_Item_Coupon::offsetSet', '4.4.0', '' );
if ( 'discount_amount' === $offset ) {
$offset = 'discount';
} elseif ( 'discount_amount_tax' === $offset ) {

View File

@ -290,7 +290,6 @@ class WC_Order_Item_Fee extends WC_Order_Item {
/**
* OffsetGet for ArrayAccess/Backwards compatibility.
*
* @deprecated Add deprecation notices in future release.
* @param string $offset Offset.
* @return mixed
*/
@ -308,11 +307,12 @@ class WC_Order_Item_Fee extends WC_Order_Item {
/**
* OffsetSet for ArrayAccess/Backwards compatibility.
*
* @deprecated Add deprecation notices in future release.
* @deprecated 4.4.0
* @param string $offset Offset.
* @param mixed $value Value.
*/
public function offsetSet( $offset, $value ) {
wc_deprecated_function( 'WC_Order_Item_Fee::offsetSet', '4.4.0', '' );
if ( 'line_total' === $offset ) {
$offset = 'total';
} elseif ( 'line_tax' === $offset ) {

View File

@ -425,7 +425,6 @@ class WC_Order_Item_Product extends WC_Order_Item {
/**
* OffsetGet for ArrayAccess/Backwards compatibility.
*
* @deprecated Add deprecation notices in future release.
* @param string $offset Offset.
* @return mixed
*/
@ -449,11 +448,12 @@ class WC_Order_Item_Product extends WC_Order_Item {
/**
* OffsetSet for ArrayAccess/Backwards compatibility.
*
* @deprecated Add deprecation notices in future release.
* @deprecated 4.4.0
* @param string $offset Offset.
* @param mixed $value Value.
*/
public function offsetSet( $offset, $value ) {
wc_deprecated_function( 'WC_Order_Item_Product::offsetSet', '4.4.0', '' );
if ( 'line_subtotal' === $offset ) {
$offset = 'subtotal';
} elseif ( 'line_subtotal_tax' === $offset ) {

View File

@ -276,7 +276,6 @@ class WC_Order_Item_Shipping extends WC_Order_Item {
/**
* Offset get: for ArrayAccess/Backwards compatibility.
*
* @deprecated Add deprecation notices in future release.
* @param string $offset Key.
* @return mixed
*/
@ -290,11 +289,12 @@ class WC_Order_Item_Shipping extends WC_Order_Item {
/**
* Offset set: for ArrayAccess/Backwards compatibility.
*
* @deprecated Add deprecation notices in future release.
* @deprecated 4.4.0
* @param string $offset Key.
* @param mixed $value Value to set.
*/
public function offsetSet( $offset, $value ) {
wc_deprecated_function( 'WC_Order_Item_Shipping::offsetSet', '4.4.0', '' );
if ( 'cost' === $offset ) {
$offset = 'total';
}

View File

@ -244,7 +244,6 @@ class WC_Order_Item_Tax extends WC_Order_Item {
/**
* O for ArrayAccess/Backwards compatibility.
*
* @deprecated Add deprecation notices in future release.
* @param string $offset Offset.
* @return mixed
*/
@ -260,11 +259,12 @@ class WC_Order_Item_Tax extends WC_Order_Item {
/**
* OffsetSet for ArrayAccess/Backwards compatibility.
*
* @deprecated Add deprecation notices in future release.
* @deprecated 4.4.0
* @param string $offset Offset.
* @param mixed $value Value.
*/
public function offsetSet( $offset, $value ) {
wc_deprecated_function( 'WC_Order_Item_Tax::offsetSet', '4.4.0', '' );
if ( 'tax_amount' === $offset ) {
$offset = 'tax_total';
} elseif ( 'shipping_tax_amount' === $offset ) {

View File

@ -1411,8 +1411,7 @@ class WC_Order extends WC_Abstract_Order {
$needs_address = false;
foreach ( $this->get_shipping_methods() as $shipping_method ) {
// Remove any instance IDs after ":".
$shipping_method_id = current( explode( ':', $shipping_method['method_id'] ) );
$shipping_method_id = $shipping_method->get_method_id();
if ( ! in_array( $shipping_method_id, $hide, true ) ) {
$needs_address = true;

View File

@ -282,10 +282,12 @@ class WC_Product_Variable extends WC_Product {
/**
* Get an array of available variations for the current product.
*
* @return array
* @param bool $render_variations Prepares a data array for each variant for output in the add to cart form. Pass `false` to only return the available variations as objects.
* @param bool $return_array_of_data If true, return an array of data for the variation; if false, return a WC_Product_Variation object.
*
* @return array|WC_Product_Variation
*/
public function get_available_variations() {
public function get_available_variations( $render_variations = true, $return_array_of_data = true ) {
$variation_ids = $this->get_children();
$available_variations = array();
@ -307,14 +309,41 @@ class WC_Product_Variable extends WC_Product {
continue;
}
$available_variations[] = $this->get_available_variation( $variation );
if ( $render_variations ) {
$available_variations[] = $return_array_of_data ? $this->get_available_variation( $variation ) : $variation;
} else {
$available_variations[] = $variation;
}
}
$available_variations = array_values( array_filter( $available_variations ) );
if ( $render_variations ) {
$available_variations = array_values( array_filter( $available_variations ) );
}
return $available_variations;
}
/**
* Check if a given variation is currently available.
*
* @param WC_Product_Variation $variation Variation to check.
*
* @return bool True if the variation is available, false otherwise.
*/
private function variation_is_available( WC_Product_Variation $variation ) {
// Hide out of stock variations if 'Hide out of stock items from the catalog' is checked.
if ( ! $variation || ! $variation->exists() || ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) && ! $variation->is_in_stock() ) ) {
return false;
}
// Filter 'woocommerce_hide_invisible_variations' to optionally hide invisible variations (disabled variations and variations with empty price).
if ( apply_filters( 'woocommerce_hide_invisible_variations', true, $this->get_id(), $variation ) && ! $variation->variation_is_visible() ) {
return false;
}
return true;
}
/**
* Returns an array of data for a variation. Used in the add to cart form.
*
@ -565,6 +594,95 @@ class WC_Product_Variable extends WC_Product {
return true;
}
/**
* Returns whether or not the product is visible in the catalog (doesn't trigger filters).
*
* @return bool
*/
protected function is_visible_core() {
if ( ! $this->parent_is_visible_core() ) {
return false;
}
$query_filters = $this->get_layered_nav_chosen_attributes();
if ( empty( $query_filters ) ) {
return true;
}
/**
* If there are attribute filters in the request, a variable product will be visible
* if at least one of the available variations matches the filters.
*/
$attributes_with_terms = array();
array_walk(
$query_filters,
function( $value, $key ) use ( &$attributes_with_terms ) {
$attributes_with_terms[ $key ] = $value['terms'];
}
);
$variations = $this->get_available_variations( true, false );
foreach ( $variations as $variation ) {
if ( $this->variation_matches_filters( $variation, $attributes_with_terms ) ) {
return true;
}
}
return false;
}
/**
* Checks if a given variation matches the active attribute filters.
*
* @param WC_Product_Variation $variation The variation to check.
* @param array $query_filters The active filters as an array of attribute_name => [term1, term2...].
*
* @return bool True if the variation matches the active attribute filters.
*/
private function variation_matches_filters( WC_Product_Variation $variation, array $query_filters ) {
// Get the variation attributes as an array of attribute_name => attribute_value.
// The array_filter will filter out attributes having a value of '', these correspond
// to "Any..." variations that don't participate in filtering.
$variation_attributes = array_filter( $variation->get_variation_attributes( false ) );
$variation_attribute_names_in_filters = array_intersect( array_keys( $query_filters ), array_keys( $variation_attributes ) );
if ( empty( $variation_attribute_names_in_filters ) ) {
// The variation doesn't have any attribute that participates in filtering so we consider it a match.
return true;
}
foreach ( $variation_attribute_names_in_filters as $attribute_name ) {
if ( ! in_array( $variation_attributes[ $attribute_name ], $query_filters[ $attribute_name ], true ) ) {
// Multiple filters interact with AND logic, so as soon as one of them
// doesn't match then the variation doesn't match.
return false;
}
}
return true;
}
/**
* What does is_visible_core in the parent class say?
* This method exists to ease unit testing.
*
* @return bool
*/
protected function parent_is_visible_core() {
return parent::is_visible_core();
}
/**
* Get an array of attributes and terms selected with the layered nav widget.
* This method exists to ease unit testing.
*
* @return array
*/
protected function get_layered_nav_chosen_attributes() {
return WC()->query::get_layered_nav_chosen_attributes();
}
/*
|--------------------------------------------------------------------------
| Sync with child variations.

View File

@ -110,15 +110,18 @@ class WC_Product_Variation extends WC_Product_Simple {
}
/**
* Get variation attribute values. Keys are prefixed with attribute_, as stored.
* Get variation attribute values. Keys are prefixed with attribute_, as stored, unless $with_prefix is false.
*
* @return array of attributes and their values for this variation
* @param bool $with_prefix Whether keys should be prepended with attribute_ or not, default is true.
* @return array of attributes and their values for this variation.
*/
public function get_variation_attributes() {
public function get_variation_attributes( $with_prefix = true ) {
$attributes = $this->get_attributes();
$variation_attributes = array();
$prefix = $with_prefix ? 'attribute_' : '';
foreach ( $attributes as $key => $value ) {
$variation_attributes[ 'attribute_' . $key ] = $value;
$variation_attributes[ $prefix . $key ] = $value;
}
return $variation_attributes;
}
@ -580,4 +583,22 @@ class WC_Product_Variation extends WC_Product_Simple {
return $valid_classes;
}
/**
* Delete variation, set the ID to 0, and return result.
*
* @since 4.4.0
* @param bool $force_delete Should the variation be deleted permanently.
* @return bool result
*/
public function delete( $force_delete = false ) {
$variation_id = $this->get_id();
if ( ! parent::delete( $force_delete ) ) {
return false;
}
wp_delete_object_term_relationships( $variation_id, wc_get_attribute_taxonomy_names() );
return true;
}
}

View File

@ -32,7 +32,7 @@ class WC_Query {
*
* @var array
*/
private static $_chosen_attributes;
private static $chosen_attributes;
/**
* Constructor for the query class. Hooks in methods.
@ -45,6 +45,7 @@ class WC_Query {
add_action( 'parse_request', array( $this, 'parse_request' ), 0 );
add_action( 'pre_get_posts', array( $this, 'pre_get_posts' ) );
add_filter( 'the_posts', array( $this, 'remove_product_query_filters' ) );
add_filter( 'found_posts', array( $this, 'adjust_posts_count' ) );
add_filter( 'get_pagenum_link', array( $this, 'remove_add_to_cart_pagination' ), 10, 1 );
}
$this->init_query_vars();
@ -54,7 +55,8 @@ class WC_Query {
* Get any errors from querystring.
*/
public function get_errors() {
$error = ! empty( $_GET['wc_error'] ) ? sanitize_text_field( wp_unslash( $_GET['wc_error'] ) ) : ''; // WPCS: input var ok, CSRF ok.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$error = ! empty( $_GET['wc_error'] ) ? sanitize_text_field( wp_unslash( $_GET['wc_error'] ) ) : '';
if ( $error && ! wc_has_notice( $error, 'error' ) ) {
wc_add_notice( $error, 'error' );
@ -217,14 +219,16 @@ class WC_Query {
public function parse_request() {
global $wp;
// phpcs:disable WordPress.Security.NonceVerification.Recommended
// Map query vars to their keys, or get them if endpoints are not supported.
foreach ( $this->get_query_vars() as $key => $var ) {
if ( isset( $_GET[ $var ] ) ) { // WPCS: input var ok, CSRF ok.
$wp->query_vars[ $key ] = sanitize_text_field( wp_unslash( $_GET[ $var ] ) ); // WPCS: input var ok, CSRF ok.
if ( isset( $_GET[ $var ] ) ) {
$wp->query_vars[ $key ] = sanitize_text_field( wp_unslash( $_GET[ $var ] ) );
} elseif ( isset( $wp->query_vars[ $var ] ) ) {
$wp->query_vars[ $key ] = $wp->query_vars[ $var ];
}
}
// phpcs:enable WordPress.Security.NonceVerification.Recommended
}
/**
@ -363,6 +367,61 @@ class WC_Query {
return $posts;
}
/**
* When the request is filtering by attributes via layered nav plugin we need to adjust the total posts count
* to account for variable products having stock in some variations but not in others.
* We do that by just checking if each product is visible.
*
* We also cache the post visibility so that it isn't checked again when displaying the posts list.
*
* @since 4.4.0
* @param int $count Original posts count, as supplied by the found_posts filter.
*
* @return int Adjusted posts count.
*/
public function adjust_posts_count( $count ) {
$posts = $this->get_current_posts();
if ( is_null( $posts ) ) {
return $count;
}
$count = 0;
foreach ( $posts as $post ) {
$id = is_object( $post ) ? $post->ID : $post;
$product = wc_get_product( $id );
if ( ! is_object( $product ) ) {
continue;
}
if ( $product->is_visible() ) {
wc_set_loop_product_visibility( $id, true );
$count++;
} else {
wc_set_loop_product_visibility( $id, false );
}
}
wc_set_loop_prop( 'total', $count );
return $count;
}
/**
* Instance version of get_layered_nav_chosen_attributes, needed for unit tests.
*
* @return array
*/
protected function get_layered_nav_chosen_attributes_inst() {
return self::get_layered_nav_chosen_attributes();
}
/**
* Get the posts (or the ids of the posts) found in the current WP loop.
*
* @return array Array of posts or post ids.
*/
protected function get_current_posts() {
return $GLOBALS['wp_query']->posts;
}
/**
* WP SEO meta description.
*
@ -447,7 +506,8 @@ class WC_Query {
public function get_catalog_ordering_args( $orderby = '', $order = '' ) {
// Get ordering from query string unless defined.
if ( ! $orderby ) {
$orderby_value = isset( $_GET['orderby'] ) ? wc_clean( (string) wp_unslash( $_GET['orderby'] ) ) : wc_clean( get_query_var( 'orderby' ) ); // WPCS: sanitization ok, input var ok, CSRF ok.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$orderby_value = isset( $_GET['orderby'] ) ? wc_clean( (string) wp_unslash( $_GET['orderby'] ) ) : wc_clean( get_query_var( 'orderby' ) );
if ( ! $orderby_value ) {
if ( is_search() ) {
@ -522,12 +582,15 @@ class WC_Query {
public function price_filter_post_clauses( $args, $wp_query ) {
global $wpdb;
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( ! $wp_query->is_main_query() || ( ! isset( $_GET['max_price'] ) && ! isset( $_GET['min_price'] ) ) ) {
return $args;
}
$current_min_price = isset( $_GET['min_price'] ) ? floatval( wp_unslash( $_GET['min_price'] ) ) : 0; // WPCS: input var ok, CSRF ok.
$current_max_price = isset( $_GET['max_price'] ) ? floatval( wp_unslash( $_GET['max_price'] ) ) : PHP_INT_MAX; // WPCS: input var ok, CSRF ok.
// phpcs:disable WordPress.Security.NonceVerification.Recommended
$current_min_price = isset( $_GET['min_price'] ) ? floatval( wp_unslash( $_GET['min_price'] ) ) : 0;
$current_max_price = isset( $_GET['max_price'] ) ? floatval( wp_unslash( $_GET['max_price'] ) ) : PHP_INT_MAX;
// phpcs:enable WordPress.Security.NonceVerification.Recommended
/**
* Adjust if the store taxes are not displayed how they are stored.
@ -666,9 +729,11 @@ class WC_Query {
$product_visibility_not_in[] = $product_visibility_terms['outofstock'];
}
// phpcs:disable WordPress.Security.NonceVerification.Recommended
// Filter by rating.
if ( isset( $_GET['rating_filter'] ) ) { // WPCS: input var ok, CSRF ok.
$rating_filter = array_filter( array_map( 'absint', explode( ',', $_GET['rating_filter'] ) ) ); // WPCS: input var ok, CSRF ok, Sanitization ok.
if ( isset( $_GET['rating_filter'] ) ) {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$rating_filter = array_filter( array_map( 'absint', explode( ',', wp_unslash( $_GET['rating_filter'] ) ) ) );
$rating_terms = array();
for ( $i = 1; $i <= 5; $i ++ ) {
if ( in_array( $i, $rating_filter, true ) && isset( $product_visibility_terms[ 'rated-' . $i ] ) ) {
@ -685,6 +750,7 @@ class WC_Query {
);
}
}
// phpcs:enable WordPress.Security.NonceVerification.Recommended
if ( ! empty( $product_visibility_not_in ) ) {
$tax_query[] = array(
@ -753,8 +819,9 @@ class WC_Query {
$term = substr( $term, 1 );
}
$like = '%' . $wpdb->esc_like( $term ) . '%';
$sql[] = $wpdb->prepare( "(($wpdb->posts.post_title $like_op %s) $andor_op ($wpdb->posts.post_excerpt $like_op %s) $andor_op ($wpdb->posts.post_content $like_op %s))", $like, $like, $like ); // unprepared SQL ok.
$like = '%' . $wpdb->esc_like( $term ) . '%';
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$sql[] = $wpdb->prepare( "(($wpdb->posts.post_title $like_op %s) $andor_op ($wpdb->posts.post_excerpt $like_op %s) $andor_op ($wpdb->posts.post_content $like_op %s))", $like, $like, $like );
}
if ( ! empty( $sql ) && ! is_user_logged_in() ) {
@ -770,11 +837,12 @@ class WC_Query {
* @return array
*/
public static function get_layered_nav_chosen_attributes() {
if ( ! is_array( self::$_chosen_attributes ) ) {
self::$_chosen_attributes = array();
// phpcs:disable WordPress.Security.NonceVerification.Recommended
if ( ! is_array( self::$chosen_attributes ) ) {
self::$chosen_attributes = array();
if ( ! empty( $_GET ) ) { // WPCS: input var ok, CSRF ok.
foreach ( $_GET as $key => $value ) { // WPCS: input var ok, CSRF ok.
if ( ! empty( $_GET ) ) {
foreach ( $_GET as $key => $value ) {
if ( 0 === strpos( $key, 'filter_' ) ) {
$attribute = wc_sanitize_taxonomy_name( str_replace( 'filter_', '', $key ) );
$taxonomy = wc_attribute_taxonomy_name( $attribute );
@ -784,14 +852,15 @@ class WC_Query {
continue;
}
$query_type = ! empty( $_GET[ 'query_type_' . $attribute ] ) && in_array( $_GET[ 'query_type_' . $attribute ], array( 'and', 'or' ), true ) ? wc_clean( wp_unslash( $_GET[ 'query_type_' . $attribute ] ) ) : ''; // WPCS: sanitization ok, input var ok, CSRF ok.
self::$_chosen_attributes[ $taxonomy ]['terms'] = array_map( 'sanitize_title', $filter_terms ); // Ensures correct encoding.
self::$_chosen_attributes[ $taxonomy ]['query_type'] = $query_type ? $query_type : apply_filters( 'woocommerce_layered_nav_default_query_type', 'and' );
$query_type = ! empty( $_GET[ 'query_type_' . $attribute ] ) && in_array( $_GET[ 'query_type_' . $attribute ], array( 'and', 'or' ), true ) ? wc_clean( wp_unslash( $_GET[ 'query_type_' . $attribute ] ) ) : '';
self::$chosen_attributes[ $taxonomy ]['terms'] = array_map( 'sanitize_title', $filter_terms ); // Ensures correct encoding.
self::$chosen_attributes[ $taxonomy ]['query_type'] = $query_type ? $query_type : apply_filters( 'woocommerce_layered_nav_default_query_type', 'and' );
}
}
}
}
return self::$_chosen_attributes;
return self::$chosen_attributes;
// phpcs:disable WordPress.Security.NonceVerification.Recommended
}
/**
@ -804,7 +873,6 @@ class WC_Query {
return remove_query_arg( 'add-to-cart', $url );
}
// @codingStandardsIgnoreStart
/**
* Return a meta query for filtering by rating.
*
@ -819,7 +887,7 @@ class WC_Query {
* Returns a meta query to handle product visibility.
*
* @deprecated 3.0.0 Replaced with taxonomy.
* @param string $compare (default: 'IN')
* @param string $compare (default: 'IN').
* @return array
*/
public function visibility_meta_query( $compare = 'IN' ) {
@ -830,7 +898,7 @@ class WC_Query {
* Returns a meta query to handle product stock status.
*
* @deprecated 3.0.0 Replaced with taxonomy.
* @param string $status (default: 'instock')
* @param string $status (default: 'instock').
* @return array
*/
public function stock_status_meta_query( $status = 'instock' ) {
@ -869,6 +937,8 @@ class WC_Query {
/**
* Search post excerpt.
*
* @param string $where Where clause.
*
* @deprecated 3.2.0 - Not needed anymore since WordPress 4.5.
*/
public function search_post_excerpt( $where = '' ) {
@ -878,10 +948,10 @@ class WC_Query {
/**
* Remove the posts_where filter.
*
* @deprecated 3.2.0 - Nothing to remove anymore because search_post_excerpt() is deprecated.
*/
public function remove_posts_where() {
wc_deprecated_function( 'WC_Query::remove_posts_where', '3.2.0', 'Nothing to remove anymore because search_post_excerpt() is deprecated.' );
}
// @codingStandardsIgnoreEnd
}

View File

@ -216,7 +216,7 @@ class WC_Structured_Data {
if ( '' !== $product->get_price() ) {
// Assume prices will be valid until the end of next year, unless on sale and there is an end date.
$price_valid_until = date( 'Y-12-31', time() + YEAR_IN_SECONDS );
$price_valid_until = gmdate( 'Y-12-31', time() + YEAR_IN_SECONDS );
if ( $product->is_type( 'variable' ) ) {
$lowest = $product->get_variation_price( 'min', false );
@ -243,7 +243,7 @@ class WC_Structured_Data {
}
} else {
if ( $product->is_on_sale() && $product->get_date_on_sale_to() ) {
$price_valid_until = date( 'Y-m-d', $product->get_date_on_sale_to()->getTimestamp() );
$price_valid_until = gmdate( 'Y-m-d', $product->get_date_on_sale_to()->getTimestamp() );
}
$markup_offer = array(
'@type' => 'Offer',
@ -459,7 +459,7 @@ class WC_Structured_Data {
continue;
}
$product = $order->get_product_from_item( $item );
$product = $item->get_product();
$product_exists = is_object( $product );
$is_visible = $product_exists && $product->is_visible();
@ -472,12 +472,12 @@ class WC_Structured_Data {
'priceCurrency' => $order->get_currency(),
'eligibleQuantity' => array(
'@type' => 'QuantitativeValue',
'value' => apply_filters( 'woocommerce_email_order_item_quantity', $item['qty'], $item ),
'value' => apply_filters( 'woocommerce_email_order_item_quantity', $item->get_quantity(), $item ),
),
),
'itemOffered' => array(
'@type' => 'Product',
'name' => apply_filters( 'woocommerce_order_item_name', $item['name'], $item, $is_visible ),
'name' => apply_filters( 'woocommerce_order_item_name', $item->get_name(), $item, $is_visible ),
'sku' => $product_exists ? $product->get_sku() : '',
'image' => $product_exists ? wp_get_attachment_image_url( $product->get_image_id() ) : '',
'url' => $is_visible ? get_permalink( $product->get_id() ) : get_home_url(),

View File

@ -8,6 +8,8 @@
defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Proxies\LegacyProxy;
/**
* Main WooCommerce Class.
*
@ -653,7 +655,7 @@ final class WooCommerce {
/**
* Legacy image sizes.
*
* @deprecated These sizes will be removed in 4.0.
* @deprecated 3.3.0 These sizes will be removed in 4.6.0.
*/
add_image_size( 'shop_catalog', $thumbnail['width'], $thumbnail['height'], $thumbnail['crop'] );
add_image_size( 'shop_single', $single['width'], $single['height'], $single['crop'] );
@ -902,4 +904,60 @@ final class WooCommerce {
public function is_wc_admin_active() {
return function_exists( 'wc_admin_url' );
}
/**
* Call a user function. This should be used to execute any non-idempotent function, especially
* those in the `includes` directory or provided by WordPress.
*
* This method can be useful for unit tests, since functions called using this method
* can be easily mocked by using WC_Unit_Test_Case::register_legacy_proxy_function_mocks.
*
* @param string $function_name The function to execute.
* @param mixed ...$parameters The parameters to pass to the function.
*
* @return mixed The result from the function.
*
* @since 4.4
*/
public function call_function( $function_name, ...$parameters ) {
return wc_get_container()->get( LegacyProxy::class )->call_function( $function_name, ...$parameters );
}
/**
* Call a static method in a class. This should be used to execute any non-idempotent method in classes
* from the `includes` directory.
*
* This method can be useful for unit tests, since methods called using this method
* can be easily mocked by using WC_Unit_Test_Case::register_legacy_proxy_static_mocks.
*
* @param string $class_name The name of the class containing the method.
* @param string $method_name The name of the method.
* @param mixed ...$parameters The parameters to pass to the method.
*
* @return mixed The result from the method.
*
* @since 4.4
*/
public function call_static( $class_name, $method_name, ...$parameters ) {
return wc_get_container()->get( LegacyProxy::class )->call_static( $class_name, $method_name, ...$parameters );
}
/**
* Gets an instance of a given legacy class.
* This must not be used to get instances of classes in the `src` directory.
*
* This method can be useful for unit tests, since objects obtained using this method
* can be easily mocked by using WC_Unit_Test_Case::register_legacy_proxy_class_mocks.
*
* @param string $class_name The name of the class to get an instance for.
* @param mixed ...$args Parameters to be passed to the class constructor or to the appropriate internal 'get_instance_of_' method.
*
* @return object The instance of the class.
* @throws \Exception The requested class belongs to the `src` directory, or there was an error creating an instance of the class.
*
* @since 4.4
*/
public function get_instance_of( string $class_name, ...$args ) {
return wc_get_container()->get( LegacyProxy::class )->get_instance_of( $class_name, ...$args );
}
}

View File

@ -190,7 +190,7 @@ class WC_Product_Variable_Data_Store_CPT extends WC_Product_Data_Store_CPT imple
$query_args = array( 'attribute_name' => wc_variation_attribute_name( $attribute['name'] ) ) + $child_ids;
$values = array_unique(
$wpdb->get_col(
$wpdb->prepare( // wpcs: PreparedSQLPlaceholders replacement count ok.
$wpdb->prepare(
"SELECT meta_value FROM {$wpdb->postmeta} WHERE meta_key = %s AND post_id IN {$query_in}", // @codingStandardsIgnoreLine.
$query_args
)
@ -661,6 +661,7 @@ class WC_Product_Variable_Data_Store_CPT extends WC_Product_Data_Store_CPT imple
if ( $force_delete ) {
do_action( 'woocommerce_before_delete_product_variation', $variation_id );
wp_delete_post( $variation_id, true );
wp_delete_object_term_relationships( $variation_id, wc_get_attribute_taxonomy_names() );
do_action( 'woocommerce_delete_product_variation', $variation_id );
} else {
wp_trash_post( $variation_id );

View File

@ -473,10 +473,12 @@ class WC_Product_Variation_Data_Store_CPT extends WC_Product_Data_Store_CPT impl
if ( $force || array_key_exists( 'attributes', $changes ) ) {
global $wpdb;
$product_id = $product->get_id();
$attributes = $product->get_attributes();
$updated_attribute_keys = array();
foreach ( $attributes as $key => $value ) {
update_post_meta( $product->get_id(), 'attribute_' . $key, wp_slash( $value ) );
update_post_meta( $product_id, 'attribute_' . $key, wp_slash( $value ) );
$updated_attribute_keys[] = 'attribute_' . $key;
}
@ -486,13 +488,27 @@ class WC_Product_Variation_Data_Store_CPT extends WC_Product_Data_Store_CPT impl
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.QuotedDynamicPlaceholderGeneration
"SELECT meta_key FROM {$wpdb->postmeta} WHERE meta_key LIKE %s AND meta_key NOT IN ( '" . implode( "','", array_map( 'esc_sql', $updated_attribute_keys ) ) . "' ) AND post_id = %d",
$wpdb->esc_like( 'attribute_' ) . '%',
$product->get_id()
$product_id
)
);
foreach ( $delete_attribute_keys as $key ) {
delete_post_meta( $product->get_id(), $key );
delete_post_meta( $product_id, $key );
}
// Set the attributes as regular taxonomy terms too...
$variation_attributes = array_keys( $product->get_variation_attributes( false ) );
foreach ( $attributes as $name => $value ) {
if ( '' !== $value && in_array( $name, $variation_attributes, true ) && term_exists( $value, $name ) ) {
wp_set_post_terms( $product_id, array( $value ), $name );
} elseif ( taxonomy_exists( $name ) ) {
wp_delete_object_term_relationships( $product_id, $name );
}
}
// ...and remove old taxonomy terms.
$attributes_to_delete = array_diff( wc_get_attribute_taxonomy_names(), array_keys( $attributes ) );
wp_delete_object_term_relationships( $product_id, $attributes_to_delete );
}
}

View File

@ -289,7 +289,7 @@ class WC_Gateway_BACS extends WC_Payment_Gateway {
// Get sortcode label in the $locale array and use appropriate one.
$sortcode = isset( $locale[ $country ]['sortcode']['label'] ) ? $locale[ $country ]['sortcode']['label'] : __( 'Sort code', 'woocommerce' );
$bacs_accounts = apply_filters( 'woocommerce_bacs_accounts', $this->account_details );
$bacs_accounts = apply_filters( 'woocommerce_bacs_accounts', $this->account_details, $order_id );
if ( ! empty( $bacs_accounts ) ) {
$account_html = '';

View File

@ -312,11 +312,12 @@ abstract class WC_Abstract_Legacy_Order extends WC_Data {
/**
* Get a product (either product or variation).
* @deprecated Add deprecation notices in future release. Replaced with $item->get_product()
* @deprecated 4.4.0
* @param object $item
* @return WC_Product|bool
*/
public function get_product_from_item( $item ) {
wc_deprecated_function( 'WC_Abstract_Legacy_Order::get_product_from_item', '4.4.0', '$item->get_product()' );
if ( is_callable( array( $item, 'get_product' ) ) ) {
$product = $item->get_product();
} else {
@ -461,7 +462,7 @@ abstract class WC_Abstract_Legacy_Order extends WC_Data {
* has_meta function for order items. This is different to the WC_Data
* version and should be removed in future versions.
*
* @deprecated
* @deprecated 3.0
*
* @param int $order_item_id
*

View File

@ -274,14 +274,14 @@ class WC_Legacy_API {
/**
* Rest API Init.
*
* @deprecated since 3.7.0 - REST API clases autoload.
* @deprecated 3.7.0 - REST API clases autoload.
*/
public function rest_api_init() {}
/**
* Include REST API classes.
*
* @deprecated since 3.7.0 - REST API clases autoload.
* @deprecated 3.7.0 - REST API clases autoload.
*/
public function rest_api_includes() {
$this->rest_api_init();
@ -289,9 +289,10 @@ class WC_Legacy_API {
/**
* Register REST API routes.
*
* @deprecated since 3.7.0 - Not used.
* @deprecated 3.7.0
*/
public function register_rest_routes() {
wc_deprecated_function( 'WC_Legacy_API::register_rest_routes', '3.7.0', '' );
$this->register_wp_admin_settings();
}
}

View File

@ -326,7 +326,7 @@ abstract class WC_Legacy_Cart {
/**
* Function to apply discounts to a product and get the discounted price (before tax is applied).
*
* @deprecated Calculation and coupon logic is handled in WC_Cart_Totals.
* @deprecated 3.2.0 Calculation and coupon logic is handled in WC_Cart_Totals.
* @param mixed $values Cart item.
* @param mixed $price Price of item.
* @param bool $add_totals Legacy.
@ -377,17 +377,18 @@ abstract class WC_Legacy_Cart {
/**
* Coupons enabled function. Filterable.
*
* @deprecated 2.5.0 in favor to wc_coupons_enabled()
* @deprecated 2.5.0
* @return bool
*/
public function coupons_enabled() {
wc_deprecated_function( 'WC_Legacy_Cart::coupons_enabled', '2.5.0', 'wc_coupons_enabled' );
return wc_coupons_enabled();
}
/**
* Gets the total (product) discount amount - these are applied before tax.
*
* @deprecated Order discounts (after tax) removed in 2.3 so multiple methods for discounts are no longer required.
* @deprecated 2.3.0 Order discounts (after tax) removed in 2.3 so multiple methods for discounts are no longer required.
* @return mixed formatted price or false if there are none.
*/
public function get_discounts_before_tax() {
@ -403,7 +404,7 @@ abstract class WC_Legacy_Cart {
/**
* Get the total of all order discounts (after tax discounts).
*
* @deprecated Order discounts (after tax) removed in 2.3.
* @deprecated 2.3.0 Order discounts (after tax) removed in 2.3.
* @return int
*/
public function get_order_discount_total() {
@ -414,7 +415,7 @@ abstract class WC_Legacy_Cart {
/**
* Function to apply cart discounts after tax.
*
* @deprecated Coupons can not be applied after tax.
* @deprecated 2.3.0 Coupons can not be applied after tax.
* @param $values
* @param $price
*/
@ -425,7 +426,7 @@ abstract class WC_Legacy_Cart {
/**
* Function to apply product discounts after tax.
*
* @deprecated Coupons can not be applied after tax.
* @deprecated 2.3.0 Coupons can not be applied after tax.
*
* @param $values
* @param $price
@ -437,7 +438,7 @@ abstract class WC_Legacy_Cart {
/**
* Gets the order discount amount - these are applied after tax.
*
* @deprecated Coupons can not be applied after tax.
* @deprecated 2.3.0 Coupons can not be applied after tax.
*/
public function get_discounts_after_tax() {
wc_deprecated_function( 'get_discounts_after_tax', '2.3' );

View File

@ -78,7 +78,7 @@ class WC_Shipping_Legacy_Flat_Rate extends WC_Shipping_Method {
$this->tax_status = $this->get_option( 'tax_status' );
$this->cost = $this->get_option( 'cost' );
$this->type = $this->get_option( 'type', 'class' );
$this->options = $this->get_option( 'options', false ); // @deprecated in 2.4.0
$this->options = $this->get_option( 'options', false ); // @deprecated 2.4.0
}
/**

View File

@ -1015,7 +1015,7 @@ function wc_print_js() {
* @since 2.6.0
* @param string $js JavaScript code.
*/
echo apply_filters( 'woocommerce_queued_js', $js ); // WPCS: XSS ok.
echo apply_filters( 'woocommerce_queued_js', $js ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
unset( $wc_queued_js );
}
@ -1125,11 +1125,11 @@ function flush_rewrite_rules_on_shop_page_save() {
}
// Check if page is edited.
if ( empty( $_GET['post'] ) || empty( $_GET['action'] ) || ( isset( $_GET['action'] ) && 'edit' !== $_GET['action'] ) ) { // WPCS: input var ok, CSRF ok.
if ( empty( $_GET['post'] ) || empty( $_GET['action'] ) || ( isset( $_GET['action'] ) && 'edit' !== $_GET['action'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
return;
}
$post_id = intval( $_GET['post'] ); // WPCS: input var ok, CSRF ok.
$post_id = intval( $_GET['post'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$shop_page_id = wc_get_page_id( 'shop' );
if ( $shop_page_id === $post_id || in_array( $post_id, wc_get_page_children( $shop_page_id ), true ) ) {
@ -1752,10 +1752,12 @@ function wc_uasort_comparison( $a, $b ) {
* @return int
*/
function wc_ascii_uasort_comparison( $a, $b ) {
// phpcs:disable WordPress.PHP.NoSilencedErrors.Discouraged
if ( function_exists( 'iconv' ) && defined( 'ICONV_IMPL' ) && @strcasecmp( ICONV_IMPL, 'unknown' ) !== 0 ) {
$a = @iconv( 'UTF-8', 'ASCII//TRANSLIT//IGNORE', $a );
$b = @iconv( 'UTF-8', 'ASCII//TRANSLIT//IGNORE', $b );
}
// phpcs:enable WordPress.PHP.NoSilencedErrors.Discouraged
return strcmp( $a, $b );
}
@ -1952,10 +1954,10 @@ function wc_print_r( $expression, $return = false ) {
if ( function_exists( $alternative['func'] ) ) {
$res = $alternative['func']( ...$alternative['args'] );
if ( $return ) {
return $res;
return $res; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
echo $res; // WPCS: XSS ok.
echo $res; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
return true;
}
}
@ -2124,7 +2126,8 @@ function wc_make_phone_clickable( $phone ) {
* @return mixed Value sanitized by wc_clean.
*/
function wc_get_post_data_by_key( $key, $default = '' ) {
return wc_clean( wp_unslash( wc_get_var( $_POST[ $key ], $default ) ) ); // @codingStandardsIgnoreLine
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput, WordPress.Security.NonceVerification.Missing
return wc_clean( wp_unslash( wc_get_var( $_POST[ $key ], $default ) ) );
}
/**
@ -2212,19 +2215,21 @@ add_filter( 'auto_update_plugin', 'wc_prevent_dangerous_auto_updates', 99, 2 );
function wc_delete_expired_transients() {
global $wpdb;
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
$sql = "DELETE a, b FROM $wpdb->options a, $wpdb->options b
WHERE a.option_name LIKE %s
AND a.option_name NOT LIKE %s
AND b.option_name = CONCAT( '_transient_timeout_', SUBSTRING( a.option_name, 12 ) )
AND b.option_value < %d";
$rows = $wpdb->query( $wpdb->prepare( $sql, $wpdb->esc_like( '_transient_' ) . '%', $wpdb->esc_like( '_transient_timeout_' ) . '%', time() ) ); // WPCS: unprepared SQL ok.
$rows = $wpdb->query( $wpdb->prepare( $sql, $wpdb->esc_like( '_transient_' ) . '%', $wpdb->esc_like( '_transient_timeout_' ) . '%', time() ) );
$sql = "DELETE a, b FROM $wpdb->options a, $wpdb->options b
WHERE a.option_name LIKE %s
AND a.option_name NOT LIKE %s
AND b.option_name = CONCAT( '_site_transient_timeout_', SUBSTRING( a.option_name, 17 ) )
AND b.option_value < %d";
$rows2 = $wpdb->query( $wpdb->prepare( $sql, $wpdb->esc_like( '_site_transient_' ) . '%', $wpdb->esc_like( '_site_transient_timeout_' ) . '%', time() ) ); // WPCS: unprepared SQL ok.
$rows2 = $wpdb->query( $wpdb->prepare( $sql, $wpdb->esc_like( '_site_transient_' ) . '%', $wpdb->esc_like( '_site_transient_timeout_' ) . '%', time() ) );
// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
return absint( $rows + $rows2 );
}
@ -2393,11 +2398,13 @@ function wc_get_server_database_version() {
);
}
// phpcs:disable WordPress.DB.RestrictedFunctions, PHPCompatibility.Extensions.RemovedExtensions.mysql_DeprecatedRemoved
if ( $wpdb->use_mysqli ) {
$server_info = mysqli_get_server_info( $wpdb->dbh ); // @codingStandardsIgnoreLine.
$server_info = mysqli_get_server_info( $wpdb->dbh );
} else {
$server_info = mysql_get_server_info( $wpdb->dbh ); // @codingStandardsIgnoreLine.
$server_info = mysql_get_server_info( $wpdb->dbh );
}
// phpcs:enable WordPress.DB.RestrictedFunctions, PHPCompatibility.Extensions.RemovedExtensions.mysql_DeprecatedRemoved
return array(
'string' => $server_info,
@ -2433,5 +2440,6 @@ function wc_load_cart() {
* @return bool
*/
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'];
}

View File

@ -885,8 +885,7 @@ function woocommerce_track_product_view() {
}
/**
* @since 2.3
* @deprecated has no replacement
* @deprecated 2.3 has no replacement
*/
function woocommerce_compile_less_styles() {
wc_deprecated_function( 'woocommerce_compile_less_styles', '2.3' );
@ -895,7 +894,7 @@ function woocommerce_compile_less_styles() {
/**
* woocommerce_calc_shipping was an option used to determine if shipping was enabled prior to version 2.6.0. This has since been replaced with wc_shipping_enabled() function and
* the woocommerce_ship_to_countries setting.
* @since 2.6.0
* @deprecated 2.6.0
* @return string
*/
function woocommerce_calc_shipping_backwards_compatibility( $value ) {

View File

@ -822,7 +822,7 @@ function wc_update_total_sales_counts( $order_id ) {
if ( $product_id ) {
$data_store = WC_Data_Store::load( 'product' );
$data_store->update_product_sales( $product_id, absint( $item['qty'] ), 'increase' );
$data_store->update_product_sales( $product_id, absint( $item->get_quantity() ), 'increase' );
}
}
}

View File

@ -18,11 +18,13 @@ defined( 'ABSPATH' ) || exit;
function wc_template_redirect() {
global $wp_query, $wp;
// phpcs:disable WordPress.Security.NonceVerification.Recommended
// When default permalinks are enabled, redirect shop page to post type archive url.
if ( ! empty( $_GET['page_id'] ) && '' === get_option( 'permalink_structure' ) && wc_get_page_id( 'shop' ) === absint( $_GET['page_id'] ) && get_post_type_archive_link( 'product' ) ) { // WPCS: input var ok, CSRF ok.
if ( ! empty( $_GET['page_id'] ) && '' === get_option( 'permalink_structure' ) && wc_get_page_id( 'shop' ) === absint( $_GET['page_id'] ) && get_post_type_archive_link( 'product' ) ) {
wp_safe_redirect( get_post_type_archive_link( 'product' ) );
exit;
}
// phpcs:enable WordPress.Security.NonceVerification.Recommended
// When on the checkout with an empty cart, redirect to cart page.
if ( is_page( wc_get_page_id( 'checkout' ) ) && wc_get_page_id( 'checkout' ) !== wc_get_page_id( 'cart' ) && WC()->cart->is_empty() && empty( $wp->query_vars['order-pay'] ) && ! isset( $wp->query_vars['order-received'] ) && ! is_customize_preview() && apply_filters( 'woocommerce_checkout_redirect_empty_cart', true ) ) {
@ -33,7 +35,7 @@ function wc_template_redirect() {
}
// Logout.
if ( isset( $wp->query_vars['customer-logout'] ) && ! empty( $_REQUEST['_wpnonce'] ) && wp_verify_nonce( sanitize_key( $_REQUEST['_wpnonce'] ), 'customer-logout' ) ) { // WPCS: input var ok, CSRF ok.
if ( isset( $wp->query_vars['customer-logout'] ) && ! empty( $_REQUEST['_wpnonce'] ) && wp_verify_nonce( sanitize_key( $_REQUEST['_wpnonce'] ), 'customer-logout' ) ) {
wp_safe_redirect( str_replace( '&amp;', '&', wp_logout_url( wc_get_page_permalink( 'myaccount' ) ) ) );
exit;
}
@ -96,9 +98,11 @@ add_action( 'template_redirect', 'wc_send_frame_options_header' );
* @since 2.5.3
*/
function wc_prevent_endpoint_indexing() {
if ( is_wc_endpoint_url() || isset( $_GET['download_file'] ) ) { // WPCS: input var ok, CSRF ok.
@header( 'X-Robots-Tag: noindex' ); // @codingStandardsIgnoreLine
// phpcs:disable WordPress.Security.NonceVerification.Recommended, WordPress.PHP.NoSilencedErrors.Discouraged
if ( is_wc_endpoint_url() || isset( $_GET['download_file'] ) ) {
@header( 'X-Robots-Tag: noindex' );
}
// phpcs:enable WordPress.Security.NonceVerification.Recommended, WordPress.PHP.NoSilencedErrors.Discouraged
}
add_action( 'template_redirect', 'wc_prevent_endpoint_indexing' );
@ -232,6 +236,29 @@ function wc_set_loop_prop( $prop, $value = '' ) {
$GLOBALS['woocommerce_loop'][ $prop ] = $value;
}
/**
* Set the current visbility for a product in the woocommerce_loop global.
*
* @since 4.4.0
* @param int $product_id Product it to cache visbiility for.
* @param bool $value The poduct visibility value to cache.
*/
function wc_set_loop_product_visibility( $product_id, $value ) {
wc_set_loop_prop( "product_visibility_$product_id", $value );
}
/**
* Gets the cached current visibility for a product from the woocommerce_loop global.
*
* @since 4.4.0
* @param int $product_id Product id to get the cached visibility for.
*
* @return bool|null The cached product visibility, or null if on visibility has been cached for that product.
*/
function wc_get_loop_product_visibility( $product_id ) {
return wc_get_loop_prop( "product_visibility_$product_id", null );
}
/**
* Should the WooCommerce loop be displayed?
*
@ -704,7 +731,8 @@ function wc_product_class( $class = '', $product_id = null ) {
*/
function wc_query_string_form_fields( $values = null, $exclude = array(), $current_key = '', $return = false ) {
if ( is_null( $values ) ) {
$values = $_GET; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$values = $_GET;
} elseif ( is_string( $values ) ) {
$url_parts = wp_parse_url( $values );
$values = array();
@ -1022,7 +1050,8 @@ if ( ! function_exists( 'woocommerce_demo_store' ) ) {
$notice_id = md5( $notice );
echo apply_filters( 'woocommerce_demo_store', '<p class="woocommerce-store-notice demo_store" data-notice-id="' . esc_attr( $notice_id ) . '" style="display:none;">' . wp_kses_post( $notice ) . ' <a href="#" class="woocommerce-store-notice__dismiss-link">' . esc_html__( 'Dismiss', 'woocommerce' ) . '</a></p>', $notice ); // WPCS: XSS ok.
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo apply_filters( 'woocommerce_demo_store', '<p class="woocommerce-store-notice demo_store" data-notice-id="' . esc_attr( $notice_id ) . '" style="display:none;">' . wp_kses_post( $notice ) . ' <a href="#" class="woocommerce-store-notice__dismiss-link">' . esc_html__( 'Dismiss', 'woocommerce' ) . '</a></p>', $notice );
}
}
@ -1062,7 +1091,8 @@ if ( ! function_exists( 'woocommerce_page_title' ) ) {
$page_title = apply_filters( 'woocommerce_page_title', $page_title );
if ( $echo ) {
echo $page_title; // WPCS: XSS ok.
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $page_title;
} else {
return $page_title;
}
@ -1087,7 +1117,8 @@ if ( ! function_exists( 'woocommerce_product_loop_start' ) ) {
$loop_start = apply_filters( 'woocommerce_product_loop_start', ob_get_clean() );
if ( $echo ) {
echo $loop_start; // WPCS: XSS ok.
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $loop_start;
} else {
return $loop_start;
}
@ -1110,7 +1141,8 @@ if ( ! function_exists( 'woocommerce_product_loop_end' ) ) {
$loop_end = apply_filters( 'woocommerce_product_loop_end', ob_get_clean() );
if ( $echo ) {
echo $loop_end; // WPCS: XSS ok.
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $loop_end;
} else {
return $loop_end;
}
@ -1139,7 +1171,8 @@ if ( ! function_exists( 'woocommerce_template_loop_category_title' ) ) {
echo esc_html( $category->name );
if ( $category->count > 0 ) {
echo apply_filters( 'woocommerce_subcategory_count_html', ' <mark class="count">(' . esc_html( $category->count ) . ')</mark>', $category ); // WPCS: XSS ok.
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo apply_filters( 'woocommerce_subcategory_count_html', ' <mark class="count">(' . esc_html( $category->count ) . ')</mark>', $category );
}
?>
</h2>
@ -1199,7 +1232,8 @@ if ( ! function_exists( 'woocommerce_taxonomy_archive_description' ) ) {
$term = get_queried_object();
if ( $term && ! empty( $term->description ) ) {
echo '<div class="term-description">' . wc_format_content( $term->description ) . '</div>'; // WPCS: XSS ok.
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<div class="term-description">' . wc_format_content( $term->description ) . '</div>';
}
}
}
@ -1220,7 +1254,8 @@ if ( ! function_exists( 'woocommerce_product_archive_description' ) ) {
if ( $shop_page ) {
$description = wc_format_content( $shop_page->post_content );
if ( $description ) {
echo '<div class="page-description">' . $description . '</div>'; // WPCS: XSS ok.
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<div class="page-description">' . $description . '</div>';
}
}
}
@ -1276,7 +1311,8 @@ if ( ! function_exists( 'woocommerce_template_loop_product_thumbnail' ) ) {
* Get the product thumbnail for the loop.
*/
function woocommerce_template_loop_product_thumbnail() {
echo woocommerce_get_product_thumbnail(); // WPCS: XSS ok.
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo woocommerce_get_product_thumbnail();
}
}
if ( ! function_exists( 'woocommerce_template_loop_price' ) ) {
@ -1368,7 +1404,9 @@ if ( ! function_exists( 'woocommerce_catalog_ordering' ) ) {
);
$default_orderby = wc_get_loop_prop( 'is_search' ) ? 'relevance' : apply_filters( 'woocommerce_default_catalog_orderby', get_option( 'woocommerce_default_catalog_orderby', '' ) );
$orderby = isset( $_GET['orderby'] ) ? wc_clean( wp_unslash( $_GET['orderby'] ) ) : $default_orderby; // WPCS: sanitization ok, input var ok, CSRF ok.
// phpcs:disable WordPress.Security.NonceVerification.Recommended
$orderby = isset( $_GET['orderby'] ) ? wc_clean( wp_unslash( $_GET['orderby'] ) ) : $default_orderby;
// phpcs:enable WordPress.Security.NonceVerification.Recommended
if ( wc_get_loop_prop( 'is_search' ) ) {
$catalog_orderby_options = array_merge( array( 'relevance' => __( 'Relevance', 'woocommerce' ) ), $catalog_orderby_options );
@ -1700,7 +1738,8 @@ if ( ! function_exists( 'woocommerce_quantity_input' ) ) {
wc_get_template( 'global/quantity-input.php', $args );
if ( $echo ) {
echo ob_get_clean(); // WPCS: XSS ok.
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo ob_get_clean();
} else {
return ob_get_clean();
}
@ -1780,7 +1819,8 @@ if ( ! function_exists( 'woocommerce_sort_product_tabs' ) ) {
// Make sure the $tabs parameter is an array.
if ( ! is_array( $tabs ) ) {
trigger_error( 'Function woocommerce_sort_product_tabs() expects an array as the first parameter. Defaulting to empty array.' ); // @codingStandardsIgnoreLine
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
trigger_error( 'Function woocommerce_sort_product_tabs() expects an array as the first parameter. Defaulting to empty array.' );
$tabs = array();
}
@ -1817,7 +1857,8 @@ if ( ! function_exists( 'woocommerce_comments' ) ) {
* @param int $depth Depth.
*/
function woocommerce_comments( $comment, $args, $depth ) {
$GLOBALS['comment'] = $comment; // WPCS: override ok.
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
$GLOBALS['comment'] = $comment;
wc_get_template(
'single-product/review.php',
array(
@ -2443,7 +2484,8 @@ if ( ! function_exists( 'woocommerce_output_product_categories' ) ) {
return false;
}
echo $args['before']; // WPCS: XSS ok.
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $args['before'];
foreach ( $product_categories as $category ) {
wc_get_template(
@ -2454,7 +2496,8 @@ if ( ! function_exists( 'woocommerce_output_product_categories' ) ) {
);
}
echo $args['after']; // WPCS: XSS ok.
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $args['after'];
return true;
}
@ -2839,7 +2882,8 @@ if ( ! function_exists( 'woocommerce_form_field' ) ) {
if ( $args['return'] ) {
return $field;
} else {
echo $field; // WPCS: XSS ok.
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $field;
}
}
}
@ -2882,7 +2926,8 @@ if ( ! function_exists( 'get_product_search_form' ) ) {
return $form;
}
echo $form; // WPCS: XSS ok.
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $form;
}
}
@ -2951,8 +2996,10 @@ if ( ! function_exists( 'wc_dropdown_variation_attribute_options' ) ) {
// Get selected value.
if ( false === $args['selected'] && $args['attribute'] && $args['product'] instanceof WC_Product ) {
$selected_key = 'attribute_' . sanitize_title( $args['attribute'] );
$args['selected'] = isset( $_REQUEST[ $selected_key ] ) ? wc_clean( wp_unslash( $_REQUEST[ $selected_key ] ) ) : $args['product']->get_variation_default_attribute( $args['attribute'] ); // WPCS: input var ok, CSRF ok, sanitization ok.
$selected_key = 'attribute_' . sanitize_title( $args['attribute'] );
// phpcs:disable WordPress.Security.NonceVerification.Recommended
$args['selected'] = isset( $_REQUEST[ $selected_key ] ) ? wc_clean( wp_unslash( $_REQUEST[ $selected_key ] ) ) : $args['product']->get_variation_default_attribute( $args['attribute'] );
// phpcs:enable WordPress.Security.NonceVerification.Recommended
}
$options = $args['options'];
@ -2999,7 +3046,8 @@ if ( ! function_exists( 'wc_dropdown_variation_attribute_options' ) ) {
$html .= '</select>';
echo apply_filters( 'woocommerce_dropdown_variation_attribute_options_html', $html, $args ); // WPCS: XSS ok.
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo apply_filters( 'woocommerce_dropdown_variation_attribute_options_html', $html, $args );
}
}
@ -3236,7 +3284,8 @@ if ( ! function_exists( 'wc_display_item_meta' ) ) {
$html = apply_filters( 'woocommerce_display_item_meta', $html, $item, $args );
if ( $args['echo'] ) {
echo $html; // WPCS: XSS ok.
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $html;
} else {
return $html;
}
@ -3290,7 +3339,8 @@ if ( ! function_exists( 'wc_display_item_downloads' ) ) {
$html = apply_filters( 'woocommerce_display_item_downloads', $html, $item, $args );
if ( $args['echo'] ) {
echo $html; // WPCS: XSS ok.
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $html;
} else {
return $html;
}
@ -3698,3 +3748,5 @@ function wc_get_pay_buttons() {
}
echo '</div>';
}
// phpcs:enable Generic.Commenting.Todo.TaskFound

View File

@ -28,6 +28,7 @@ function wc_update_200_file_paths() {
$old_file_path = trim( $existing_file_path->meta_value );
if ( ! empty( $old_file_path ) ) {
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
$file_paths = serialize( array( md5( $old_file_path ) => $old_file_path ) );
$wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->postmeta} SET meta_key = '_file_paths', meta_value = %s WHERE meta_id = %d", $file_paths, $existing_file_path->meta_id ) );
@ -53,11 +54,11 @@ function wc_update_200_permalinks() {
$base_slug = $shop_page_id > 0 && get_post( $shop_page_id ) ? get_page_uri( $shop_page_id ) : 'shop';
$category_base = get_option( 'woocommerce_prepend_shop_page_to_urls' ) == 'yes' ? trailingslashit( $base_slug ) : '';
$category_base = 'yes' === get_option( 'woocommerce_prepend_shop_page_to_urls' ) ? trailingslashit( $base_slug ) : '';
$category_slug = get_option( 'woocommerce_product_category_slug' ) ? get_option( 'woocommerce_product_category_slug' ) : _x( 'product-category', 'slug', 'woocommerce' );
$tag_slug = get_option( 'woocommerce_product_tag_slug' ) ? get_option( 'woocommerce_product_tag_slug' ) : _x( 'product-tag', 'slug', 'woocommerce' );
if ( 'yes' == get_option( 'woocommerce_prepend_shop_page_to_products' ) ) {
if ( 'yes' === get_option( 'woocommerce_prepend_shop_page_to_products' ) ) {
$product_base = trailingslashit( $base_slug );
} else {
$product_slug = get_option( 'woocommerce_product_slug' );
@ -68,7 +69,7 @@ function wc_update_200_permalinks() {
}
}
if ( get_option( 'woocommerce_prepend_category_to_products' ) == 'yes' ) {
if ( 'yes' === get_option( 'woocommerce_prepend_category_to_products' ) ) {
$product_base .= trailingslashit( '%product_cat%' );
}
@ -90,16 +91,16 @@ function wc_update_200_permalinks() {
*/
function wc_update_200_subcat_display() {
// Update subcat display settings.
if ( get_option( 'woocommerce_shop_show_subcategories' ) == 'yes' ) {
if ( get_option( 'woocommerce_hide_products_when_showing_subcategories' ) == 'yes' ) {
if ( 'yes' === get_option( 'woocommerce_shop_show_subcategories' ) ) {
if ( 'yes' === get_option( 'woocommerce_hide_products_when_showing_subcategories' ) ) {
update_option( 'woocommerce_shop_page_display', 'subcategories' );
} else {
update_option( 'woocommerce_shop_page_display', 'both' );
}
}
if ( get_option( 'woocommerce_show_subcategories' ) == 'yes' ) {
if ( get_option( 'woocommerce_hide_products_when_showing_subcategories' ) == 'yes' ) {
if ( 'yes' === get_option( 'woocommerce_show_subcategories' ) ) {
if ( 'yes' === get_option( 'woocommerce_hide_products_when_showing_subcategories' ) ) {
update_option( 'woocommerce_category_archive_display', 'subcategories' );
} else {
update_option( 'woocommerce_category_archive_display', 'both' );
@ -128,7 +129,7 @@ function wc_update_200_taxrates() {
foreach ( $states as $state ) {
if ( '*' == $state ) {
if ( '*' === $state ) {
$state = '';
}
@ -160,7 +161,7 @@ function wc_update_200_taxrates() {
$location_type = ( 'postcode' === $tax_rate['location_type'] ) ? 'postcode' : 'city';
if ( '*' == $tax_rate['state'] ) {
if ( '*' === $tax_rate['state'] ) {
$tax_rate['state'] = '';
}
@ -246,7 +247,7 @@ function wc_update_200_line_items() {
)
);
// Add line item meta.
// Add line item meta.
if ( $item_id ) {
wc_add_order_item_meta( $item_id, '_qty', absint( $order_item['qty'] ) );
wc_add_order_item_meta( $item_id, '_tax_class', $order_item['tax_class'] );
@ -324,7 +325,7 @@ function wc_update_200_line_items() {
)
);
// Add line item meta.
// Add line item meta.
if ( $item_id ) {
wc_add_order_item_meta( $item_id, 'compound', absint( isset( $order_tax['compound'] ) ? $order_tax['compound'] : 0 ) );
wc_add_order_item_meta( $item_id, 'tax_amount', wc_clean( $order_tax['cart_tax'] ) );
@ -393,6 +394,8 @@ function wc_update_200_db_version() {
function wc_update_209_brazillian_state() {
global $wpdb;
// phpcs:disable WordPress.DB.SlowDBQuery
// Update brazillian state codes.
$wpdb->update(
$wpdb->postmeta,
@ -434,6 +437,8 @@ function wc_update_209_brazillian_state() {
'meta_value' => 'BH',
)
);
// phpcs:enable WordPress.DB.SlowDBQuery
}
/**
@ -492,6 +497,7 @@ function wc_update_210_file_paths() {
}
}
if ( $needs_update ) {
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
$new_value = serialize( $new_value );
$wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->postmeta} SET meta_key = %s, meta_value = %s WHERE meta_id = %d", '_downloadable_files', $new_value, $existing_file_path->meta_id ) );
@ -857,6 +863,8 @@ function wc_update_240_api_keys() {
* @return void
*/
function wc_update_240_webhooks() {
// phpcs:disable WordPress.DB.SlowDBQuery
/**
* Webhooks.
* Make sure order.update webhooks get the woocommerce_order_edit_status hook.
@ -873,6 +881,8 @@ function wc_update_240_webhooks() {
$webhook = new WC_Webhook( $order_update_webhook->ID );
$webhook->set_topic( 'order.updated' );
}
// phpcs:enable WordPress.DB.SlowDBQuery
}
/**
@ -993,6 +1003,8 @@ function wc_update_250_currency() {
update_option( 'woocommerce_currency', 'LAK' );
}
// phpcs:disable WordPress.DB.SlowDBQuery
// Update LAK currency code.
$wpdb->update(
$wpdb->postmeta,
@ -1005,6 +1017,7 @@ function wc_update_250_currency() {
)
);
// phpcs:enable WordPress.DB.SlowDBQuery
}
/**
@ -1184,6 +1197,8 @@ function wc_update_260_db_version() {
* @return void
*/
function wc_update_300_webhooks() {
// phpcs:disable WordPress.DB.SlowDBQuery
/**
* Make sure product.update webhooks get the woocommerce_product_quick_edit_save
* and woocommerce_product_bulk_edit_save hooks.
@ -1200,6 +1215,8 @@ function wc_update_300_webhooks() {
$webhook = new WC_Webhook( $product_update_webhook->ID );
$webhook->set_topic( 'product.updated' );
}
// phpcs:enable WordPress.DB.SlowDBQuery
}
/**
@ -1601,7 +1618,7 @@ function wc_update_330_product_stock_status() {
AND t3.meta_key = '_backorders' AND ( t3.meta_value = 'yes' OR t3.meta_value = 'notify' )",
$min_stock_amount
)
); // WPCS: db call ok, unprepared SQL ok, cache ok.
);
if ( empty( $post_ids ) ) {
return;
@ -1609,12 +1626,14 @@ function wc_update_330_product_stock_status() {
$post_ids = array_map( 'absint', $post_ids );
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
// Set the status to onbackorder for those products.
$wpdb->query(
"UPDATE $wpdb->postmeta
SET meta_value = 'onbackorder'
WHERE meta_key = '_stock_status' AND post_id IN ( " . implode( ',', $post_ids ) . ' )'
); // WPCS: db call ok, unprepared SQL ok, cache ok.
);
// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
}
/**
@ -2065,7 +2084,8 @@ function wc_update_390_move_maxmind_database() {
$new_path = apply_filters( 'woocommerce_geolocation_local_database_path', $new_path, 2 );
$new_path = apply_filters( 'woocommerce_maxmind_geolocation_database_path', $new_path );
@rename( $old_path, $new_path ); // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
@rename( $old_path, $new_path );
}
/**
@ -2110,3 +2130,48 @@ function wc_update_400_reset_action_scheduler_migration_status() {
function wc_update_400_db_version() {
WC_Install::update_db_version( '4.0.0' );
}
/**
* Register attributes as terms for variable products, in increments of 100 products.
*
* @return bool true if there are more products to process.
*/
function wc_update_440_insert_attribute_terms_for_variable_products() {
$state_option_name = 'woocommerce_' . __FUNCTION__ . '_state';
$page = intval( get_option( $state_option_name, 1 ) );
$products = wc_get_products(
array(
'type' => 'variable',
'limit' => 100,
'page' => $page,
)
);
if ( empty( $products ) ) {
delete_option( $state_option_name );
return false;
}
$attribute_taxonomy_names = wc_get_attribute_taxonomy_names();
foreach ( $products as $product ) {
$variation_ids = $product->get_children();
foreach ( $variation_ids as $variation_id ) {
$variation = wc_get_product( $variation_id );
$variation_attributes = $variation->get_attributes();
foreach ( $variation_attributes as $attr_name => $attr_value ) {
wp_set_post_terms( $variation_id, array( $attr_value ), $attr_name );
}
$attributes_to_delete = array_diff( $attribute_taxonomy_names, array_keys( $variation_attributes ) );
wp_delete_object_term_relationships( $variation_id, $attributes_to_delete );
}
}
return update_option( $state_option_name, $page + 1 );
}
/**
* Update DB version.
*/
function wc_update_440_db_version() {
WC_Install::update_db_version( '4.4.0' );
}

View File

@ -9,21 +9,40 @@
defined( 'ABSPATH' ) || exit;
/**
* Process the synchronous web hooks at the end of the request.
* Process the web hooks at the end of the request.
*
* @since 4.4.0
*/
function wc_webhook_execute_synchronous_queue() {
global $wc_queued_sync_webhooks;
if ( empty( $wc_queued_sync_webhooks ) ) {
function wc_webhook_execute_queue() {
global $wc_queued_webhooks;
if ( empty( $wc_queued_webhooks ) ) {
return;
}
foreach ( $wc_queued_sync_webhooks as $data ) {
$data['webhook']->deliver( $data['arg'] );
foreach ( $wc_queued_webhooks as $data ) {
// Webhooks are processed in the background by default
// so as to avoid delays or failures in delivery from affecting the
// user who triggered it.
if ( apply_filters( 'woocommerce_webhook_deliver_async', true, $data['webhook'], $data['arg'] ) ) {
$queue_args = array(
'webhook_id' => $data['webhook']->get_id(),
'arg' => $data['arg'],
);
$next_scheduled_date = WC()->queue()->get_next( 'woocommerce_deliver_webhook_async', $queue_args, 'woocommerce-webhooks' );
// Make webhooks unique - only schedule one webhook every 10 minutes to maintain backward compatibility with WP Cron behaviour seen in WC < 3.5.0.
if ( is_null( $next_scheduled_date ) || $next_scheduled_date->getTimestamp() >= ( 600 + gmdate( 'U' ) ) ) {
WC()->queue()->add( 'woocommerce_deliver_webhook_async', $queue_args, 'woocommerce-webhooks' );
}
} else {
// Deliver immediately.
$data['webhook']->deliver( $data['arg'] );
}
}
}
register_shutdown_function( 'wc_webhook_execute_synchronous_queue' );
add_action( 'shutdown', 'wc_webhook_execute_queue' );
/**
* Process webhook delivery.
@ -33,34 +52,15 @@ register_shutdown_function( 'wc_webhook_execute_synchronous_queue' );
* @param array $arg Delivery arguments.
*/
function wc_webhook_process_delivery( $webhook, $arg ) {
// Webhooks are processed in the background by default
// so as to avoid delays or failures in delivery from affecting the
// user who triggered it.
if ( apply_filters( 'woocommerce_webhook_deliver_async', true, $webhook, $arg ) ) {
$queue_args = array(
'webhook_id' => $webhook->get_id(),
'arg' => $arg,
);
$next_scheduled_date = WC()->queue()->get_next( 'woocommerce_deliver_webhook_async', $queue_args, 'woocommerce-webhooks' );
// Make webhooks unique - only schedule one webhook every 10 minutes to maintain backward compatibility with WP Cron behaviour seen in WC < 3.5.0.
if ( is_null( $next_scheduled_date ) || $next_scheduled_date->getTimestamp() >= ( 600 + gmdate( 'U' ) ) ) {
WC()->queue()->add( 'woocommerce_deliver_webhook_async', $queue_args, 'woocommerce-webhooks' );
}
} else {
// We need to queue the webhook so that it can be ran after the request has finished processing.
// This must be done in order to keep parity with how they are executed asynchronously.
global $wc_queued_sync_webhooks;
if ( ! isset( $wc_queued_sync_webhooks ) ) {
$wc_queued_sync_webhooks = array();
}
$wc_queued_sync_webhooks[] = array(
'webhook' => $webhook,
'arg' => $arg,
);
// We need to queue the webhook so that it can be ran after the request has finished processing.
global $wc_queued_webhooks;
if ( ! isset( $wc_queued_webhooks ) ) {
$wc_queued_webhooks = array();
}
$wc_queued_webhooks[] = array(
'webhook' => $webhook,
'arg' => $arg,
);
}
add_action( 'woocommerce_webhook_process_delivery', 'wc_webhook_process_delivery', 10, 2 );

View File

@ -344,49 +344,77 @@ class WC_Widget_Layered_Nav extends WC_Widget {
protected function get_filtered_term_product_counts( $term_ids, $taxonomy, $query_type ) {
global $wpdb;
$tax_query = WC_Query::get_main_tax_query();
$meta_query = WC_Query::get_main_meta_query();
$main_tax_query = $this->get_main_tax_query();
$meta_query = $this->get_main_meta_query();
if ( 'or' === $query_type ) {
foreach ( $tax_query as $key => $query ) {
if ( is_array( $query ) && $taxonomy === $query['taxonomy'] ) {
unset( $tax_query[ $key ] );
$non_variable_tax_query_sql = array( 'where' => '' );
$is_and_query = 'and' === $query_type;
foreach ( $main_tax_query as $key => $query ) {
if ( is_array( $query ) && $taxonomy === $query['taxonomy'] ) {
if ( $is_and_query ) {
$non_variable_tax_query_sql = $this->convert_tax_query_to_sql( array( $query ) );
}
unset( $main_tax_query[ $key ] );
}
}
$meta_query = new WP_Meta_Query( $meta_query );
$tax_query = new WP_Tax_Query( $tax_query );
$meta_query_sql = $meta_query->get_sql( 'post', $wpdb->posts, 'ID' );
$tax_query_sql = $tax_query->get_sql( $wpdb->posts, 'ID' );
$exclude_variable_products_tax_query_sql = $this->get_extra_tax_query_sql( 'product_type', array( 'variable' ), 'NOT IN' );
$meta_query_sql = ( new WP_Meta_Query( $meta_query ) )->get_sql( 'post', $wpdb->posts, 'ID' );
$main_tax_query_sql = $this->convert_tax_query_to_sql( $main_tax_query );
$term_ids_sql = '(' . implode( ',', array_map( 'absint', $term_ids ) ) . ')';
// Generate query.
$query = array();
$query['select'] = "SELECT COUNT( DISTINCT {$wpdb->posts}.ID ) as term_count, terms.term_id as term_count_id";
$query['from'] = "FROM {$wpdb->posts}";
$query['join'] = "
INNER JOIN {$wpdb->term_relationships} AS term_relationships ON {$wpdb->posts}.ID = term_relationships.object_id
INNER JOIN {$wpdb->term_relationships} AS tr ON {$wpdb->posts}.ID = tr.object_id
INNER JOIN {$wpdb->term_taxonomy} AS term_taxonomy USING( term_taxonomy_id )
INNER JOIN {$wpdb->terms} AS terms USING( term_id )
" . $tax_query_sql['join'] . $meta_query_sql['join'];
{$main_tax_query_sql['join']} {$meta_query_sql['join']}"; // Not an omission, really no more JOINs required.
$variable_where_part = "
OR ({$wpdb->posts}.post_type = 'product_variation'
AND NOT EXISTS (
SELECT ID FROM {$wpdb->posts} AS parent
WHERE parent.ID = {$wpdb->posts}.post_parent AND parent.post_status NOT IN ('publish')
))
";
$search_sql = '';
$search = $this->get_main_search_query_sql();
if ( $search ) {
$search_sql = ' AND ' . $search;
}
$query['where'] = "
WHERE {$wpdb->posts}.post_type IN ( 'product' )
AND {$wpdb->posts}.post_status = 'publish'"
. $tax_query_sql['where'] . $meta_query_sql['where'] .
'AND terms.term_id IN (' . implode( ',', array_map( 'absint', $term_ids ) ) . ')';
WHERE
{$wpdb->posts}.post_status = 'publish'
{$main_tax_query_sql['where']} {$meta_query_sql['where']}
AND (
(
{$wpdb->posts}.post_type = 'product'
{$exclude_variable_products_tax_query_sql['where']}
{$non_variable_tax_query_sql['where']}
)
{$variable_where_part}
)
AND terms.term_id IN {$term_ids_sql}
{$search_sql}";
$search = WC_Query::get_main_search_query_sql();
$search = $this->get_main_search_query_sql();
if ( $search ) {
$query['where'] .= ' AND ' . $search;
}
$query['group_by'] = 'GROUP BY terms.term_id';
$query = apply_filters( 'woocommerce_get_filtered_term_product_counts_query', $query );
$query = implode( ' ', $query );
$query_sql = implode( ' ', $query );
// We have a query - let's see if cached results of this query already exist.
$query_hash = md5( $query );
$query_hash = md5( $query_sql );
// Maybe store a transient of the count values.
$cache = apply_filters( 'woocommerce_layered_nav_count_maybe_cache', true );
@ -397,17 +425,88 @@ class WC_Widget_Layered_Nav extends WC_Widget {
}
if ( ! isset( $cached_counts[ $query_hash ] ) ) {
$results = $wpdb->get_results( $query, ARRAY_A ); // @codingStandardsIgnoreLine
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
$results = $wpdb->get_results( $query_sql, ARRAY_A );
$counts = array_map( 'absint', wp_list_pluck( $results, 'term_count', 'term_count_id' ) );
$cached_counts[ $query_hash ] = $counts;
if ( true === $cache ) {
set_transient( 'wc_layered_nav_counts_' . sanitize_title( $taxonomy ), $cached_counts, DAY_IN_SECONDS );
}
// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
}
return array_map( 'absint', (array) $cached_counts[ $query_hash ] );
}
/**
* Wrapper for WC_Query::get_main_tax_query() to ease unit testing.
*
* @since 4.4.0
* @return array
*/
protected function get_main_tax_query() {
return WC_Query::get_main_tax_query();
}
/**
* Wrapper for WC_Query::get_main_search_query_sql() to ease unit testing.
*
* @since 4.4.0
* @return string
*/
protected function get_main_search_query_sql() {
return WC_Query::get_main_search_query_sql();
}
/**
* Wrapper for WC_Query::get_main_search_queryget_main_meta_query to ease unit testing.
*
* @since 4.4.0
* @return array
*/
protected function get_main_meta_query() {
return WC_Query::get_main_meta_query();
}
/**
* Get a tax query SQL for a given set of taxonomy, terms and operator.
* Uses an intermediate WP_Tax_Query object.
*
* @since 4.4.0
* @param string $taxonomy Taxonomy name.
* @param array $terms Terms to include in the query.
* @param string $operator Query operator, as supported by WP_Tax_Query; e.g. "NOT IN".
*
* @return array
*/
private function get_extra_tax_query_sql( $taxonomy, $terms, $operator ) {
$query = array(
array(
'taxonomy' => $taxonomy,
'field' => 'slug',
'terms' => $terms,
'operator' => $operator,
'include_children' => false,
),
);
return $this->convert_tax_query_to_sql( $query );
}
/**
* Convert a tax query array to SQL using an intermediate WP_Tax_Query object.
*
* @since 4.4.0
* @param array $query Query array in the same format accepted by WP_Tax_Query constructor.
*
* @return array Query SQL as returned by WP_Tax_Query->get_sql.
*/
private function convert_tax_query_to_sql( $query ) {
global $wpdb;
return ( new WP_Tax_Query( $query ) )->get_sql( $wpdb->posts, 'ID' );
}
/**
* Show list based layered nav.
*
@ -442,8 +541,9 @@ class WC_Widget_Layered_Nav extends WC_Widget {
continue;
}
$filter_name = 'filter_' . wc_attribute_taxonomy_slug( $taxonomy );
$current_filter = isset( $_GET[ $filter_name ] ) ? explode( ',', wc_clean( wp_unslash( $_GET[ $filter_name ] ) ) ) : array(); // WPCS: input var ok, CSRF ok.
$filter_name = 'filter_' . wc_attribute_taxonomy_slug( $taxonomy );
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$current_filter = isset( $_GET[ $filter_name ] ) ? explode( ',', wc_clean( wp_unslash( $_GET[ $filter_name ] ) ) ) : array();
$current_filter = array_map( 'sanitize_title', $current_filter );
if ( ! in_array( $term->slug, $current_filter, true ) ) {
@ -487,7 +587,8 @@ class WC_Widget_Layered_Nav extends WC_Widget {
$term_html .= ' ' . apply_filters( 'woocommerce_layered_nav_count', '<span class="count">(' . absint( $count ) . ')</span>', $count, $term );
echo '<li class="woocommerce-widget-layered-nav-list__item wc-layered-nav-term ' . ( $option_is_set ? 'woocommerce-widget-layered-nav-list__item--chosen chosen' : '' ) . '">';
echo apply_filters( 'woocommerce_layered_nav_term_html', $term_html, $term, $link, $count ); // WPCS: XSS ok.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.EscapeOutput.OutputNotEscaped
echo apply_filters( 'woocommerce_layered_nav_term_html', $term_html, $term, $link, $count );
echo '</li>';
}

48
package-lock.json generated
View File

@ -7663,9 +7663,9 @@
}
},
"@types/json-schema": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.4.tgz",
"integrity": "sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA==",
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.5.tgz",
"integrity": "sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ==",
"dev": true
},
"@types/mime-types": {
@ -7764,21 +7764,21 @@
"dev": true
},
"@typescript-eslint/experimental-utils": {
"version": "2.26.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.26.0.tgz",
"integrity": "sha512-RELVoH5EYd+JlGprEyojUv9HeKcZqF7nZUGSblyAw1FwOGNnmQIU8kxJ69fttQvEwCsX5D6ECJT8GTozxrDKVQ==",
"version": "2.34.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.34.0.tgz",
"integrity": "sha512-eS6FTkq+wuMJ+sgtuNTtcqavWXqsflWcfBnlYhg/nS4aZ1leewkXGbvBhaapn1q6qf4M71bsR1tez5JTRMuqwA==",
"dev": true,
"requires": {
"@types/json-schema": "^7.0.3",
"@typescript-eslint/typescript-estree": "2.26.0",
"@typescript-eslint/typescript-estree": "2.34.0",
"eslint-scope": "^5.0.0",
"eslint-utils": "^2.0.0"
},
"dependencies": {
"eslint-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.0.0.tgz",
"integrity": "sha512-0HCPuJv+7Wv1bACm8y5/ECVfYdfsAm9xmVb7saeFlxjPYALefjhbYoCkBjPdPzGH8wWyTpAez82Fh3VKYEZ8OA==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz",
"integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==",
"dev": true,
"requires": {
"eslint-visitor-keys": "^1.1.0"
@ -7787,9 +7787,9 @@
}
},
"@typescript-eslint/typescript-estree": {
"version": "2.26.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.26.0.tgz",
"integrity": "sha512-3x4SyZCLB4zsKsjuhxDLeVJN6W29VwBnYpCsZ7vIdPel9ZqLfIZJgJXO47MNUkurGpQuIBALdPQKtsSnWpE1Yg==",
"version": "2.34.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.34.0.tgz",
"integrity": "sha512-OMAr+nJWKdlVM9LOqCqh3pQQPwxHAN7Du8DR6dmwCrAmxtiXQnhHJ6tBNtf+cggqfo51SG/FCwnKhXCIM7hnVg==",
"dev": true,
"requires": {
"debug": "^4.1.1",
@ -7797,7 +7797,7 @@
"glob": "^7.1.6",
"is-glob": "^4.0.1",
"lodash": "^4.17.15",
"semver": "^6.3.0",
"semver": "^7.3.2",
"tsutils": "^3.17.1"
},
"dependencies": {
@ -7825,9 +7825,9 @@
}
},
"lodash": {
"version": "4.17.15",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
"version": "4.17.19",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
"integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==",
"dev": true
},
"ms": {
@ -7837,9 +7837,9 @@
"dev": true
},
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"version": "7.3.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz",
"integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==",
"dev": true
}
}
@ -8025,7 +8025,7 @@
"dev": true,
"requires": {
"@wordpress/e2e-test-utils": "^4.6.0",
"@wordpress/jest-preset-default": "^5.4.0",
"@wordpress/jest-preset-default": "^6.2.0",
"app-root-path": "^3.0.0",
"jest": "^25.1.0",
"jest-puppeteer": "^4.4.0",
@ -14328,9 +14328,9 @@
}
},
"eslint-plugin-jest": {
"version": "23.8.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-23.8.2.tgz",
"integrity": "sha512-xwbnvOsotSV27MtAe7s8uGWOori0nUsrXh2f1EnpmXua8sDfY6VZhHAhHg2sqK7HBNycRQExF074XSZ7DvfoFg==",
"version": "23.19.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-23.19.0.tgz",
"integrity": "sha512-l5PLflALqnODl8Yy0H5hDs18aKJS1KTf66VZGXRpIhmbLbPLaTuMB2P+65fBpkdseSpnTVcIlBYvTvJSBi/itg==",
"dev": true,
"requires": {
"@typescript-eslint/experimental-utils": "^2.5.0"

View File

@ -45,7 +45,7 @@
"deasync": "0.1.20",
"eslint": "6.8.0",
"eslint-config-wpcalypso": "5.0.0",
"eslint-plugin-jest": "23.8.2",
"eslint-plugin-jest": "23.19.0",
"github-contributors-list": "https://github.com/woocommerce/github-contributors-list/tarball/master",
"grunt": "1.1.0",
"grunt-contrib-clean": "2.0.0",

View File

@ -80,4 +80,8 @@
<rule ref="Squiz.Commenting.FileComment.Missing">
<exclude-pattern>tests/php/</exclude-pattern>
</rule>
<rule ref="WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid">
<exclude-pattern>src/</exclude-pattern>
</rule>
</ruleset>

View File

@ -51,5 +51,6 @@
</listeners>
<extensions>
<extension class="\Automattic\WooCommerce\Testing\Tools\CodeHacking\CodeHackerTestHook" />
<extension class="\Automattic\WooCommerce\Testing\Tools\DependencyManagement\DependencyManagementTestHook" />
</extensions>
</phpunit>

View File

@ -36,7 +36,26 @@ class Autoloader {
return false;
}
return require $autoloader;
/**
* In order to support local development for feature plugins the autoloader must support dev versions.
*
* - If the checked out branch cannot supply Composer with version information then it
* assigns it a dev version string for the Jetpack Autoloader to use.
* - By default the Jetpack Autoloader will ignore these dev versions in favor of tagged versions.
*
* Due to this interaction, feature plugin files from the included packages will always be loaded instead
* of the versions in the feature plugin when checked out from a repository as a dev version. By setting
* this constant we change the behavior of the autoloader so that dev versions are prioritized over the
* tagged versions included in WooCommerce Core.
*/
define( 'JETPACK_AUTOLOAD_DEV', true );
$autoloader_result = require $autoloader;
if ( ! $autoloader_result ) {
return false;
}
return $autoloader_result;
}
/**

97
src/Container.php Normal file
View File

@ -0,0 +1,97 @@
<?php
/**
* Container class file.
*
* @package Automattic\WooCommerce
*/
namespace Automattic\WooCommerce;
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\ProxiesServiceProvider;
use Automattic\WooCommerce\Internal\DependencyManagement\ExtendedContainer;
/**
* PSR11 compliant dependency injection container for WooCommerce.
*
* Classes in the `src` directory should specify dependencies from that directory via constructor arguments
* with type hints. If an instance of the container itself is needed, the type hint to use is \Psr\Container\ContainerInterface.
*
* Classes in the `src` directory should interact with anything outside (especially code in the `includes` directory
* and WordPress functions) by using the classes in the `Proxies` directory. The exception is idempotent
* functions (e.g. `wp_parse_url`), those can be used directly.
*
* Classes in the `includes` directory should use the `wc_get_container` function to get the instance of the container when
* they need to get an instance of a class from the `src` directory.
*
* Class registration should be done via service providers that inherit from Automattic\WooCommerce\Tools\DependencyManagement
* and those should go in the `src\Tools\DependencyManagement\ServiceProviders` folder unless there's a good reason
* to put them elsewhere. All the service provider class names must be in the `SERVICE_PROVIDERS` constant.
*/
final class Container implements \Psr\Container\ContainerInterface {
/**
* The root namespace of all WooCommerce classes in the `src` directory.
*/
const WOOCOMMERCE_ROOT_NAMESPACE = 'Automattic\\WooCommerce';
/**
* The list of service provider classes to register.
*
* @var string[]
*/
private $service_providers = array(
ProxiesServiceProvider::class,
);
/**
* The underlying container.
*
* @var \League\Container\Container
*/
private $container;
/**
* Class constructor.
*/
public function __construct() {
$this->container = new ExtendedContainer();
// Add ourselves as the shared instance of ContainerInterface,
// register everything else using service providers.
$this->container->share( \Psr\Container\ContainerInterface::class, $this );
foreach ( $this->service_providers as $service_provider_class ) {
$this->container->addServiceProvider( $service_provider_class );
}
}
/**
* Finds an entry of the container by its identifier and returns it.
*
* @param string $id Identifier of the entry to look for.
*
* @throws NotFoundExceptionInterface No entry was found for **this** identifier.
* @throws Psr\Container\ContainerExceptionInterface Error while retrieving the entry.
*
* @return mixed Entry.
*/
public function get( $id ) {
return $this->container->get( $id );
}
/**
* Returns true if the container can return an entry for the given identifier.
* Returns false otherwise.
*
* `has($id)` returning true does not mean that `get($id)` will not throw an exception.
* It does however mean that `get($id)` will not throw a `NotFoundExceptionInterface`.
*
* @param string $id Identifier of the entry to look for.
*
* @return bool
*/
public function has( $id ) {
return $this->container->has( $id );
}
}

View File

@ -0,0 +1,143 @@
<?php
/**
* AbstractServiceProvider class file.
*
* @package Automattic\WooCommerce\Internal\DependencyManagement
*/
namespace Automattic\WooCommerce\Internal\DependencyManagement;
use League\Container\Argument\RawArgument;
use League\Container\Definition\DefinitionInterface;
use League\Container\Definition\Definition;
/**
* Base class for the service providers used to register classes in the container.
*
* See the documentation of the original class this one is based on (https://container.thephpleague.com/3.x/service-providers)
* for basic usage details. What this class adds is:
*
* - The `add_with_auto_arguments` method that allows to register classes without having to specify the constructor arguments.
* - The `share_with_auto_arguments` method, sibling of the above.
* - Convenience `add` and `share` methods that are just proxies for the same methods in `$this->getContainer()`.
*/
abstract class AbstractServiceProvider extends \League\Container\ServiceProvider\AbstractServiceProvider {
/**
* Register a class in the container and use reflection to guess the constructor arguments.
*
* WARNING: this method uses reflection, so please have performance in mind when using it.
*
* @param string $class_name Class name to register.
* @param mixed $concrete The concrete to register. Can be a shared instance, a factory callback, or a class name.
* @param bool $shared Whether to register the class as shared (`get` always returns the same instance) or not.
*
* @return DefinitionInterface The generated container definition.
*
* @throws ContainerException Error when reflecting the class, or class constructor is not public, or an argument has no valid type hint.
*/
protected function add_with_auto_arguments( string $class_name, $concrete = null, bool $shared = false ) : DefinitionInterface {
$definition = new Definition( $class_name, $concrete );
$function = $this->reflect_class_or_callable( $class_name, $concrete );
if ( ! is_null( $function ) ) {
$arguments = $function->getParameters();
foreach ( $arguments as $argument ) {
if ( $argument->isDefaultValueAvailable() ) {
$default_value = $argument->getDefaultValue();
$definition->addArgument( new RawArgument( $default_value ) );
} else {
$argument_class = $argument->getClass();
if ( is_null( $argument_class ) ) {
throw new ContainerException( "AbstractServiceProvider::add_with_auto_arguments: constructor argument '{$argument->getName()}' of class '$class_name' doesn't have a type hint or has one that doesn't specify a class." );
}
$definition->addArgument( $argument_class->name );
}
}
}
// Register the definition only after being sure that no exception will be thrown.
$this->getContainer()->add( $definition->getAlias(), $definition, $shared );
return $definition;
}
/**
* Check if a combination of class name and concrete is valid for registration.
* Also return the class constructor if the concrete is either a class name or null (then use the supplied class name).
*
* @param string $class_name The class name to check.
* @param mixed $concrete The concrete to check.
*
* @return \ReflectionFunctionAbstract|null A reflection instance for the $class_name constructor or $concrete constructor or callable; null otherwise.
* @throws ContainerException Class has a private constructor, can't reflect class, or the concrete is invalid.
*/
private function reflect_class_or_callable( string $class_name, $concrete ) {
if ( ! isset( $concrete ) || is_string( $concrete ) && class_exists( $concrete ) ) {
try {
$class = $concrete ?? $class_name;
$reflector = new \ReflectionClass( $class );
$constructor = $reflector->getConstructor();
if ( isset( $constructor ) && ! $constructor->isPublic() ) {
throw new ContainerException( "AbstractServiceProvider::add_with_auto_arguments: constructor of class '$class' isn't public, instances can't be created." );
}
return $constructor;
} catch ( \ReflectionException $ex ) {
throw new ContainerException( "AbstractServiceProvider::add_with_auto_arguments: error when reflecting class '$class': {$ex->getMessage()}" );
}
} elseif ( is_callable( $concrete ) ) {
try {
return new \ReflectionFunction( $concrete );
} catch ( \ReflectionException $ex ) {
throw new ContainerException( "AbstractServiceProvider::add_with_auto_arguments: error when reflecting callable: {$ex->getMessage()}" );
}
}
return null;
}
/**
* Register a class in the container and use reflection to guess the constructor arguments.
* The class is registered as shared, so `get` on the container always returns the same instance.
*
* WARNING: this method uses reflection, so please have performance in mind when using it.
*
* @param string $class_name Class name to register.
* @param mixed $concrete The concrete to register. Can be a shared instance, a factory callback, or a class name.
*
* @return DefinitionInterface The generated container definition.
*
* @throws ContainerException Error when reflecting the class, or class constructor is not public, or an argument has no valid type hint.
*/
protected function share_with_auto_arguments( string $class_name, $concrete = null ) : DefinitionInterface {
return $this->add_with_auto_arguments( $class_name, $concrete, true );
}
/**
* Register an entry in the container.
*
* @param string $id Entry id (typically a class or interface name).
* @param mixed|null $concrete Concrete entity to register under that id, null for automatic creation.
* @param bool|null $shared Whether to register the class as shared (`get` always returns the same instance) or not.
*
* @return DefinitionInterface The generated container definition.
*/
protected function add( string $id, $concrete = null, bool $shared = null ) : DefinitionInterface {
return $this->getContainer()->add( $id, $concrete, $shared );
}
/**
* Register a shared entry in the container (`get` always returns the same instance).
*
* @param string $id Entry id (typically a class or interface name).
* @param mixed|null $concrete Concrete entity to register under that id, null for automatic creation.
*
* @return DefinitionInterface The generated container definition.
*/
protected function share( string $id, $concrete = null ) : DefinitionInterface {
return $this->add( $id, $concrete, true );
}
}

View File

@ -0,0 +1,25 @@
<?php
/**
* ExtendedContainer class file.
*
* @package Automattic\WooCommerce\Internal\DependencyManagement
*/
namespace Automattic\WooCommerce\Internal\DependencyManagement;
/**
* Class ContainerException.
* Used to signal error conditions related to the dependency injection container.
*/
class ContainerException extends \Exception {
/**
* Create a new instance of the class.
*
* @param null $message The exception message to throw.
* @param int $code The error code.
* @param Exception|null $previous The previous throwable used for exception chaining.
*/
public function __construct( $message = null, $code = 0, Exception $previous = null ) {
parent::__construct( $message, $code, $previous );
}
}

View File

@ -0,0 +1,108 @@
<?php
/**
* ExtendedContainer class file.
*
* @package Automattic\WooCommerce\Internal\DependencyManagement
*/
namespace Automattic\WooCommerce\Internal\DependencyManagement;
use Automattic\WooCommerce\Container;
use League\Container\Definition\DefinitionInterface;
/**
* This class extends the original League's Container object by adding some functionality
* that we need for WooCommerce.
*/
class ExtendedContainer extends \League\Container\Container {
/**
* Whitelist of classes that we can register using the container
* despite not belonging to the WooCommerce root namespace.
*
* In general we allow only the registration of classes in the
* WooCommerce root namespace to prevent registering 3rd party code
* (which doesn't really belong to this container) or old classes
* (which may be eventually deprecated, also the LegacyProxy
* should be used for those).
*
* @var string[]
*/
private $registration_whitelist = array(
\Psr\Container\ContainerInterface::class,
);
/**
* Register a class in the container.
*
* @param string $class_name Class name.
* @param mixed $concrete How to resolve the class with `get`: a factory callback, a concrete instance, another class name, or null to just create an instance of the class.
* @param bool|null $shared Whether the resolution should be performed only once and cached.
*
* @return DefinitionInterface The generated definition for the container.
* @throws ContainerException Invalid parameters.
*/
public function add( string $class_name, $concrete = null, bool $shared = null ) : DefinitionInterface {
if ( ! $this->class_is_in_root_namespace( $class_name ) && ! in_array( $class_name, $this->registration_whitelist, true ) ) {
throw new ContainerException( "Can't use the container to register '$class_name', only objects in the " . Container::WOOCOMMERCE_ROOT_NAMESPACE . ' namespace are allowed for registration.' );
}
return parent::add( $class_name, $concrete, $shared );
}
/**
* Does a class belong to the WooCommerce root namespace?
*
* @param string $class_name The class name to check.
*
* @return bool True if the class belongs to the WooCommerce root namespace.
*/
private function class_is_in_root_namespace( $class_name ) {
return substr( $class_name, 0, strlen( Container::WOOCOMMERCE_ROOT_NAMESPACE ) + 1 ) === Container::WOOCOMMERCE_ROOT_NAMESPACE . '\\';
}
/**
* Replace an existing registration with a different concrete.
*
* @param string $class_name The class name whose definition will be replaced.
* @param mixed $concrete The new concrete (same as "add").
*
* @return DefinitionInterface The modified definition.
* @throws ContainerException Invalid parameters.
*/
public function replace( string $class_name, $concrete ) {
if ( ! $this->has( $class_name ) ) {
throw new ContainerException( "ExtendedContainer::replace: The container doesn't have '$class_name' registered, please use 'add' instead of 'replace'." );
}
return $this->extend( $class_name )->setConcrete( $concrete );
}
/**
* Reset all the cached resolutions, so any further "get" for shared definitions will generate the instance again.
*/
public function reset_all_resolved() {
foreach ( $this->definitions->getIterator() as $definition ) {
// setConcrete causes the cached resolved value to be forgotten.
$concrete = $definition->getConcrete();
$definition->setConcrete( $concrete );
}
}
/**
* Get an instance of a registered class.
*
* @param string $id The class name.
* @param bool $new True to generate a new instance even if the class was registered as shared.
*
* @return object An instance of the requested class.
* @throws ContainerException Attempt to get an instance of a non-namespaced class.
*/
public function get( $id, bool $new = false ) {
if ( false === strpos( $id, '\\' ) ) {
throw new ContainerException( "Attempt to get an instance of the non-namespaced class '$id' from the container, did you forget to add a namespace import?" );
}
return parent::get( $id, $new );
}
}

View File

@ -0,0 +1,36 @@
<?php
/**
* Proxies class file.
*
* @package Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders
*/
namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;
use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;
use Automattic\WooCommerce\Proxies\LegacyProxy;
use Automattic\WooCommerce\Proxies\ActionsProxy;
/**
* Service provider for the classes in the Automattic\WooCommerce\Tools\Proxies namespace.
*/
class ProxiesServiceProvider extends AbstractServiceProvider {
/**
* The classes/interfaces that are serviced by this service provider.
*
* @var array
*/
protected $provides = array(
LegacyProxy::class,
ActionsProxy::class,
);
/**
* Register the classes.
*/
public function register() {
$this->share( ActionsProxy::class );
$this->share_with_auto_arguments( LegacyProxy::class );
}
}

5
src/Internal/README.md Normal file
View File

@ -0,0 +1,5 @@
# The internal namespace
All the code in this directory (and hence in the `Automattic\WooCommerce\Internal` namespace) is internal WooCommerce infrastructure code and not intended to be used by plugins. The important thing that this implies is that **backwards compatibility of the public surface for classes in this namespace is not guaranteed in future releases of WooCommerce**.
Therefore **plugin developers should never use classes in this namespace directly in their code**. See [the README file for the src folder](https://github.com/woocommerce/woocommerce/blob/master/src/README.md#the-internal-namespace) for more detailed guidance.

View File

@ -0,0 +1,44 @@
<?php
/**
* ActionsProxy class file.
*
* @package Automattic/WooCommerce/Tools/Proxies
*/
namespace Automattic\WooCommerce\Proxies;
/**
* Proxy for interacting with WordPress actions and filters.
*
* This class should be used instead of directly accessing the WordPress functions, to ease unit testing.
*
* @package Automattic\WooCommerce\Tools\Proxies
*/
class ActionsProxy {
/**
* Retrieve the number of times an action is fired.
*
* @param string $tag The name of the action hook.
*
* @return int The number of times action hook $tag is fired.
*/
public function did_action( $tag ) {
return did_action( $tag );
}
/**
* Calls the callback functions that have been added to a filter hook.
*
* @param string $tag The name of the filter hook.
* @param mixed $value The value to filter.
* @param mixed ...$parameters Additional parameters to pass to the callback functions.
*
* @return mixed The filtered value after all hooked functions are applied to it.
*/
public function apply_filters( $tag, $value, ...$parameters ) {
return apply_filters( $tag, $value, ...$parameters );
}
// TODO: Add the rest of the actions and filters related methods.
}

View File

@ -0,0 +1,94 @@
<?php
/**
* LegacyProxy class file.
*
* @package Automattic/WooCommerce/Tools/Proxies
*/
namespace Automattic\WooCommerce\Proxies;
use \Psr\Container\ContainerInterface as Container;
/**
* Proxy class to access legacy WooCommerce functionality.
*
* This class should be used to interact with code outside the `src` directory, especially functions and classes
* in the `includes` directory, unless a more specific proxy exists for the functionality at hand (e.g. `ActionsProxy`).
* Idempotent functions can be executed directly.
*
* @package Automattic\WooCommerce\Tools\Proxies
*/
class LegacyProxy {
/**
* Gets an instance of a given legacy class.
* This must not be used to get instances of classes in the `src` directory.
*
* If a given class needs a special procedure to get an instance of it,
* please add a private get_instance_of_(lowercased_class_name) and it will be
* automatically invoked. See also how objects of classes having a static `instance`
* method are retrieved, similar approaches can be used as needed to make use
* of existing factory methods such as e.g. 'load'.
*
* @param string $class_name The name of the class to get an instance for.
* @param mixed ...$args Parameters to be passed to the class constructor or to the appropriate internal 'get_instance_of_' method.
*
* @return object The instance of the class.
* @throws \Exception The requested class belongs to the `src` directory, or there was an error creating an instance of the class.
*/
public function get_instance_of( string $class_name, ...$args ) {
if ( false !== strpos( $class_name, '\\' ) ) {
throw new \Exception( 'The LegacyProxy class is not intended for getting instances of classes in the src directory, please use constructor injection or the instance of \\Psr\\Container\\ContainerInterface for that.' );
}
// If a class has a dedicated method to obtain a instance, use it.
$method = 'get_instance_of_' . strtolower( $class_name );
if ( method_exists( __CLASS__, $method ) ) {
return $this->$method( ...$args );
}
// If the class is a singleton, use the "instance" method.
if ( method_exists( $class_name, 'instance' ) ) {
return $class_name::instance( ...$args );
}
// Fallback to simply creating a new instance of the class.
return new $class_name( ...$args );
}
/**
* Get an instance of a class implementing WC_Queue_Interface.
*
* @return \WC_Queue_Interface The instance.
*/
private function get_instance_of_wc_queue_interface() {
return \WC_Queue::instance();
}
/**
* Call a user function. This should be used to execute any non-idempotent function, especially
* those in the `includes` directory or provided by WordPress.
*
* @param string $function_name The function to execute.
* @param mixed ...$parameters The parameters to pass to the function.
*
* @return mixed The result from the function.
*/
public function call_function( $function_name, ...$parameters ) {
return call_user_func_array( $function_name, $parameters );
}
/**
* Call a static method in a class. This should be used to execute any non-idempotent method in classes
* from the `includes` directory.
*
* @param string $class_name The name of the class containing the method.
* @param string $method_name The name of the method.
* @param mixed ...$parameters The parameters to pass to the method.
*
* @return mixed The result from the method.
*/
public function call_static( $class_name, $method_name, ...$parameters ) {
return call_user_func_array( "$class_name::$method_name", $parameters );
}
}

View File

@ -1,8 +1,34 @@
# WooCommerce `src` files
This directory is home to new WooCommerce class files under the \Automattic\WooCommerce\ namespace using PSR-4 file naming. This is to take full advantage of autoloading.
## Table of contents
* [Installing Composer](#installing-composer)
+ [Updating the autoloader class maps](#updating-the-autoloader-class-maps)
* [Installing packages](#installing-packages)
* [The container](#the-container)
+ [Resolving classes](#resolving-classes)
- [From other classes in the `src` directory](#1-other-classes-in-the-src-directory)
- [From code in the `includes` directory](#2-code-in-the-includes-directory)
+ [Registering classes](#registering-classes)
- [Using concretes](#using-concretes)
- [A note on legacy classes](#a-note-on-legacy-classes)
* [The `Internal` namespace](#the-internal-namespace)
* [Interacting with legacy code](#interacting-with-legacy-code)
+ [The `LegacyProxy` class](#the-legacyproxy-class)
+ [Using the legacy proxy](#using-the-legacy-proxy)
+ [Using the mockable proxy in tests](#using-the-mockable-proxy-in-tests)
+ [But how does `get_instance_of` work?](#but-how-does-get_instance_of-work)
+ [Creating specialized proxies](#creating-specialized-proxies)
* [Defining new actions and filters](#defining-new-actions-and-filters)
* [Writing unit tests](#writing-unit-tests)
+ [Mocking dependencies](#mocking-dependencies)
This directory is home to new WooCommerce class files under the `Automattic\WooCommerce` namespace using [PSR-4](https://www.php-fig.org/psr/psr-4/) file naming. This is to take full advantage of autoloading.
Ideally, all the new code for WooCommerce should consist of classes following the PSR-4 naming and living in this directory, and the code in [the `includes` directory](https://github.com/woocommerce/woocommerce/tree/master/includes/README.md) should receive the minimum amount of changes required for bug fixing. This will not always be possible but that should be the rule of thumb.
A [PSR-11](https://www.php-fig.org/psr/psr-11/) container is in place for registering and resolving the classes in this directory by using the [dependency injection](https://en.wikipedia.org/wiki/Dependency_injection) pattern. There are tools in place to interact with legacy code (and code outside the `src`directory in general) in a way that makes it easy to write unit tests.
Currently, these classes have a PHP 7.0 requirement. No required core classes will be added here until this PHP version is enforced. If running an older version of PHP, these class files will not be used.
## Installing Composer
@ -10,6 +36,15 @@ Composer is used to generate autoload class-maps for the files here. The stable
If you don't have Composer installed, go and check how to [install Composer](https://github.com/woocommerce/woocommerce/wiki/How-to-set-up-WooCommerce-development-environment) and then continue here.
### Updating the autoloader class maps
If you add a class to WooCommerce you need to run the following to ensure it's included in the autoloader class-maps:
```
composer dump-autoload
```
## Installing packages
To install the packages WooCommerce requires, from the main directory run:
@ -24,30 +59,377 @@ To update packages run:
composer update
```
If you add a class to WooCommerce and want to ensure it's included in the autoloader class-maps, run:
```
composer dump-autoload
```
## The container
### Using classes
WooCommerce uses a [PSR-11](https://www.php-fig.org/psr/psr-11/) compatible container for registering and resolving all the classes in this directory by using the [dependency injection](https://en.wikipedia.org/wiki/Dependency_injection) pattern. More specifically, we use [the container from The PHP League](https://container.thephpleague.com/); this is relevant when registering classes, but not when resolving them. The full class name of the container used is `Automattic\WooCommerce\Container` (it uses the PHP League's container under the hood).
To use something a namespaced class you have to declare it at the top of the file before any other instruction, and then use it in the code. For example:
_Resolving_ a class means asking the container to provide an instance of the class (or interface). _Registering_ a class means telling the container how the class should be resolved.
In principle, the container should be used to register and resolve all the classes in the `src` directory. The exception might be data-only classes that could be created the old way (using a plain `new` statement); but as a rule of thumb, the container should always be used.
There are two ways to resolve registered classes, depending on from where they are resolved:
* Classes in the `src` directory specify their dependencies as constructor arguments, which are automatically supplied by the container when the class is resolved (this is called _constructor injection_).
* For code in the `includes` directory there's a `wc_get_container` function that will return the container, then its `get` method can be used to resolve any class.
### Resolving classes
There are two ways to resolve registered classes, depending on from where they need to be resolved:
#### 1. Other classes in the `src` directory
When a class in the `src` directory depends on other one classes from the same directory, it should use constructor injection. This means specifying these dependencies as constructor arguments with appropriate type hints, and storing these in private variables, ready to be used when needed:
```php
use Automattic\WooCommerce\TestClass;
use TheService1Namespace\Service1;
use TheService2Namespace\Service2;
// other code...
class TheClassWithDependencies {
private $service1;
$test_class = new TestClass();
private $service2;
public function __construct( Service1Class $service1, Service2Class $service2 ) {
$this->$service1 = $service1;
$this->$service2 = $service2;
}
public function method_that_needs_service_1() {
$this->service1->do_something();
}
}
```
If you need to rule out conflicts, you can alias it:
Whenever the container is about to resolve `TheClassWithDependencies` it will also resolve `Service1Class` and `Service2Class` and pass them as constructor arguments to the requested class. If these service classes have constructor arguments too then those will also be appropriately resolved recursively.
A "lazy" approach is also possible if needed: you can specify the container itself as a constructor argument (using `\Psr\Container\ContainerInterface` as type hint), and use its `get` method to obtain the required instance at the appropriate time:
```php
use Automattic\WooCommerce\TestClass as Test_Class_Alias;
use TheService1Namespace\Service1;
// other code...
class TheClassWithDependencies {
private $container;
$test_class = new Test_Class_Alias();
public function __construct( \Psr\Container\ContainerInterface $container ) {
$this->$container = $container;
}
public function method_that_needs_service_1() {
$this->container->get( Service1::class )->do_something();
}
}
```
In general, however, constructor injection is preferred and the lazy approach should be used only when really necessary.
#### 2. Code in the `includes` directory
When you need to use classes defined in the `src` directory from within legacy code in `includes`, use the `wc_get_container` function to get the instance of the container, then resolve the required class with `get`:
```php
use TheService1Namespace\Service1;
function wc_function_that_needs_service_1() {
$service = wc_get_container()->get( Service1::class );
$service->do_something();
}
```
This is also the recommended approach when moving code from `includes` to `src` while keeping the existing entry points for the old code in place for compatibility.
Worth noting: the container will throw a `ContainerException` when receiving a request for resolving a class that hasn't been registered. All classes need to have been registered prior to being resolved.
### Registering classes
For a class to be resolvable using the container, it needs to have been previously registered in the same container.
The `Container` class is "read-only", in that it has a `get` method to resolve classes but it doesn't have any method to register classes. Instead, class registration is done by using [service providers](https://container.thephpleague.com/3.x/service-providers/). That's how the whole process would go when creating a new class:
First, create the class in the appropriate namespace (and thus in the matching folder), remember that the base namespace for the classes in the `src` directory is `Atuomattic\WooCommerce`. If the class depends on other classes from `src`, specify these dependencies as constructor arguments in detailed above.
Example of such a class:
```php
namespace Automattic\WooCommerce\TheClassNamespace;
use Automattic\WooCommerce\TheDependencyNamespace\TheDependencyClass;
class TheClass {
private $the_dependency;
public function __construct( TheDependencyClass $dependency ) {
$this->the_dependency = $dependency;
}
}
```
Then, create a `<class name>ServiceProvider` class in the `src/Internal/DependencyManagement/ServiceProviders` folder (and thus in the appropriate namespace) as follows:
```php
namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;
use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;
use Automattic\WooCommerce\TheClassNamespace\TheClass;
use Automattic\WooCommerce\TheDependencyNamespace\TheDependencyClass;
class TheClassServiceProvider extends AbstractServiceProvider {
protected $provides = array(
TheClass::class
);
public function register() {
$this->add( TheClass::class )->addArgument( TheDependencyClass::class );
}
}
```
Last (but certainly not least, don't forget this step!), add the class name of the service provider to the `$service_providers` property in the `Container` class.
Worth noting:
* In the example the service provider is used to register only one class, but service providers can be used to register a group of related classes. The `$provides` property must contain all the names of the classes that the provider can register.
* The container will invoke the provider `register` method the first time any of the classes in `$provides` is resolved.
* If you look at [the service provider documentation](https://container.thephpleague.com/3.x/service-providers/) you will see that classes are registered using `this->getContainer()->add`. WooCommerce's `AbstractServiceProvider` adds a utility `add` method itself that serves the same purpose.
* You can use `share` instead of `add` to register single-instance classes (the class is instantiated only once and cached, so the same instance is returned every time the class is resolved).
If the class being registered has constructor arguments then the `add` (or `share`) method must be followed by as many `addArguments` calls as needed. WooCommerce's `AbstractServiceProvider` adds a utility `add_with_auto_arguments` method (and a sibling `share_with_auto_arguments` method) that uses reflection to figure out and register all the constructor arguments (which need to have type hints). Please have in mind the possible performance penalty incurred by the usage of reflection when using this helper method.
An alternative version of the service provider, which is used to register both the class and its dependency, and which takes advantage of `add_with_auto_arguments`, could be as follows:
```php
namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;
use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;
use Automattic\WooCommerce\TheClassNamespace\TheClass;
use Automattic\WooCommerce\TheDependencyNamespace\TheDependencyClass;
class TheClassServiceProvider extends AbstractServiceProvider {
protected $provides = array(
TheClass::class,
TheDependencyClass::class
);
public function register() {
$this->share( TheDependencyClass::class );
$this->share_with_auto_arguments( ActionsProxy::class );
}
}
```
#### Using concretes
By default, the `add` and `share` methods instruct the container to resolve the registered class by using `new` to create a new instance of the class. But these methods accept an optional `$concrete` argument that can be used to tell the container to resolve the class in a different way. `$concrete` may be one of the following:
* A class name
The supplied class name will be instantiated when the registered class name is resolved. This is especially useful to register interfaces, example:
```php
$this->add( TheInterface::class, TheClassImplementingTheInterface::class );
```
* An object
The supplied object will be returned then the registerd class name is resolved. Example:
```php
$instance = new TheClass();
$this->add( TheClass::class, $instance );
```
* A closure
The closure will be executed and the result value will be returned when the registerd class name is resolved. Example:
```php
$factory = function( TheDependencyClass $dependency ) {
return new TheClass( $dependency );
};
$this->add( TheClass::class, $factory );
```
Note that if the closure is defined as a function with arguments, the supplied parameters will be resolved too.
#### A note on legacy classes
The container is intended for registering **only** classes in the `src` folder. There is a check in place to prevent classes outside the root `Automattic\Woocommerce` namespace from being registered.
This implies that classes outside `src` can't be constructor-injected, and thus must not be used as type hints in constructor arguments. There are mechanisms in place to interact with "outside" code (including code from the `includes` folder and third-party code) in a way that makes it easy to write unit tests.
## The `Internal` namespace
While it's up to the developer to choose the appropriate namespaces for any newly created classes, and those namespaces should make sense from a semantic point of view, there's one namespace that has a special meaning: `Automattic\WooCommerce\Internal`.
Classes in `Automattic\WooCommerce\Internal` are meant to be WooCommerce infrastructure code that might change in future releases. In other words, for code inside that namespace, **backwards compatibility of the public surface is not guaranteed**: future releases might include breaking changes including renaming or renaming classes, renaming or removing public methods, or changing the signature of public methods. The code in this namespace is considered "internal", whereas all the other code in `src` is considered "public".
What this implies for you as developer depends on what type of contribution are you making:
* **If you are woking on WooCommerce core:** When you need to add a new class please think carefully if the class could be useful for plugins. If you really think so, add it to the appropriate namespace rooted at `Automattic\WooCommerce`. If not, add it to the appropriate namespace but rooted at `Automattic\WooCommerce\Internal`.
* When in doubt, always make the code internal. If an internal class is later deemed to be worth being made public, the change can be made easily (by just changing the class namespace) and nothing will break. Turning a public class into an internal class, on the other hand, is impossible since it could break existing plugins.
* **If you are a plugin developer:** You should **never** use code from the `Automattic\WooCommerce\Internal` namespace in your plugins. Doing so might cause your plugin to break in future versions of WooCommerce.
## Interacting with legacy code
Here by "legacy code" we refer mainly to the old WooCommerce code in the `includes` directory, but the mechanisms described in this section are useful for dealing with any code outside the `src` directory.
The code in the `src` directory can for sure interact directly with legacy code. A function needs to be called? Call it. You need an instance of an object? Instantiate it. The problem is that this makes the code difficult to test: it's not easy to mock functions (unless you use [hacks](https://github.com/woocommerce/woocommerce/blob/master/tests/Tools/CodeHacking/README.md), or objects that are instantiated directly with `new` or whose instance is retrieved via a `TheClass::instance()` method).
But we want the WooCommerce code base (and especially the code in `src`) to be well covered by unit tests, and so there are mechanisms in place to interact with legacy code while keeping the code testable.
### The `LegacyProxy` class
`LegacyProxy` is a class that contains three public methods intended to allow interaction with legacy code:
* `get_instance_of`: Retrieves an instance of a legacy (non-`src`) class.
* `call_function`: Calls a standalone function.
* `call_static`: Calls a static method in a class.
Whenever a `src` class needs to get an instance of a legacy class, or call a function, or call a static method from another class, and that would make the code difficult to test, it should use the `LegacyProxy` methods instead.
But how does using `LegacyProxy` help in making the code testable? The trick is that when tests run what is registered instead of `LegacyProxy` is an instance of `MockableLegacyProxy`, a class with the same public surface but with additional methods that allow to easily mock legacy classes, functions and static methods.
### Using the legacy proxy
`LegacyProxy` is a class that is registered in the container as any other class, so an instance can be obtained by using constructor injection:
```php
use Automattic\WooCommerce\Proxies\LegacyProxy;
class TheClass {
private $legacy_proxy;
public function __construct( LegacyProxy $legacy_proxy ) {
$this->legacy_proxy = $legacy_proxy;
}
public function do_something_using_some_function() {
$this->legacy_proxy->call_function( 'the_function_name', 'param1', 'param2' );
}
}
```
However, the recommended way (especially when no other dependencies need to be constructor-injected) is to use the equivalent methods in the `WooCommerce` class via the `WC()` helper, like this:
```php
class TheClass {
public function do_something_using_some_function() {
WC()->call_function( 'the_function_name', 'param1', 'param2' );
}
}
```
Both ways are completely equivalent since the helper methods are just doing `wc_get_container()->get( LegacyProxy::class )->...` under the hood.
### Using the mockable proxy in tests
When unit tests run the container will return an instance of `MockableLegacyProxy` when `LegacyProxy` is resolved. This class has the same public methods as `LegacyProxy` but also the following ones:
* `register_class_mocks`: defines mocks for classes that are retrieved via `get_instance_of`.
* `register_function_mocks`: defines mocks for functions that are invoked via `call_function`.
* `register_static_mocks`: defines mocks for functions that are invoked via `call_static`.
These methods could be accessed via `wc_get_container()->get( LegacyProxy::class )->register...` directly from the tests, but the preferred way is to use the equivalent helper methods offered by the `WC_Unit_Test_Case` class,: `register_legacy_proxy_class_mocks`, `register_legacy_proxy_function_mocks` and `register_legacy_proxy_static_mocks`.
Here's an example of how function mocks are defined:
```php
// In this context '$this' is a class that extends WC_Unit_Test_Case
$this->register_legacy_proxy_function_mocks(
array(
'the_function_name' => function( $param1, $param2 ) {
return "I'm the mock of the_function_name and I was invoked with $param1 and $param2.";
},
)
);
```
Of course, for the cases where no mocks are defined `MockableLegacyProxy` works the same way as `LegacyProxy`.
Please see [the code of the MockableLegacyProxy class](https://github.com/woocommerce/woocommerce/blob/master/tests/Tools/DependencyManagement/MockableLegacyProxy.php) and [its unit tests](https://github.com/woocommerce/woocommerce/blob/master/tests/php/src/Proxies/MockableLegacyProxyTest.php) for more detailed usage instructions and examples.
### But how does `get_instance_of` work?
We use a container to resolve instances of classes in the `src` directory, but how does the legacy proxy's `get_instance_of` know how to resolve legacy classes?
This is a mostly ad-hoc process. When a class has a special way to be instantiated or retrieved (e.g. a static `instance` method), then that is used; otherwise the method fallbacks to simply creating a new instance of the class using `new`.
This means that the `get_instance_of` method will most likely need to evolve over time to cover additional special cases. Take a look at the method code in [LegacyProxy](https://github.com/woocommerce/woocommerce/blob/master/src/Proxies/LegacyProxy.php) for details on how to properly make changes to the method.
### Creating specialized proxies
While helpful to make the code testable, using the legacy proxy can make the code somewhat more difficult to read or maintain, so it should be used judiciously and only when really needed to make the code properly testable.
That said, an alternative middle ground would be to create more specialized cases for frequently used pieces of legacy code, for example:
```php
class ActionsProxy {
public function did_action( $tag ) {
return did_action( $tag );
}
public function apply_filters( $tag, $value, ...$parameters ) {
return apply_filters( $tag, $value, ...$parameters );
}
}
```
Note however that such a class would have to be explicitly constructor-injected (unless additional helper methods are defined in the `WooCommerce` class), and that you would need to create a pairing mock class (e.g. `MockableActionsProxy`) and replace the original registration using `wc_get_container()->replace( ActionsProxy::class, MockableActionsProxy::class )`.
## Defining new actions and filters
WordPress' hooks (actions and filters) are a very powerful extensibility mechanism and it's the core tool that allows WooCommerce extensions to be developer. However it has been often (ab)used in the WooCommerce core codebase to drive internal logic, e.g. an action is triggered from within one class or function with the assumption that somewhere there's some other class or function that will handle it and continue whatever processing is supposed to happen.
In order to keep the code as easy as reasonably possible to read and maintain, **hooks shouldn't be used to drive WooCommerce's internal logic and processes**. If you need the services of a given class or function, please call these directly (by using constructor injection or the legacy proxy as appropriate to get access to the desired service). **New hooks should be introduced only if they provide a valuable extension point for plugins**.
As usual, there might be reasonable exceptions to this; but please keep this rule in mind whenever you consider creating a new hook.
## Writing unit tests
Unit tests are a fundamental tool to keep the code reliable and reasonably safe from regression errors. To that end, any new code added to the WooCommerce codebase, but especially to the `src` directory, should be reasonably covered by such tests.
**If you are a WooCommerce core team member or a contributor from other team at Automattic:** Please write unit tests to cover any code addition or modification that you make to the `src` directory (and ideally the same for the `includes` directory, by the way). There are always reasonable exceptions, but the rule of thumb is that all code should be covered by tests.
**If you are an external contributor:** When adding or changing code on the WooCommerce codebase, and especially in the `src` directory, adding unit tests is recommended but not mandatory: no contributions will be rejected solely for lacking unit tests. However, please try to at least make the code easily testable by honoring the container and constructor injection mechanism, and by using the legacy proxy to interact with legacy code when needed. If you do so, the WooCommerce team or other contributors will be able to add the missing tests.
### Mocking dependencies
Since all the dependencies for classes in this directory are constructor-injected or retrieved lazily by directly accessing the container, it's easy to mock them by either manually creating a mock class with the same public surface or by using [PHPUnit's test doubles](https://phpunit.readthedocs.io/en/9.2/test-doubles.html):
```php
$dependency_mock = somehow_create_mock();
$sut = new TheClassToTest( $dependency_mock ); //sut = System Under Test
$result = $sut->do_something();
$this->assertEquals( $result, 'the expected result' );
```
However, while this works well for simple scenarios, in the real world dependencies will often have other dependencies in turn, so instantiating all the required intermediate objects will be complex. To make things easier, while tests run the `Container` class is replaced with an `ExtendedContainer` class that has a couple of additional methods:
* `replace`: allows defining a new replacement concrete for a given class registration.
* `reset_all_resolved`: discards all the cached resolutions. You may need when mocking classes that have been defined as shared.
It's worth noting that at unit testing session bootstrap time `reset_all_resolved` is called once to reset any cached resolutions made during WC install, and `replace` is used to swap the `LegacyProxy` with a `MockableLegacyProxy`.
The same example using `replace`:
```php
$dependency_mock = somehow_create_mock();
$container = wc_get_container();
$container->reset_all_resolved(); //if either the SUT or the dependency are shared
$container->replace( TheDependencyClass::class, $dependency_mock );
$sut = $container->get( TheClassToTest::class );
$result = $sut->do_something();
$this->assertEquals( $result, 'the expected result' );
```
Note: of course all of this applies to dependencies from the `src` directory, for mocking legacy dependencies [the `MockableLegacyProxy`](#using-the-mockable-proxy-in-tests) should be used instead.

View File

@ -20,7 +20,7 @@ defined( 'ABSPATH' ) || exit;
global $product;
// Ensure visibility.
if ( empty( $product ) || ! $product->is_visible() ) {
if ( empty( $product ) || false === wc_get_loop_product_visibility( $product->get_id() ) || ! $product->is_visible() ) {
return;
}
?>

View File

@ -26,7 +26,6 @@ do_action( 'woocommerce_email_header', $email_heading, $email ); ?>
<?php /* translators: %s: Customer first name */ ?>
<p><?php printf( esc_html__( 'Hi %s,', 'woocommerce' ), esc_html( $order->get_billing_first_name() ) ); ?></p>
<?php /* translators: %s: Site title */ ?>
<p><?php esc_html_e( 'We have finished processing your order.', 'woocommerce' ); ?></p>
<?php

View File

@ -23,7 +23,8 @@ if ( ! defined( 'ABSPATH' ) ) {
?>
<p class="woocommerce-result-count">
<?php
if ( 1 === $total ) {
// phpcs:disable WordPress.Security
if ( 1 === intval( $total ) ) {
_e( 'Showing the single result', 'woocommerce' );
} elseif ( $total <= $per_page || -1 === $per_page ) {
/* translators: %d: total results */
@ -34,5 +35,6 @@ if ( ! defined( 'ABSPATH' ) ) {
/* translators: 1: first result 2: last result 3: total results */
printf( _nx( 'Showing %1$d&ndash;%2$d of %3$d result', 'Showing %1$d&ndash;%2$d of %3$d results', $total, 'with first and last result', 'woocommerce' ), $first, $last, $total );
}
// phpcs:enable WordPress.Security
?>
</p>

View File

@ -1,5 +1,8 @@
# WooCommerce Tests
This document discusses unit tests. See [the e2e README](https://github.com/woocommerce/woocommerce/tree/master/tests/e2e) to learn how to setup testing environment for running e2e tests and run them.
## Table of contents
- [WooCommerce Unit Tests](#woocommerce-unit-tests)
@ -10,9 +13,8 @@
- [Code Coverage](#code-coverage)
- [WooCommerce E2E Tests](#woocommerce-e2e-tests)
## WooCommerce Unit Tests
### Initial Setup
## Initial Setup
From the WooCommerce root directory (if you are using VVV you might need to `vagrant ssh` first), run the following:
@ -36,7 +38,8 @@ Example:
**Important**: The `<db-name>` database will be created if it doesn't exist and all data will be removed during testing.
### Running Tests
## Running Tests
Change to the plugin root directory and type:
@ -52,10 +55,26 @@ A text code coverage summary can be displayed using the `--coverage-text` option
$ vendor/bin/phpunit --coverage-text
### Writing Tests
* There are two different PHPUnit directories, `tests/legacy` and `tests/php`. The `tests/legacy` directory contains all of the tests for code in the `includes` directory, and the `tests/php` directory is a PSR-4 namespaced directory for tests of code in the `src` directory.
* Each test file should roughly correspond to an associated source file, e.g. the `formatting/functions.php` test file covers code in the `wc-formatting-functions.php` file
## Writing Tests
There are three different unit test directories:
- `tests/legacy/unit-tests` contains tests for code in the `includes` directory. No new tests should be added here, ever; existing test classes shouldn't get new tests either. Fixing faulty existing tests is allowed.
- `tests/php/includes` is where all the new tests for code in the `includes` directory should be written.
- `tests/php/src` is where all the tests for code in the `src` directory should be written.
Each test file should correspond to an associated source file and be named accordingly:
* For `src` code: The base namespace for tests is `Automattic\WooCommerce\Tests`. A class named `Automattic\WooCommerce\TheNamespace\TheClass` should have a test named `Automattic\WooCommerce\Tests\TheNamespace\TheClassTest`.
* For `includes` code:
* When testing classes: use the same approach as for `src` except that namespaces are not used. So a `WC_Something` class in `includes/somefolder/class-wc-something.php` should have its tests in `tests/src/internal/somefolder/class-wc-something-test.php`.
* When testing functions: use one test file per functions group file, for example `wc-formatting-functions-test.php` for code in the `wc-formatting-functions.php` file.
See also [the guidelines for writing unit tests for `src` code](https://github.com/woocommerce/woocommerce/tree/master/src/README.md#writing-unit-tests) and [the guidelines for `includes` code](https://github.com/woocommerce/woocommerce/tree/master/includes/README.md#writing-unit-tests).
General guidelines for all the unit tests:
* Each test method should cover a single method or function with one or more assertions
* A single method or function can have multiple associated test methods if it's a large or complex method
* Use the test coverage HTML report (under `tmp/coverage/index.html`) to examine which lines your tests are covering and aim for 100% coverage
@ -66,14 +85,12 @@ A text code coverage summary can be displayed using the `--coverage-text` option
* Filters persist between test cases so be sure to remove them in your test method or in the `tearDown()` method.
* Use data providers where possible. Be sure that their name is like `data_provider_function_to_test` (i.e. the data provider for `test_is_postcode` would be `data_provider_test_is_postcode`). Read more about data providers [here](https://phpunit.de/manual/current/en/writing-tests-for-phpunit.html#writing-tests-for-phpunit.data-providers).
### Automated Tests
## Automated Tests
Tests are automatically run with [Travis-CI](https://travis-ci.org/woocommerce/woocommerce) for each commit and pull request.
### Code Coverage
## Code Coverage
Code coverage is available on [Codecov](https://codecov.io/gh/woocommerce/woocommerce/) which receives updated data after each Travis build.
## WooCommerce E2E Tests
See [e2e README](https://github.com/woocommerce/woocommerce/tree/master/tests/e2e) to learn how to setup testing environment for running e2e tests and run them.

View File

@ -118,7 +118,7 @@ final class CodeHacker {
public static function add_hack( $hack ) {
if ( ! self::is_valid_hack_object( $hack ) ) {
$class = get_class( $hack );
throw new \Exception( "CodeHacker::addhack for instance of $class: Hacks must be objects having a 'process(\$text, \$path)' method and a 'reset()' method." );
throw new \Exception( "CodeHacker::add_hack for instance of $class: Hacks must be objects having a 'process(\$text, \$path)' method and a 'reset()' method." );
}
self::$hacks[] = $hack;

View File

@ -0,0 +1,33 @@
<?php
/**
* DependencyManagementTestHook class file.
*
* @package Automattic\WooCommerce\Testing\Tools\DependencyManagement
*/
namespace Automattic\WooCommerce\Testing\Tools\DependencyManagement;
use Automattic\WooCommerce\Proxies\LegacyProxy;
use PHPUnit\Runner\BeforeTestHook;
/**
* Hook to perform dependency management related setup in PHPUnit. To use, add this to phpunit.xml:
*
* <extensions>
* <extension class="DependencyManagementTestHook" />
* </extensions>
*/
final class DependencyManagementTestHook implements BeforeTestHook {
/**
* Runs before each test.
*
* @param string $test "TestClass::TestMethod".
*/
public function executeBeforeTest( string $test ): void {
// Reset the instance of MockableLegacyProxy that was registered during bootstrap,
// in order to start the test in a clean state (without anything mocked).
wc_get_container()->get( LegacyProxy::class )->reset();
}
}

View File

@ -0,0 +1,184 @@
<?php
/**
* MockableLegacyProxy class file.
*
* @package Automattic\WooCommerce\Testing\Tools\DependencyManagement
*/
namespace Automattic\WooCommerce\Testing\Tools\DependencyManagement;
/**
* Mockable version of LegacyProxy.
*
* This version contains methods that allow to easily mock any standalone function or static class method,
* as well as mocking the instantiation of legacy classes, within unit tests.
* By default, and unless any mock is registered, this class acts exactly as LegacyProxy does.
*
* @package Automattic\WooCommerce\Testing\Tools\DependencyManagement
*/
class MockableLegacyProxy extends \Automattic\WooCommerce\Proxies\LegacyProxy {
/**
* The currently registered mocks for classes.
*
* @var array
*/
private $mocked_classes = array();
/**
* The currently registered mocks for functions.
*
* @var array
*/
private $mocked_functions = array();
/**
* The currently registered mocks for static methods.
*
* @var array
*/
private $mocked_statics = array();
/**
* Reset the instance to its initial state by removing all the mocks.
*/
public function reset() {
$this->mocked_classes = array();
$this->mocked_functions = array();
$this->mocked_statics = array();
}
/**
* Register the function mocks to use.
*
* @param array $mocks An associative array where keys are function names and values are function replacement callbacks.
*
* @throws \Exception Invalid parameter.
*/
public function register_function_mocks( array $mocks ) {
foreach ( $mocks as $function_name => $mock ) {
if ( ! is_string( $function_name ) || ! is_callable( $mock ) ) {
throw new \Exception( 'MockableLegacyProxy::register_function_mocks: The supplied mocks array must have function names as keys and function replacement callbacks as values.' );
}
}
$this->mocked_functions = array_merge( $this->mocked_functions, $mocks );
}
/**
* Register the static method mocks to use.
*
* @param array $mocks An associative array where keys are class names and values are associative arrays, in which keys are method names and values are method replacement callbacks.
*
* @throws \Exception Invalid parameter.
*/
public function register_static_mocks( array $mocks ) {
$exception_text = 'MockableLegacyProxy::register_static_mocks: $mocks must be an associative array of class name => associative array of method name => callable.';
foreach ( $mocks as $class_name => $class_mocks ) {
if ( ! is_string( $class_name ) || ! is_array( $class_mocks ) ) {
throw new \Exception( $exception_text );
}
foreach ( $class_mocks as $method_name => $method_mock ) {
if ( ! is_string( $method_name ) || ! is_callable( $method_mock ) ) {
throw new \Exception( $exception_text );
}
}
}
// TODO: replace the following with just "$this->mocked_statics = array_merge_recursive( $this->mocked_statics, $mocks )" once the minimum PHP version is bumped to 7.1 or newer (see https://bugs.php.net/bug.php?id=76505).
$class_names = array_keys( $mocks );
foreach ( $class_names as $class_name ) {
if ( array_key_exists( $class_name, $this->mocked_statics ) ) {
$this->mocked_statics[ $class_name ] = array_merge( $this->mocked_statics[ $class_name ], $mocks[ $class_name ] );
} else {
$this->mocked_statics[ $class_name ] = $mocks[ $class_name ];
}
}
}
/**
* Register the class mocks to use.
*
* @param array $mocks An associative array where keys are class names and values are either factory callbacks (with any extra arguments that get_instance_of would accept) or objects.
*
* @throws \Exception Invalid parameter.
*/
public function register_class_mocks( array $mocks ) {
foreach ( $mocks as $class_name => $mock ) {
if ( ! is_string( $class_name ) || ( ! is_object( $mock ) && ! is_callable( $mock ) ) ) {
throw new \Exception( 'MockableLegacyProxy::register_class_mocks: $mocks must be an associative array of class_name => object or factory callback.' );
}
}
$this->mocked_classes = array_merge( $this->mocked_classes, $mocks );
}
/**
* Call a user function. This should be used to execute any non-idempotent function, especially
* those in the `includes` directory or provided by WordPress.
*
* If a mock has been defined for the requested function using `register_function_mocks`, then the registered
* callback will be executed instead of the function.
*
* @param string $function_name The function to execute.
* @param mixed ...$parameters The parameters to pass to the function.
*
* @return mixed The result from the function or mock callback.
*/
public function call_function( $function_name, ...$parameters ) {
if ( array_key_exists( $function_name, $this->mocked_functions ) ) {
return call_user_func_array( $this->mocked_functions[ $function_name ], $parameters );
}
return parent::call_function( $function_name, ...$parameters );
}
/**
* Call a static method in a class. This should be used to execute any non-idempotent method in classes
* from the `includes` directory.
*
* If a mock has been defined for the requested class and method using `register_function_mocks`,
* then the registered callback will be executed instead of the method.
*
* @param string $class_name The name of the class containing the method.
* @param string $method_name The name of the method.
* @param mixed ...$parameters The parameters to pass to the method.
*
* @return mixed The result from the method or mock callback.
*/
public function call_static( $class_name, $method_name, ...$parameters ) {
if ( array_key_exists( $class_name, $this->mocked_statics ) ) {
$class_mocks = $this->mocked_statics[ $class_name ];
if ( array_key_exists( $method_name, $class_mocks ) ) {
$method_mock = $class_mocks[ $method_name ];
return call_user_func_array( $method_mock, $parameters );
}
}
return parent::call_static( $class_name, $method_name, ...$parameters );
}
/**
* Gets an instance of a given legacy class.
* This must not be used to get instances of classes in the `src` directory.
*
* If a mock has been defined for the requested class using `register_class_mocks`, then the registered
* object will be returned or the registered callback will be used to generate the instance to return.
*
* @param string $class_name The name of the class to get an instance for.
* @param mixed ...$args Parameters to be passed to the callback function or to the parent class method.
*
* @return object The (possibly mocked) instance of the class.
* @throws \Exception The requested class belongs to the `src` directory, or there was an error creating an instance of the class.
*/
public function get_instance_of( string $class_name, ...$args ) {
if ( array_key_exists( $class_name, $this->mocked_classes ) ) {
$mock = $this->mocked_classes[ $class_name ];
return is_callable( $mock ) ? call_user_func_array( $mock, $args ) : $mock;
}
return parent::get_instance_of( $class_name, ...$args );
}
}

View File

@ -22,7 +22,7 @@
"module": "build-module/index.js",
"dependencies": {
"@wordpress/e2e-test-utils": "^4.6.0",
"@wordpress/jest-preset-default": "^5.4.0",
"@wordpress/jest-preset-default": "^6.2.0",
"app-root-path": "^3.0.0",
"jest": "^25.1.0",
"jest-puppeteer": "^4.4.0",

View File

@ -51,13 +51,16 @@ describe( 'Store owner can go through store Setup Wizard', () => {
describe( 'Store owner can go through setup Task List', () => {
it( 'can setup shipping', async () => {
await page.evaluate( () => {
document.querySelector( '.woocommerce-list__item-title' ).scrollIntoView();
} );
// Query for all tasks on the list
const taskListItems = await page.$$( '.woocommerce-list__item-title' );
expect( taskListItems ).toHaveLength( 6 );
await Promise.all( [
// Click on "Set up shipping" task to move to the next step
taskListItems[3].click(),
taskListItems[4].click(),
// Wait for shipping setup section to load
page.waitForNavigation( { waitUntil: 'networkidle0' } ),

View File

@ -88,10 +88,10 @@ const completeOnboardingWizard = async () => {
// Query for the industries checkboxes
const industryCheckboxes = await page.$$( '.components-checkbox-control__input' );
expect( industryCheckboxes ).toHaveLength( 8 );
expect( industryCheckboxes ).toHaveLength( 10 );
// Select all industries including "Other"
for ( let i = 0; i < 8; i++ ) {
for ( let i = 0; i < 10; i++ ) {
await industryCheckboxes[i].click();
}
@ -113,7 +113,7 @@ const completeOnboardingWizard = async () => {
// Query for the product types checkboxes
const productTypesCheckboxes = await page.$$( '.components-checkbox-control__input' );
expect( productTypesCheckboxes ).toHaveLength( 6 );
expect( productTypesCheckboxes ).toHaveLength( 8 );
// Select Physical and Downloadable products
for ( let i = 0; i < 2; i++ ) {
@ -139,22 +139,17 @@ const completeOnboardingWizard = async () => {
// Fill the number of products you plan to sell
await selectControls[0].click();
await page.waitForSelector( '.woocommerce-select-control__listbox' );
await page.waitForSelector( '.woocommerce-select-control__control' );
await expect( page ).toClick( '.woocommerce-select-control__option', { text: config.get( 'onboardingwizard.numberofproducts' ) } );
// Fill currently selling elsewhere
await selectControls[1].click();
await page.waitForSelector( '.woocommerce-select-control__listbox' );
await page.waitForSelector( '.woocommerce-select-control__control' );
await expect( page ).toClick( '.woocommerce-select-control__option', { text: config.get( 'onboardingwizard.sellingelsewhere' ) } );
// Query for the plugin upload toggles
const pluginToggles = await page.$$( '.components-form-toggle__input' );
expect( pluginToggles ).toHaveLength( 3 );
// Disable Market on Facebook, Mailchimp and Google Shopping download
for ( let i = 0; i < 3; i++ ) {
await pluginToggles[i].click();
}
// Disable business extension downloads
const pluginToggle = await page.$( '.woocommerce-business-extensions .components-checkbox-control__input-container' );
pluginToggle.click();
// Wait for "Continue" button to become active
await page.waitForSelector( 'button.is-primary:not(:disabled)' );

View File

@ -6,11 +6,12 @@
* @package WooCommerce Tests
*/
use Automattic\WooCommerce\Proxies\LegacyProxy;
use Automattic\WooCommerce\Testing\Tools\CodeHacking\CodeHacker;
use Automattic\WooCommerce\Testing\Tools\CodeHacking\Hacks\StaticMockerHack;
use Automattic\WooCommerce\Testing\Tools\CodeHacking\Hacks\FunctionsMockerHack;
use Automattic\WooCommerce\Testing\Tools\CodeHacking\Hacks\BypassFinalsHack;
use Composer\Autoload\ClassLoader;
use Automattic\WooCommerce\Testing\Tools\DependencyManagement\MockableLegacyProxy;
/**
* Class WC_Unit_Tests_Bootstrap
@ -35,7 +36,11 @@ class WC_Unit_Tests_Bootstrap {
* @since 2.2
*/
public function __construct() {
$this->tests_dir = dirname( __FILE__ );
$this->tests_dir = dirname( __FILE__ );
$this->plugin_dir = dirname( dirname( $this->tests_dir ) );
$this->register_autoloader_for_testing_tools();
$this->initialize_code_hacker();
ini_set( 'display_errors', 'on' ); // phpcs:ignore WordPress.PHP.IniSet.display_errors_Blacklisted
@ -64,6 +69,32 @@ class WC_Unit_Tests_Bootstrap {
// load WC testing framework.
$this->includes();
// re-initialize dependency injection, this needs to be the last operation after everything else is in place.
$this->initialize_dependency_injection();
}
/**
* Register autoloader for the files in the 'tests/tools' directory, for the root namespace 'Automattic\WooCommerce\Testing\Tools'.
*/
protected static function register_autoloader_for_testing_tools() {
return spl_autoload_register(
function ( $class ) {
$prefix = 'Automattic\\WooCommerce\\Testing\\Tools\\';
$base_dir = dirname( dirname( __FILE__ ) ) . '/Tools/';
$len = strlen( $prefix );
if ( strncmp( $prefix, $class, $len ) !== 0 ) {
// no, move to the next registered autoloader.
return;
}
$relative_class = substr( $class, $len );
$file = $base_dir . str_replace( '\\', '/', $relative_class ) . '.php';
if ( ! file_exists( $file ) ) {
throw new \Exception( 'Autoloader for unit tests: file not found: ' . $file );
}
require $file;
}
);
}
/**
@ -72,15 +103,6 @@ class WC_Unit_Tests_Bootstrap {
* @throws Exception Error when initializing one of the hacks.
*/
private function initialize_code_hacker() {
$this->plugin_dir = dirname( dirname( $this->tests_dir ) );
$hacking_base = $this->plugin_dir . '/tests/Tools/CodeHacking';
require_once $hacking_base . '/CodeHacker.php';
require_once $hacking_base . '/Hacks/CodeHack.php';
require_once $hacking_base . '/Hacks/StaticMockerHack.php';
require_once $hacking_base . '/Hacks/FunctionsMockerHack.php';
require_once $hacking_base . '/Hacks/BypassFinalsHack.php';
CodeHacker::initialize( array( __DIR__ . '/../../includes/' ) );
$replaceable_functions = include_once __DIR__ . '/mockable-functions.php';
if ( ! empty( $replaceable_functions ) ) {
@ -99,6 +121,35 @@ class WC_Unit_Tests_Bootstrap {
CodeHacker::enable();
}
/**
* Re-initialize the dependency injection engine.
*
* The dependency injection engine has been already initialized as part of the Woo initialization, but we need
* to replace the registered read-only container with a fully configurable one for testing.
* To this end we hack a bit and use reflection to grab the underlying container that the read-only one stores
* in a private property.
*
* Additionally, we replace the legacy/function proxies with mockable versions to easily replace anything
* in tests as appropriate.
*
* @throws \Exception The Container class doesn't have a 'container' property.
*/
private function initialize_dependency_injection() {
try {
$inner_container_property = new \ReflectionProperty( \Automattic\WooCommerce\Container::class, 'container' );
} catch ( ReflectionException $ex ) {
throw new \Exception( "Error when trying to get the private 'container' property from the " . \Automattic\WooCommerce\Container::class . ' class using reflection during unit testing bootstrap, has the property been removed or renamed?' );
}
$inner_container_property->setAccessible( true );
$inner_container = $inner_container_property->getValue( wc_get_container() );
$inner_container->replace( LegacyProxy::class, MockableLegacyProxy::class );
$inner_container->reset_all_resolved();
$GLOBALS['wc_container'] = $inner_container;
}
/**
* Load WooCommerce.
*

View File

@ -1,4 +1,10 @@
<?php
/**
* Base class for REST API test classes.
*
* @package WooCommerce\Tests\Framework
*/
/**
* WC API Unit Test Case
*
@ -19,18 +25,16 @@ class WC_API_Unit_Test_Case extends WC_Unit_Test_Case {
parent::setUp();
// load API classes
// load API classes.
WC()->api->includes();
// set user
$this->user_id = $this->factory->user->create( array( 'role' => 'shop_manager' ) );
wp_set_current_user( $this->user_id );
// set user.
$this->user_id = $this->login_as_role( 'shop_manager' );
// this isn't used, but it causes a warning unless set
// this isn't used, but it causes a warning unless set.
$_SERVER['REQUEST_METHOD'] = null;
// mock the API server to prevent headers from being sent
// $this->mock_server = $this->getMock( 'WC_API_Server', array( 'header' ), array( '/' ) );
// mock the API server to prevent headers from being sent.
$this->mock_server = $this->getMockBuilder( 'WC_API_Server' )->setMethods( array( 'header' ) )->disableOriginalConstructor()->getMock();
WC()->api->register_resources( $this->mock_server );
@ -40,19 +44,19 @@ class WC_API_Unit_Test_Case extends WC_Unit_Test_Case {
* Assert the given response is an API error with a specific code and status.
*
* @since 2.2
* @param string $code error code, e.g. `woocommerce_api_user_cannot_read_orders_count`
* @param int|null $status HTTP status code associated with error, e.g. 400
* @param WP_Error $response
* @param string $message optional message to render when assertion fails
* @param string $code error code, e.g. `woocommerce_api_user_cannot_read_orders_count`.
* @param int|null $status HTTP status code associated with error, e.g. 400.
* @param WP_Error $response Response to assert.
* @param string $message optional message to render when assertion fails.
*/
public function assertHasAPIError( $code, $status = null, $response, $message = '' ) {
$this->assertWPError( $response, $message );
// code
// code.
$this->assertEquals( $code, $response->get_error_code(), $message );
// status
// status.
$data = $response->get_error_data();
$this->assertArrayHasKey( 'status', $data, $message );
@ -65,14 +69,14 @@ class WC_API_Unit_Test_Case extends WC_Unit_Test_Case {
* permission checking.
*
* @since 2.2
* @param string $capability, e.g. `read_private_shop_orders`
* @param string $capability e.g. `read_private_shop_orders`.
*/
protected function disable_capability( $capability ) {
$user = wp_get_current_user();
$user->add_cap( $capability, false );
// flush capabilities, see https://core.trac.wordpress.org/ticket/28374
// flush capabilities, see https://core.trac.wordpress.org/ticket/28374.
$user->get_role_caps();
$user->update_user_level_from_caps();
}

View File

@ -5,6 +5,7 @@
* @package WooCommerce\Tests
*/
use Automattic\WooCommerce\Proxies\LegacyProxy;
use Automattic\WooCommerce\Testing\Tools\CodeHacking\CodeHacker;
/**
@ -133,7 +134,6 @@ class WC_Unit_Test_Case extends WP_HTTP_TestCase {
throw new Exception( $message, $code );
}
/**
* Copies a file, temporarily disabling the code hacker.
* Use this instead of "copy" in tests for compatibility with the code hacker.
@ -148,6 +148,102 @@ class WC_Unit_Test_Case extends WP_HTTP_TestCase {
self::disable_code_hacker();
$result = copy( $source, $dest );
self::reenable_code_hacker();
return $result;
}
/**
* Create a new user in a given role and set it as the current user.
*
* @param string $role The role for the user to be created.
* @return int The id of the user created.
*/
public function login_as_role( $role ) {
$user_id = $this->factory->user->create( array( 'role' => $role ) );
wp_set_current_user( $user_id );
return $user_id;
}
/**
* Create a new administrator user and set it as the current user.
*
* @return int The id of the user created.
*/
public function login_as_administrator() {
return $this->login_as_role( 'administrator' );
}
/**
* Get an instance of a class that has been registered in the dependency injection container.
* To get an instance of a legacy class (such as the ones in the 'íncludes' directory) use
* 'get_legacy_instance_of' instead.
*
* @param string $class_name The class name to get an instance of.
*
* @return mixed The instance.
*/
public function get_instance_of( string $class_name ) {
return wc_get_container()->get( $class_name );
}
/**
* Get an instance of legacy class (such as the ones in the 'íncludes' directory).
* To get an instance of a class registered in the dependency injection container use 'get_instance_of' instead.
*
* @param string $class_name The class name to get an instance of.
*
* @return mixed The instance.
*/
public function get_legacy_instance_of( string $class_name ) {
return wc_get_container()->get( LegacyProxy::class )->get_instance_of( $class_name );
}
/**
* Reset all the cached resolutions in the dependency injection container, so any further "get"
* for shared definitions will generate the instance again.
* This may be needed when registering mocks for already resolved shared classes.
*/
public function reset_container_resolutions() {
wc_get_container()->reset_all_resolved();
}
/**
* Reset the mock legacy proxy class so that all the registered mocks are unregistered.
*/
public function reset_legacy_proxy_mocks() {
wc_get_container()->get( LegacyProxy::class )->reset();
}
/**
* Register the function mocks to use in the mockable LegacyProxy.
*
* @param array $mocks An associative array where keys are function names and values are function replacement callbacks.
*
* @throws \Exception Invalid parameter.
*/
public function register_legacy_proxy_function_mocks( array $mocks ) {
wc_get_container()->get( LegacyProxy::class )->register_function_mocks( $mocks );
}
/**
* Register the static method mocks to use in the mockable LegacyProxy.
*
* @param array $mocks An associative array where keys are class names and values are associative arrays, in which keys are method names and values are method replacement callbacks.
*
* @throws \Exception Invalid parameter.
*/
public function register_legacy_proxy_static_mocks( array $mocks ) {
wc_get_container()->get( LegacyProxy::class )->register_static_mocks( $mocks );
}
/**
* Register the class mocks to use in the mockable LegacyProxy.
*
* @param array $mocks An associative array where keys are class names and values are either factory callbacks (optionally with a $class_name argument) or objects.
*
* @throws \Exception Invalid parameter.
*/
public function register_legacy_proxy_class_mocks( array $mocks ) {
wc_get_container()->get( LegacyProxy::class )->register_class_mocks( $mocks );
}
}

View File

@ -28,12 +28,13 @@ class WC_Helper_Product {
* Create simple product.
*
* @since 2.3
* @param bool $save Save or return object.
* @param bool $save Save or return object.
* @param array $props Properties to be set in the new product, as an associative array.
* @return WC_Product_Simple
*/
public static function create_simple_product( $save = true ) {
$product = new WC_Product_Simple();
$product->set_props(
public static function create_simple_product( $save = true, $props = array() ) {
$product = new WC_Product_Simple();
$default_props =
array(
'name' => 'Dummy Product',
'regular_price' => 10,
@ -45,8 +46,9 @@ class WC_Helper_Product {
'virtual' => false,
'stock_status' => 'instock',
'weight' => '1.1',
)
);
);
$product->set_props( array_merge( $default_props, $props ) );
if ( $save ) {
$product->save();
@ -101,14 +103,19 @@ class WC_Helper_Product {
}
/**
* Create a dummy variation product.
* Create a dummy variation product or configure an existing product object with dummy data.
*
*
* @since 2.3
*
* @param WC_Product_Variable|null $product Product object to configure, or null to create a new one.
* @return WC_Product_Variable
*/
public static function create_variation_product() {
$product = new WC_Product_Variable();
public static function create_variation_product( $product = null ) {
$is_new_product = is_null( $product );
if ( $is_new_product ) {
$product = new WC_Product_Variable();
}
$product->set_props(
array(
'name' => 'Dummy Variable Product',
@ -118,96 +125,132 @@ class WC_Helper_Product {
$attributes = array();
$attribute = new WC_Product_Attribute();
$attribute_data = self::create_attribute( 'size', array( 'small', 'large', 'huge' ) );
$attribute->set_id( $attribute_data['attribute_id'] );
$attribute->set_name( $attribute_data['attribute_taxonomy'] );
$attribute->set_options( $attribute_data['term_ids'] );
$attribute->set_position( 1 );
$attribute->set_visible( true );
$attribute->set_variation( true );
$attributes[] = $attribute;
$attribute = new WC_Product_Attribute();
$attribute_data = self::create_attribute( 'colour', array( 'red', 'blue' ) );
$attribute->set_id( $attribute_data['attribute_id'] );
$attribute->set_name( $attribute_data['attribute_taxonomy'] );
$attribute->set_options( $attribute_data['term_ids'] );
$attribute->set_position( 1 );
$attribute->set_visible( true );
$attribute->set_variation( true );
$attributes[] = $attribute;
$attribute = new WC_Product_Attribute();
$attribute_data = self::create_attribute( 'number', array( '0', '1', '2' ) );
$attribute->set_id( $attribute_data['attribute_id'] );
$attribute->set_name( $attribute_data['attribute_taxonomy'] );
$attribute->set_options( $attribute_data['term_ids'] );
$attribute->set_position( 1 );
$attribute->set_visible( true );
$attribute->set_variation( true );
$attributes[] = $attribute;
$attributes[] = self::create_product_attribute_object( 'size', array( 'small', 'large', 'huge' ) );
$attributes[] = self::create_product_attribute_object( 'colour', array( 'red', 'blue' ) );
$attributes[] = self::create_product_attribute_object( 'number', array( '0', '1', '2' ) );
$product->set_attributes( $attributes );
$product->save();
$variation_1 = new WC_Product_Variation();
$variation_1->set_props(
array(
'parent_id' => $product->get_id(),
'sku' => 'DUMMY SKU VARIABLE SMALL',
'regular_price' => 10,
)
);
$variation_1->set_attributes( array( 'pa_size' => 'small' ) );
$variation_1->save();
$variations = array();
$variation_2 = new WC_Product_Variation();
$variation_2->set_props(
array(
'parent_id' => $product->get_id(),
'sku' => 'DUMMY SKU VARIABLE LARGE',
'regular_price' => 15,
)
$variations[] = self::create_product_variation_object(
$product->get_id(),
'DUMMY SKU VARIABLE SMALL',
10,
array( 'pa_size' => 'small' )
);
$variation_2->set_attributes( array( 'pa_size' => 'large' ) );
$variation_2->save();
$variation_3 = new WC_Product_Variation();
$variation_3->set_props(
array(
'parent_id' => $product->get_id(),
'sku' => 'DUMMY SKU VARIABLE HUGE RED 0',
'regular_price' => 16,
)
$variations[] = self::create_product_variation_object(
$product->get_id(),
'DUMMY SKU VARIABLE LARGE',
15,
array( 'pa_size' => 'large' )
);
$variation_3->set_attributes(
$variations[] = self::create_product_variation_object(
$product->get_id(),
'DUMMY SKU VARIABLE HUGE RED 0',
16,
array(
'pa_size' => 'huge',
'pa_colour' => 'red',
'pa_number' => '0',
)
);
$variation_3->save();
$variation_4 = new WC_Product_Variation();
$variation_4->set_props(
array(
'parent_id' => $product->get_id(),
'sku' => 'DUMMY SKU VARIABLE HUGE RED 2',
'regular_price' => 17,
)
);
$variation_4->set_attributes(
$variations[] = self::create_product_variation_object(
$product->get_id(),
'DUMMY SKU VARIABLE HUGE RED 2',
17,
array(
'pa_size' => 'huge',
'pa_colour' => 'red',
'pa_number' => '2',
)
);
$variation_4->save();
return wc_get_product( $product->get_id() );
$variations[] = self::create_product_variation_object(
$product->get_id(),
'DUMMY SKU VARIABLE HUGE BLUE 2',
18,
array(
'pa_size' => 'huge',
'pa_colour' => 'blue',
'pa_number' => '2',
)
);
$variations[] = self::create_product_variation_object(
$product->get_id(),
'DUMMY SKU VARIABLE HUGE BLUE ANY NUMBER',
19,
array(
'pa_size' => 'huge',
'pa_colour' => 'blue',
'pa_number' => '',
)
);
if ( $is_new_product ) {
return wc_get_product( $product->get_id() );
}
$variation_ids = array_map(
function( $variation ) {
return $variation->get_id();
},
$variations
);
$product->set_children( $variation_ids );
return $product;
}
/**
* Creates an instance of WC_Product_Variation with the supplied parameters, optionally persisting it to the database.
*
* @param string $parent_id Parent product id.
* @param string $sku SKU for the variation.
* @param int $price Price of the variation.
* @param array $attributes Attributes that define the variation, e.g. ['pa_color'=>'red'].
* @param bool $save If true, the object will be saved to the database after being created and configured.
*
* @return WC_Product_Variation The created object.
*/
public static function create_product_variation_object( $parent_id, $sku, $price, $attributes, $save = true ) {
$variation = new WC_Product_Variation();
$variation->set_props(
array(
'parent_id' => $parent_id,
'sku' => $sku,
'regular_price' => $price,
)
);
$variation->set_attributes( $attributes );
if ( $save ) {
$variation->save();
}
return $variation;
}
/**
* Creates an instance of WC_Product_Attribute with the supplied parameters.
*
* @param string $raw_name Attribute raw name (without 'pa_' prefix).
* @param array $terms Possible values for the attribute.
*
* @return WC_Product_Attribute The created attribute object.
*/
public static function create_product_attribute_object( $raw_name = 'size', $terms = array( 'small' ) ) {
$attribute = new WC_Product_Attribute();
$attribute_data = self::create_attribute( $raw_name, $terms );
$attribute->set_id( $attribute_data['attribute_id'] );
$attribute->set_name( $attribute_data['attribute_taxonomy'] );
$attribute->set_options( $attribute_data['term_ids'] );
$attribute->set_position( 1 );
$attribute->set_visible( true );
$attribute->set_variation( true );
return $attribute;
}
/**

View File

@ -1,4 +1,9 @@
<?php
/**
* Helper class for shipping related unit tests.
*
* @package WooCommerce\Tests|Helper
*/
/**
* Class WC_Helper_Shipping.
@ -11,15 +16,17 @@ class WC_Helper_Shipping {
* Create a simple flat rate at the cost of 10.
*
* @since 2.3
*
* @param float $cost Optional. Cost of flat rate method.
*/
public static function create_simple_flat_rate() {
public static function create_simple_flat_rate( $cost = 10 ) {
$flat_rate_settings = array(
'enabled' => 'yes',
'title' => 'Flat rate',
'availability' => 'all',
'countries' => '',
'tax_status' => 'taxable',
'cost' => '10',
'cost' => $cost,
);
update_option( 'woocommerce_flat_rate_settings', $flat_rate_settings );
@ -28,6 +35,48 @@ class WC_Helper_Shipping {
WC()->shipping()->load_shipping_methods();
}
/**
* Helper function to set customer address so that shipping can be calculated.
*/
public static function force_customer_us_address() {
add_filter( 'woocommerce_customer_get_shipping_country', array( self::class, 'force_customer_us_country' ) );
add_filter( 'woocommerce_customer_get_shipping_state', array( self::class, 'force_customer_us_state' ) );
add_filter( 'woocommerce_customer_get_shipping_postcode', array( self::class, 'force_customer_us_postcode' ) );
}
/**
* Helper that can be hooked to a filter to force the customer's shipping state to be NY.
*
* @since 4.4.0
* @param string $state State code.
* @return string
*/
public static function force_customer_us_state( $state ) {
return 'NY';
}
/**
* Helper that can be hooked to a filter to force the customer's shipping country to be US.
*
* @since 4.4.0
* @param string $country Country code.
* @return string
*/
public static function force_customer_us_country( $country ) {
return 'US';
}
/**
* Helper that can be hooked to a filter to force the customer's shipping postal code to be 12345.
*
* @since 4.4.0
* @param string $postcode Postal code.
* @return string
*/
public static function force_customer_us_postcode( $postcode ) {
return '12345';
}
/**
* Delete the simple flat rate.
*

View File

@ -66,20 +66,20 @@ class WC_Tests_Admin_Duplicate_Product extends WC_Unit_Test_Case {
array(
'dummy-variable-product-small-2',
'dummy-variable-product-large-2',
'dummy-variable-product-3',
'dummy-variable-product-4',
),
array(
'dummy-variable-product-small-3',
'dummy-variable-product-large-3',
'dummy-variable-product-5',
'dummy-variable-product-6',
),
array(
'dummy-variable-product-small-3',
'dummy-variable-product-large-3',
'dummy-variable-product-9',
'dummy-variable-product-10',
),
array(
'dummy-variable-product-small-4',
'dummy-variable-product-large-4',
'dummy-variable-product-7',
'dummy-variable-product-8',
'dummy-variable-product-13',
'dummy-variable-product-14',
),
);
@ -88,7 +88,7 @@ class WC_Tests_Admin_Duplicate_Product extends WC_Unit_Test_Case {
$duplicate_children = $duplicate->get_children();
$this->assertEquals( 4, count( $duplicate_children ) );
$this->assertEquals( 6, count( $duplicate_children ) );
foreach ( $slug_match as $key => $slug ) {
$child = wc_get_product( $duplicate_children[ $key ] );
$this->assertEquals( $slug, $child->get_slug() );

View File

@ -62,10 +62,8 @@ class WC_Tests_Cart extends WC_Unit_Test_Case {
'cost' => '9.59',
);
update_option( 'woocommerce_flat_rate_settings', $flat_rate_settings );
// Set an address so that shipping can be calculated.
add_filter( 'woocommerce_customer_get_shipping_country', array( $this, 'force_customer_us_country' ) );
add_filter( 'woocommerce_customer_get_shipping_state', array( $this, 'force_customer_us_state' ) );
add_filter( 'woocommerce_customer_get_shipping_postcode', array( $this, 'force_customer_us_postcode' ) );
WC_Helper_Shipping::force_customer_us_address();
WC()->cart->add_to_cart( $product->get_id(), 1 );
WC()->cart->add_discount( $coupon->get_code() );
@ -177,9 +175,7 @@ class WC_Tests_Cart extends WC_Unit_Test_Case {
update_option( 'woocommerce_tax_round_at_subtotal', 'yes' );
// Set an address so that shipping can be calculated.
add_filter( 'woocommerce_customer_get_shipping_country', array( $this, 'force_customer_us_country' ) );
add_filter( 'woocommerce_customer_get_shipping_state', array( $this, 'force_customer_us_state' ) );
add_filter( 'woocommerce_customer_get_shipping_postcode', array( $this, 'force_customer_us_postcode' ) );
WC_Helper_Shipping::force_customer_us_address();
// Create tax classes first.
WC_Tax::create_tax_class( '23percent' );
@ -620,9 +616,7 @@ class WC_Tests_Cart extends WC_Unit_Test_Case {
WC()->cart->empty_cart();
remove_filter( 'woocommerce_customer_get_shipping_country', array( $this, 'force_customer_gb_country' ) );
remove_filter( 'woocommerce_customer_get_shipping_postcode', array( $this, 'force_customer_gb_postcode' ) );
add_filter( 'woocommerce_customer_get_shipping_country', array( $this, 'force_customer_us_country' ) );
add_filter( 'woocommerce_customer_get_shipping_state', array( $this, 'force_customer_us_state' ) );
add_filter( 'woocommerce_customer_get_shipping_postcode', array( $this, 'force_customer_us_postcode' ) );
WC_Helper_Shipping::force_customer_us_address();
WC()->cart->add_to_cart( $product->get_id(), 1 );
// Test out of store location with no coupon.
@ -767,9 +761,7 @@ class WC_Tests_Cart extends WC_Unit_Test_Case {
WC()->cart->empty_cart();
remove_filter( 'woocommerce_customer_get_shipping_country', array( $this, 'force_customer_gb_country' ) );
remove_filter( 'woocommerce_customer_get_shipping_postcode', array( $this, 'force_customer_gb_postcode' ) );
add_filter( 'woocommerce_customer_get_shipping_country', array( $this, 'force_customer_us_country' ) );
add_filter( 'woocommerce_customer_get_shipping_state', array( $this, 'force_customer_us_state' ) );
add_filter( 'woocommerce_customer_get_shipping_postcode', array( $this, 'force_customer_us_postcode' ) );
WC_Helper_Shipping::force_customer_us_address();
WC()->cart->add_to_cart( $product->get_id(), 1 );
// Test out of store location with no coupon.
@ -872,9 +864,7 @@ class WC_Tests_Cart extends WC_Unit_Test_Case {
$full_coupon->set_amount( 100 );
$full_coupon->save();
add_filter( 'woocommerce_customer_get_shipping_country', array( $this, 'force_customer_us_country' ) );
add_filter( 'woocommerce_customer_get_shipping_state', array( $this, 'force_customer_us_state' ) );
add_filter( 'woocommerce_customer_get_shipping_postcode', array( $this, 'force_customer_us_postcode' ) );
WC_Helper_Shipping::force_customer_us_address();
WC()->cart->add_to_cart( $product->get_id(), 1 );
// Test out of store location with no coupon.
@ -941,7 +931,7 @@ class WC_Tests_Cart extends WC_Unit_Test_Case {
* @return string
*/
public function force_customer_us_country( $country ) {
return 'US';
return WC_Helper_Shipping::force_customer_us_country( $country );
}
/**
@ -952,7 +942,7 @@ class WC_Tests_Cart extends WC_Unit_Test_Case {
* @return string
*/
public function force_customer_us_state( $state ) {
return 'NY';
return WC_Helper_Shipping::force_customer_us_state( $state );
}
/**
@ -963,7 +953,7 @@ class WC_Tests_Cart extends WC_Unit_Test_Case {
* @return string
*/
public function force_customer_us_postcode( $postcode ) {
return '12345';
return WC_Helper_Shipping::force_customer_us_postcode( $postcode );
}
/**
@ -989,9 +979,7 @@ class WC_Tests_Cart extends WC_Unit_Test_Case {
update_option( 'woocommerce_calc_taxes', 'yes' );
// Set an address so that shipping can be calculated.
add_filter( 'woocommerce_customer_get_shipping_country', array( $this, 'force_customer_us_country' ) );
add_filter( 'woocommerce_customer_get_shipping_state', array( $this, 'force_customer_us_state' ) );
add_filter( 'woocommerce_customer_get_shipping_postcode', array( $this, 'force_customer_us_postcode' ) );
WC_Helper_Shipping::force_customer_us_address();
// 19% tax.
$tax_rate = array(
@ -1549,9 +1537,7 @@ class WC_Tests_Cart extends WC_Unit_Test_Case {
WC()->cart->add_to_cart( $product->get_id(), 1 );
// Set an address so that shipping can be calculated.
add_filter( 'woocommerce_customer_get_shipping_country', array( $this, 'force_customer_us_country' ) );
add_filter( 'woocommerce_customer_get_shipping_state', array( $this, 'force_customer_us_state' ) );
add_filter( 'woocommerce_customer_get_shipping_postcode', array( $this, 'force_customer_us_postcode' ) );
WC_Helper_Shipping::force_customer_us_address();
// Set the flat_rate shipping method.
WC()->session->set( 'chosen_shipping_methods', array( 'flat_rate' ) );
@ -1564,6 +1550,65 @@ class WC_Tests_Cart extends WC_Unit_Test_Case {
$this->assertEquals( 20, WC()->cart->total );
}
/**
* Test that shipping tax rounding does not round down when price are inclusive of taxes.
*/
public function test_calculate_totals_shipping_tax_rounded_26654() {
update_option( 'woocommerce_prices_include_tax', 'yes' );
update_option( 'woocommerce_calc_taxes', 'yes' );
update_option( 'woocommerce_tax_round_at_subtotal', 'yes' );
$tax_rate = array(
'tax_rate_country' => '',
'tax_rate_state' => '',
'tax_rate' => '25.0000',
'tax_rate_name' => 'Tax @ 25%',
'tax_rate_priority' => '1',
'tax_rate_compound' => '0',
'tax_rate_shipping' => '1',
'tax_rate_order' => '1',
'tax_rate_class' => 'standard',
);
WC_Tax::_insert_tax_rate( $tax_rate );
$product = WC_Helper_Product::create_simple_product();
$product->set_regular_price( 242 );
$product->set_tax_class( 'product' );
$product->save();
WC_Helper_Shipping::create_simple_flat_rate( 75.10 );
WC_Helper_Shipping::force_customer_us_address();
WC()->cart->empty_cart();
WC()->cart->add_to_cart( $product->get_id(), 1 );
WC()->session->set( 'chosen_shipping_methods', array( 'flat_rate' ) );
WC()->cart->calculate_totals();
$this->assertEquals( 18.775, WC()->cart->get_shipping_tax() );
$this->assertEquals( 335.88, WC()->cart->get_total( 'edit' ) );
$this->assertEquals( 67.18, WC()->cart->get_taxes_total() );
$checkout = WC_Checkout::instance();
$order = new WC_Order();
$checkout->set_data_from_cart( $order );
$this->assertEquals( 67.18, $order->get_total_tax() );
$this->assertEquals( 335.88, $order->get_total() );
$this->assertEquals( 18.775, $order->get_shipping_tax() );
update_option( 'woocommerce_tax_round_at_subtotal', 'no' );
WC()->cart->calculate_totals();
$this->assertEquals( 18.78, WC()->cart->get_shipping_tax() );
$this->assertEquals( 335.88, WC()->cart->get_total( 'edit' ) );
$this->assertEquals( 67.18, WC()->cart->get_taxes_total() );
$order = new WC_Order();
$checkout->set_data_from_cart( $order );
$this->assertEquals( 67.18, $order->get_total_tax() );
$this->assertEquals( 335.88, $order->get_total() );
$this->assertEquals( 18.78, $order->get_shipping_tax() );
}
/**
* Test cart fee.
*
@ -2022,7 +2067,7 @@ class WC_Tests_Cart extends WC_Unit_Test_Case {
update_option( 'woocommerce_tax_round_at_subtotal', 'yes' );
WC()->cart->empty_cart();
$tax_rate = array(
$tax_rate = array(
'tax_rate_country' => '',
'tax_rate_state' => '',
'tax_rate' => '10.0000',
@ -2067,7 +2112,14 @@ class WC_Tests_Cart extends WC_Unit_Test_Case {
$product = WC_Helper_Product::create_variation_product();
$variations = $product->get_available_variations();
$variation = array_pop( $variations );
$variation = current(
array_filter(
$variations,
function( $variation ) {
return 'DUMMY SKU VARIABLE HUGE RED 2' === $variation['sku'];
}
)
);
// Add variation with add_to_cart_action.
$_REQUEST['add-to-cart'] = $variation['variation_id'];
@ -2092,7 +2144,7 @@ class WC_Tests_Cart extends WC_Unit_Test_Case {
$variation['variation_id'],
array(
'attribute_pa_size' => 'huge',
'attribute_pa_colour' => 'red',
'attribute_pa_colour' => 'red',
'attribute_pa_number' => '2',
)
);
@ -2121,7 +2173,7 @@ class WC_Tests_Cart extends WC_Unit_Test_Case {
$variation = array_pop( $variations );
// Attempt adding variation with add_to_cart_action, specifying a different colour.
$_REQUEST['add-to-cart'] = $variation['variation_id'];
$_REQUEST['add-to-cart'] = $variation['variation_id'];
$_REQUEST['attribute_pa_colour'] = 'green';
WC_Form_Handler::add_to_cart_action( false );
$notices = WC()->session->get( 'wc_notices', array() );
@ -2152,7 +2204,7 @@ class WC_Tests_Cart extends WC_Unit_Test_Case {
$variation = array_shift( $variations );
// Attempt adding variation with add_to_cart_action, specifying attributes not defined in the variation.
$_REQUEST['add-to-cart'] = $variation['variation_id'];
$_REQUEST['add-to-cart'] = $variation['variation_id'];
$_REQUEST['attribute_pa_colour'] = 'red';
$_REQUEST['attribute_pa_number'] = '1';
WC_Form_Handler::add_to_cart_action( false );
@ -2186,7 +2238,7 @@ class WC_Tests_Cart extends WC_Unit_Test_Case {
$variation = array_shift( $variations );
// Attempt adding variation with add_to_cart_action, without specifying attribute_pa_colour.
$_REQUEST['add-to-cart'] = $variation['variation_id'];
$_REQUEST['add-to-cart'] = $variation['variation_id'];
$_REQUEST['attribute_pa_number'] = '0';
WC_Form_Handler::add_to_cart_action( false );
$notices = WC()->session->get( 'wc_notices', array() );

View File

@ -355,12 +355,7 @@ class WC_Tests_Paypal_Gateway_Request extends WC_Unit_Test_Case {
*/
public function test_request_url() {
// User set up.
$this->user = $this->factory->user->create(
array(
'role' => 'administrator',
)
);
wp_set_current_user( $this->user );
$this->login_as_administrator();
// wc_tax_enabled(), wc_prices_include_tax() and WC_Gateway_Paypal_Request::prepare_line_items() determine if
// shipping tax should be included, these are the correct options.
@ -371,6 +366,7 @@ class WC_Tests_Paypal_Gateway_Request extends WC_Unit_Test_Case {
// woocommerce_calc_taxes, woocommerce_prices_include_tax, $shipping_tax_included values.
array( 'no', 'no', false ),
array( 'yes', 'no', false ),
// phpcs:ignore Squiz.PHP.CommentedOutCode.Found
// array( 'no', 'yes', false ), // this is not a valid option due to definition of wc_prices_include_tax().
array( 'yes', 'yes', true ),
);
@ -399,7 +395,7 @@ class WC_Tests_Paypal_Gateway_Request extends WC_Unit_Test_Case {
// Many items in order -> forced to use one line item -> shipping tax included.
$this->check_large_order( true, $testmode );
// Test removing tags from line item name
// Test removing tags from line item name.
$this->check_product_title_containing_html( $testmode );
// Test amount < 0.

View File

@ -222,33 +222,35 @@ class WC_Tests_Order_Item_Product extends WC_Unit_Test_Case {
// Test line_subtotal.
$this->assertTrue( isset( $item['line_subtotal'] ) );
$item['line_subtotal'] = 50;
$item->set_subtotal( 50 );
$this->assertEquals( 50, $item->get_subtotal() );
$this->assertEquals( $item->get_subtotal(), $item['line_subtotal'] );
// Test line_subtotal_tax.
$this->assertTrue( isset( $item['line_subtotal_tax'] ) );
$item['line_subtotal_tax'] = 5;
$item->set_subtotal_tax( 5 );
$this->assertEquals( 5, $item->get_subtotal_tax() );
$this->assertEquals( $item->get_subtotal_tax(), $item['line_subtotal_tax'] );
// Test line_total.
$this->assertTrue( isset( $item['line_total'] ) );
$item['line_total'] = 55;
$item->set_total( 55 );
$this->assertEquals( 55, $item->get_total() );
$this->assertEquals( $item->get_total(), $item['line_total'] );
// Test line_tax.
$this->assertTrue( isset( $item['line_tax'] ) );
$item['line_tax'] = 5;
$item->set_total_tax( 5 );
$this->assertEquals( 5, $item->get_total_tax() );
$this->assertEquals( $item->get_total_tax(), $item['line_tax'] );
// Test line_tax_data.
$this->assertTrue( isset( $item['line_tax_data'] ) );
$item['line_tax_data'] = array(
'total' => array( 5 ),
'subtotal' => array( 5 ),
$item->set_taxes(
array(
'total' => array( 5 ),
'subtotal' => array( 5 ),
)
);
$this->assertEquals(
array(
@ -261,26 +263,21 @@ class WC_Tests_Order_Item_Product extends WC_Unit_Test_Case {
// Test qty.
$this->assertTrue( isset( $item['qty'] ) );
$item['qty'] = 150;
$item->set_quantity( 150 );
$this->assertEquals( 150, $item->get_quantity() );
$this->assertEquals( $item->get_quantity(), $item['qty'] );
// Test item_meta_array.
$this->assertTrue( isset( $item['item_meta_array'] ) );
$item['item_meta_array'] = array(
0 => (object) array(
'key' => 'test',
'value' => 'val',
),
);
$item->update_meta_data( 'test', 'val', 0 );
$this->assertInstanceOf( 'WC_Meta_Data', current( $item->get_meta_data() ) );
$this->assertEquals( current( $item->get_meta_data() ), $item['item_meta_array'][''] );
unset( $item['item_meta_array'] );
$this->assertEquals( array(), $item->get_meta_data() );
// Test default.
$this->assertFalse( isset( $item['foo'] ) );
$item['foo'] = 'bar';
$this->assertFalse( $item->meta_exists( 'foo' ) );
$item->add_meta_data( 'foo', 'bar' );
$this->assertEquals( 'bar', $item->get_meta( 'foo' ) );
}
}

View File

@ -1,22 +1,30 @@
<?php
/**
* Payment_Tokens class file.
*
* @package WooCommerce\Tests\Payment_Tokens
*/
/**
* Class Payment_Tokens
* @package WooCommerce\Tests\Payment_Tokens
*/
class WC_Tests_Payment_Tokens extends WC_Unit_Test_Case {
/**
* Setup the test case.
*
* @see WC_Unit_Test_Case::setUp()
*/
public function setUp() {
parent::setUp();
$this->user_id = $this->factory->user->create( array( 'role' => 'shop_manager' ) );
wp_set_current_user( $this->user_id );
$this->user_id = $this->login_as_role( 'shop_manager' );
}
/**
* Test getting tokens associated with an order.
* @since 2.6.0
*/
function test_wc_payment_tokens_get_order_tokens() {
public function test_wc_payment_tokens_get_order_tokens() {
$order = WC_Helper_Order::create_order();
$this->assertEmpty( WC_Payment_Tokens::get_order_tokens( $order->get_id() ) );
@ -31,7 +39,7 @@ class WC_Tests_Payment_Tokens extends WC_Unit_Test_Case {
* Test getting tokens associated with a user and no gateway ID.
* @since 2.6.0
*/
function test_wc_payment_tokens_get_customer_tokens_no_gateway() {
public function test_wc_payment_tokens_get_customer_tokens_no_gateway() {
$this->assertEmpty( WC_Payment_Tokens::get_customer_tokens( $this->user_id ) );
$token = WC_Helper_Payment_Token::create_cc_token();
@ -49,7 +57,7 @@ class WC_Tests_Payment_Tokens extends WC_Unit_Test_Case {
* Test getting tokens associated with a user and for a specific gateway.
* @since 2.6.0
*/
function test_wc_payment_tokens_get_customer_tokens_with_gateway() {
public function test_wc_payment_tokens_get_customer_tokens_with_gateway() {
$this->assertEmpty( WC_Payment_Tokens::get_customer_tokens( $this->user_id ) );
$token = WC_Helper_Payment_Token::create_cc_token();
@ -74,7 +82,7 @@ class WC_Tests_Payment_Tokens extends WC_Unit_Test_Case {
* Test getting a customers default token.
* @since 2.6.0
*/
function test_wc_get_customer_default_token() {
public function test_wc_get_customer_default_token() {
$token = WC_Helper_Payment_Token::create_cc_token();
$token->set_user_id( $this->user_id );
$token->set_gateway_id( 'bacs' );
@ -99,7 +107,7 @@ class WC_Tests_Payment_Tokens extends WC_Unit_Test_Case {
* @group failing
* @since 2.6.0
*/
function test_wc_get_customer_default_token_returns_first_created_when_no_default_token_set() {
public function test_wc_get_customer_default_token_returns_first_created_when_no_default_token_set() {
$token = WC_Helper_Payment_Token::create_cc_token( $this->user_id );
$token->set_gateway_id( 'bacs' );
$token->save();
@ -118,7 +126,7 @@ class WC_Tests_Payment_Tokens extends WC_Unit_Test_Case {
* Test getting a token by ID.
* @since 2.6.0
*/
function test_wc_payment_tokens_get() {
public function test_wc_payment_tokens_get() {
$token = WC_Helper_Payment_Token::create_cc_token();
$token_id = $token->get_id();
$get_token = WC_Payment_Tokens::get( $token_id );
@ -129,7 +137,7 @@ class WC_Tests_Payment_Tokens extends WC_Unit_Test_Case {
* Test deleting a token by ID.
* @since 2.6.0
*/
function test_wc_payment_tokens_delete() {
public function test_wc_payment_tokens_delete() {
$token = WC_Helper_Payment_Token::create_cc_token();
$token_id = $token->get_id();
@ -143,7 +151,7 @@ class WC_Tests_Payment_Tokens extends WC_Unit_Test_Case {
* Test getting a token's type by ID.
* @since 2.6.0
*/
function test_wc_payment_tokens_get_type_by_id() {
public function test_wc_payment_tokens_get_type_by_id() {
$token = WC_Helper_Payment_Token::create_cc_token();
$token_id = $token->get_id();
$this->assertEquals( 'CC', WC_Payment_Tokens::get_token_type_by_id( $token_id ) );
@ -153,7 +161,7 @@ class WC_Tests_Payment_Tokens extends WC_Unit_Test_Case {
* Test setting a users default token.
* @since 2.6.0
*/
function test_wc_payment_tokens_set_users_default() {
public function test_wc_payment_tokens_set_users_default() {
$token = WC_Helper_Payment_Token::create_cc_token( $this->user_id );
$token_id = $token->get_id();
$token->save();
@ -162,7 +170,7 @@ class WC_Tests_Payment_Tokens extends WC_Unit_Test_Case {
$token_id_2 = $token2->get_id();
$token2->save();
$this->assertTrue( $token->is_default() ); // first created is default
$this->assertTrue( $token->is_default() ); // first created is default.
$this->assertFalse( $token2->is_default() );
WC_Payment_Tokens::set_users_default( $this->user_id, $token_id_2 );

View File

@ -216,29 +216,38 @@ class WC_Tests_Product_Data_Store extends WC_Unit_Test_Case {
$product = new WC_Product_Variable( $product->get_id() );
$this->assertEquals( 4, count( $product->get_children() ) );
$this->assertEquals( 6, count( $product->get_children() ) );
$expected_prices['price'][ $children[0] ] = 8.00;
$expected_prices['price'][ $children[1] ] = 15.00;
$expected_prices['price'][ $children[2] ] = 16.00;
$expected_prices['price'][ $children[3] ] = 17.00;
$expected_prices['price'][ $children[4] ] = 18.00;
$expected_prices['price'][ $children[5] ] = 19.00;
$expected_prices['regular_price'][ $children[0] ] = 10.00;
$expected_prices['regular_price'][ $children[1] ] = 15.00;
$expected_prices['regular_price'][ $children[2] ] = 16.00;
$expected_prices['regular_price'][ $children[3] ] = 17.00;
$expected_prices['regular_price'][ $children[4] ] = 18.00;
$expected_prices['regular_price'][ $children[5] ] = 19.00;
$expected_prices['sale_price'][ $children[0] ] = 8.00;
$expected_prices['sale_price'][ $children[1] ] = 15.00;
$expected_prices['sale_price'][ $children[2] ] = 16.00;
$expected_prices['sale_price'][ $children[3] ] = 17.00;
$expected_prices['sale_price'][ $children[4] ] = 18.00;
$expected_prices['sale_price'][ $children[5] ] = 19.00;
$this->assertEquals( $expected_prices, $product->get_variation_prices() );
$expected_attributes = array(
'pa_size' => array( 'small', 'large', 'huge' ),
'pa_colour' => array( 'red' ),
'pa_number' => array( '0', '2' ),
'pa_colour' => array(
0 => 'red',
2 => 'blue',
),
'pa_number' => array( '0', '1', '2' ),
);
$this->assertEquals( $expected_attributes, $product->get_variation_attributes() );
}

View File

@ -5,9 +5,9 @@
* @since 2.3
*/
/**
* WC_Tests_Product_Functions class.
*/
/**
* WC_Tests_Product_Functions class.
*/
class WC_Tests_Product_Functions extends WC_Unit_Test_Case {
/**
@ -70,7 +70,7 @@ class WC_Tests_Product_Functions extends WC_Unit_Test_Case {
'type' => 'variation',
)
);
$this->assertCount( 4, $products );
$this->assertCount( 6, $products );
// Test parent.
$products = wc_get_products(
@ -80,7 +80,7 @@ class WC_Tests_Product_Functions extends WC_Unit_Test_Case {
'parent' => $variation->get_id(),
)
);
$this->assertCount( 4, $products );
$this->assertCount( 6, $products );
// Test parent_exclude.
$products = wc_get_products(

View File

@ -159,7 +159,7 @@ class WC_Tests_Product_Variable extends WC_Unit_Test_Case {
* @param string $expected_stock_status The expected stock status of the product after being saved.
*/
public function test_stock_status_on_save_when_managing_stock( $stock_quantity, $notify_no_stock_amount, $accepts_backorders, $expected_stock_status ) {
list($product, $child1, $child2) = $this->get_variable_product_with_children();
list( $product, $child1, $child2 ) = $this->get_variable_product_with_children();
update_option( 'woocommerce_notify_no_stock_amount', $notify_no_stock_amount );
@ -176,4 +176,199 @@ class WC_Tests_Product_Variable extends WC_Unit_Test_Case {
$this->assertEquals( $expected_stock_status, $product->get_stock_status() );
}
/**
* Setup for a test for is_visible.
*
* @param array $filtering_attributes Simulated filtering attributes as an array of attribute_name => [term1, term2...].
* @param bool $hide_out_of_stock_products Should the woocommerce_hide_out_of_stock_items option be set?.
* @param bool $is_visible_from_parent Return value of is_visible from base class.
*
* @return WC_Product_Variable A properly configured instance of WC_Product_Variable to test.
*/
private function prepare_visibility_test( $filtering_attributes, $hide_out_of_stock_products = true, $is_visible_from_parent = true ) {
foreach ( $filtering_attributes as $attribute_name => $terms ) {
$filtering_attributes[ $attribute_name ]['query_type'] = 'ANY_QUERY_TYPE';
$filtering_attributes[ $attribute_name ]['terms'] = $terms;
}
update_option( 'woocommerce_hide_out_of_stock_items', $hide_out_of_stock_products ? 'yes' : 'no' );
$sut = $this
->getMockBuilder( WC_Product_Variable::class )
->setMethods( array( 'parent_is_visible_core', 'get_layered_nav_chosen_attributes' ) )
->getMock();
$sut = WC_Helper_Product::create_variation_product( $sut, true );
$sut->save();
$sut->method( 'parent_is_visible_core' )->willReturn( $is_visible_from_parent );
$sut->method( 'get_layered_nav_chosen_attributes' )->willReturn( $filtering_attributes );
return $sut;
}
/**
* Configure the stock status for the attribute-based variations of a product.
*
* @param WC_Product_Variable $product Product with the variations to configure.
* @param array $attributes An array of attribute_name => [attribute_values], only the matching variations will have stock.
*/
private function set_variations_with_stock( $product, $attributes ) {
$variation_ids = $product->get_children();
foreach ( $variation_ids as $id ) {
$variation = wc_get_product( $id );
$attribute_matches = true;
foreach ( $attributes as $name => $values ) {
if ( ! in_array( $variation->get_attribute( $name ), $values, true ) ) {
$attribute_matches = false;
}
}
$variation->set_stock_status( $attribute_matches ? 'instock' : 'outofstock' );
$variation->save();
}
}
/**
* @testdox The product should be invisible when the parent 'is_visible' method returns false.
*/
public function test_is_invisible_when_parent_is_visible_returns_false() {
$sut = $this->prepare_visibility_test( array(), '', false, false );
$this->assertFalse( $sut->is_visible() );
}
/**
* @testdox The product should be visible when no nav filtering is supplied if at least one variation has stock.
*
* Note that if no variations have stock the base is_visible will already return false.
*/
public function test_is_visible_when_no_filtering_supplied_and_at_least_one_variation_has_stock() {
$sut = $this->prepare_visibility_test( array(), '' );
$this->set_variations_with_stock( $sut, array( 'pa_size' => array( 'small' ) ) );
$this->assertTrue( $sut->is_visible() );
}
/**
* @testdox Test product visibility when the variation requested in nav filtering has no stock, result depends on woocommerce_hide_out_of_stock_items option.
*
* @param bool $hide_out_of_stock Value for woocommerce_hide_out_of_stock_items.
* @param bool $expected_visibility Expected value of is_visible for the tested product.
*
* @testWith [true, false]
* [false, true]
*/
public function test_visibility_when_supplied_filter_has_no_stock( $hide_out_of_stock, $expected_visibility ) {
$sut = $this->prepare_visibility_test( array( 'pa_size' => array( 'large' ) ), $hide_out_of_stock );
$this->set_variations_with_stock( $sut, array( 'pa_size' => array( 'small' ) ) );
$this->assertEquals( $expected_visibility, $sut->is_visible() );
}
/**
* @testdox Product should always be visible when only one of the variations requested in nav filtering has stock.
*
* @param bool $hide_out_of_stock Value for woocommerce_hide_out_of_stock_items.
*
* @testWith [true]
* [false]
*/
public function test_visibility_when_multiple_filter_values_supplied_and_only_one_has_stock( $hide_out_of_stock ) {
$sut = $this->prepare_visibility_test( array( 'pa_size' => array( 'small', 'large' ) ), $hide_out_of_stock );
$this->set_variations_with_stock( $sut, array( 'pa_size' => array( 'small' ) ) );
$this->assertTrue( $sut->is_visible() );
}
/**
* @testdox Product should be visible when all of the variations requested in nav filtering have stock.
*
* @param bool $hide_out_of_stock Value for woocommerce_hide_out_of_stock_items.
*
* @testWith [true]
* [false]
*/
public function test_visibility_when_multiple_filter_values_supplied_and_all_of_them_have_stock( $hide_out_of_stock ) {
$sut = $this->prepare_visibility_test( array( 'pa_size' => array( 'small', 'large' ) ), $hide_out_of_stock );
$this->set_variations_with_stock( $sut, array( 'pa_size' => array( 'small', 'large' ) ) );
$this->assertTrue( $sut->is_visible() );
}
/**
* @testdox Product should be visible when multiple filters are present, and there's a variation matching all of them.
*/
public function test_visibility_when_multiple_filters_are_used_and_all_of_them_match() {
$sut = $this->prepare_visibility_test(
array(
'pa_size' => array( 'huge' ),
'pa_colour' => array( 'blue' ),
),
true
);
$this->set_variations_with_stock(
$sut,
array(
'pa_size' => array( 'huge' ),
'pa_colour' => array( 'blue' ),
'pa_number' => array( '2' ),
)
);
$this->assertTrue( $sut->is_visible() );
}
/**
* @testdox Product should not be visible when multiple filters are present, and there are no variations matching all of them.
*/
public function test_visibility_when_multiple_filters_are_used_and_one_of_them_does_not_match() {
$sut = $this->prepare_visibility_test(
array(
'pa_size' => array( 'small', 'huge' ),
'pa_colour' => array( 'red' ),
),
true
);
$this->set_variations_with_stock(
$sut,
array(
'pa_size' => array( 'huge' ),
'pa_colour' => array( 'blue' ),
'pa_number' => array( '2' ),
)
);
$this->assertFalse( $sut->is_visible() );
}
/**
* @testdox Attributes having "Any..." as value should not count when searching for matching attributes.
*/
public function test_visibility_when_multiple_filters_are_used_and_an_attribute_has_any_value() {
$sut = $this->prepare_visibility_test(
array(
'pa_size' => array( 'huge' ),
'pa_number' => array( '34' ),
),
true
);
$this->set_variations_with_stock(
$sut,
array(
'pa_size' => array( 'huge' ),
'pa_colour' => array( 'blue' ),
'pa_number' => array( '' ),
)
);
$this->assertTrue( $sut->is_visible() );
}
}

View File

@ -91,4 +91,43 @@ class WC_Tests_Product_Variation extends WC_Unit_Test_Case {
$variable_product = WC_Helper_Product::create_variation_product();
new WC_Product_Variation( $variable_product->get_id() );
}
/**
* @testdox Test that get_variation_attributes returns the appropriate values.
*
* @param bool $with_prefix Parameter for get_variation_attributes.
* @param string $expected_prefix Expected prefix on the returned attribute names.
*
* @testWith [true, "attribute_"]
* [false, ""]
*/
public function test_get_variation_attributes( $with_prefix, $expected_prefix ) {
$product = WC_Helper_Product::create_variation_product();
$sut = wc_get_product( $product->get_children()[2] );
$expected = array(
$expected_prefix . 'pa_size' => 'huge',
$expected_prefix . 'pa_colour' => 'red',
$expected_prefix . 'pa_number' => '0',
);
$actual = $sut->get_variation_attributes( $with_prefix );
$this->assertEquals( $expected, $actual );
}
/**
* @testdox Test that the delete method removes the attribute terms for the variation.
*/
public function test_delete_removes_attribute_terms() {
$product = WC_Helper_Product::create_variation_product();
$sut = wc_get_product( $product->get_children()[2] );
$id = $sut->get_id();
$sut->delete( true );
$attribute_names = wc_get_attribute_taxonomy_names();
$variation_attribute_terms = wp_get_post_terms( $id, $attribute_names );
$this->assertEmpty( $variation_attribute_terms );
}
}

View File

@ -19,8 +19,7 @@ class WC_Tests_Setup_Functions extends WC_Unit_Test_Case {
$setup_wizard = new WC_Admin_Setup_Wizard();
// non-admin user.
$this->user_id = $this->factory->user->create( array( 'role' => 'shop_manager' ) );
wp_set_current_user( $this->user_id );
$this->user_id = $this->login_as_role( 'shop_manager' );
$this->assertEquals(
array(
'paypal' => false,
@ -29,8 +28,7 @@ class WC_Tests_Setup_Functions extends WC_Unit_Test_Case {
);
// set admin user.
$this->user_id = $this->factory->user->create( array( 'role' => 'administrator' ) );
wp_set_current_user( $this->user_id );
$this->user_id = $this->login_as_administrator();
update_option( 'woocommerce_default_country', 'US' );
$this->assertEquals(

View File

@ -28,6 +28,7 @@ class WC_Tests_WC_Query extends WC_Unit_Test_Case {
$this->assertTrue( wc_has_notice( 'test', 'error' ) );
// Clean up.
// phpcs:disable WordPress.Security.NonceVerification.Recommended
unset( $_GET['wc_error'] );
wc_clear_notices();
@ -182,6 +183,7 @@ class WC_Tests_WC_Query extends WC_Unit_Test_Case {
* @group core-only
*/
public function test_get_catalog_ordering_args() {
// phpcs:disable WordPress.DB.SlowDBQuery
$data = array(
array(
'orderby' => 'menu_order',
@ -297,6 +299,7 @@ class WC_Tests_WC_Query extends WC_Unit_Test_Case {
),
),
);
// phpcs:enable WordPress.DB.SlowDBQuery
foreach ( $data as $test ) {
$result = WC()->query->get_catalog_ordering_args( $test['orderby'], $test['order'] );
@ -310,11 +313,13 @@ class WC_Tests_WC_Query extends WC_Unit_Test_Case {
public function test_get_catalog_ordering_args_GET() {
$_GET['orderby'] = 'price-desc';
// phpcs:disable WordPress.DB.SlowDBQuery
$expected = array(
'orderby' => 'price',
'order' => 'DESC',
'meta_key' => '',
);
// phpcs:enable WordPress.DB.SlowDBQuery
$this->assertEquals( $expected, WC()->query->get_catalog_ordering_args() );
@ -341,9 +346,11 @@ class WC_Tests_WC_Query extends WC_Unit_Test_Case {
'include_children' => true,
);
// phpcs:disable WordPress.DB.SlowDBQuery
$query_args = array(
'tax_query' => array( $tax_query ),
);
// phpcs:enable WordPress.DB.SlowDBQuery
WC()->query->product_query( new WP_Query( $query_args ) );
$tax_queries = WC_Query::get_main_tax_query();
@ -360,9 +367,11 @@ class WC_Tests_WC_Query extends WC_Unit_Test_Case {
'compare' => '=',
);
// phpcs:disable WordPress.DB.SlowDBQuery
$query_args = array(
'meta_query' => array( $meta_query ),
);
// phpcs:enable WordPress.DB.SlowDBQuery
WC()->query->product_query( new WP_Query( $query_args ) );
$meta_queries = WC_Query::get_main_meta_query();
@ -428,4 +437,84 @@ class WC_Tests_WC_Query extends WC_Unit_Test_Case {
WC()->query->remove_ordering_args();
}
/**
* Setup for a test for adjust_posts.
*
* @param bool $with_nav_filtering_data Should WC_Query::get_layered_nav_chosen_attributes return filtering data?.
* @param bool $use_objects If true, get_current_posts will return objects with an ID property; if false, it will returns the ids.
*
* @return array An array where the first element is the instance of WC_Query, and the second is an array of sample products created.
*/
private function setup_adjust_posts_test( $with_nav_filtering_data, $use_objects ) {
update_option( 'woocommerce_hide_out_of_stock_items', 'yes' );
if ( $with_nav_filtering_data ) {
$nav_filtering_data = array( 'pa_something' => array( 'terms' => array( 'foo', 'bar' ) ) );
} else {
$nav_filtering_data = array();
}
$products = array();
$posts = array();
for ( $i = 0; $i < 5; $i++ ) {
$product = WC_Helper_Product::create_simple_product();
array_push( $products, $product );
$post = $use_objects ? (object) array( 'ID' => $product->get_id() ) : $product->get_id();
array_push( $posts, $post );
}
$products[0]->set_stock_status( 'outofstock' );
$sut = $this
->getMockBuilder( WC_Query::class )
->setMethods( array( 'get_current_posts', 'get_layered_nav_chosen_attributes_inst' ) )
->getMock();
$sut->method( 'get_current_posts' )->willReturn( $posts );
$sut->method( 'get_layered_nav_chosen_attributes_inst' )->willReturn( $nav_filtering_data );
return array( $sut, $products );
}
/**
* @param bool $with_nav_filtering_data Should WC_Query::get_layered_nav_chosen_attributes return filtering data?.
* @param bool $use_objects If true, get_current_posts will return objects with an ID property; if false, it will returns the ids.
*
* @testdox adjust_posts should return the number of visible products and create product visibility loop variables
* @testWith [true, true]
* [false, false]
* [true, false]
* [false, true]
*/
public function test_adjust_posts_count_with_nav_filtering_attributes( $with_nav_filtering_data, $use_objects ) {
list($sut, $products) = $this->setup_adjust_posts_test( $with_nav_filtering_data, $use_objects );
$products[0]->set_stock_status( 'outofstock' );
$products[0]->save();
$products[1]->set_stock_status( 'outofstock' );
$products[1]->save();
$this->assertEquals( 3, $sut->adjust_posts_count( 34 ) );
$this->assertEquals( 3, wc_get_loop_prop( 'total' ) );
$this->assertEquals( false, wc_get_loop_product_visibility( $products[0]->get_id() ) );
$this->assertEquals( false, wc_get_loop_product_visibility( $products[1]->get_id() ) );
foreach ( array_slice( $products, 2 ) as $product ) {
$this->assertEquals( true, wc_get_loop_product_visibility( $product->get_id() ) );
}
}
/**
* @testdox adjust_posts should return the input unmodified if get_current_posts returns null.
*/
public function test_adjust_posts_count_when_there_are_no_posts() {
$sut = $this
->getMockBuilder( WC_Query::class )
->setMethods( array( 'get_current_posts', 'get_layered_nav_chosen_attributes_inst' ) )
->getMock();
$sut->method( 'get_current_posts' )->willReturn( null );
$this->assertEquals( 34, $sut->adjust_posts_count( 34 ) );
}
}

View File

@ -37,8 +37,6 @@ class WC_Tests_Install extends WC_Unit_Test_Case {
/**
* Test - install.
*/
/**
public function test_install() {
// clean existing install first.
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {

View File

@ -221,6 +221,9 @@ class WC_Tests_Webhook_Functions extends WC_Unit_Test_Case {
* will only deliver the payload once per webhook.
*/
public function test_woocommerce_webhook_is_delivered_only_once() {
global $wc_queued_webhooks;
$this->assertNull( $wc_queued_webhooks );
$webhook1 = wc_get_webhook( $this->create_webhook( 'customer.created' )->get_id() );
$webhook2 = wc_get_webhook( $this->create_webhook( 'customer.created' )->get_id() );
wc_load_webhooks( 'active' );
@ -231,6 +234,18 @@ class WC_Tests_Webhook_Functions extends WC_Unit_Test_Case {
$this->assertEquals( 1, $this->delivery_counter[ $webhook2->get_id() . $customer1->get_id() ] );
$this->assertEquals( 1, $this->delivery_counter[ $webhook1->get_id() . $customer2->get_id() ] );
$this->assertEquals( 1, $this->delivery_counter[ $webhook2->get_id() . $customer2->get_id() ] );
$this->assertCount( 4, $wc_queued_webhooks );
$this->assertEquals( $webhook2->get_id(), $wc_queued_webhooks[0]['webhook']->get_id() );
$this->assertEquals( $customer1->get_id(), $wc_queued_webhooks[0]['arg'] );
$this->assertEquals( $webhook1->get_id(), $wc_queued_webhooks[1]['webhook']->get_id() );
$this->assertEquals( $customer1->get_id(), $wc_queued_webhooks[1]['arg'] );
$this->assertEquals( $webhook2->get_id(), $wc_queued_webhooks[2]['webhook']->get_id() );
$this->assertEquals( $customer2->get_id(), $wc_queued_webhooks[2]['arg'] );
$this->assertEquals( $webhook1->get_id(), $wc_queued_webhooks[3]['webhook']->get_id() );
$this->assertEquals( $customer2->get_id(), $wc_queued_webhooks[3]['arg'] );
$wc_queued_webhooks = null;
$webhook1->delete( true );
$webhook2->delete( true );
$customer1->delete( true );
@ -238,29 +253,6 @@ class WC_Tests_Webhook_Functions extends WC_Unit_Test_Case {
remove_action( 'woocommerce_webhook_process_delivery', array( $this, 'woocommerce_webhook_process_delivery' ), 1, 2 );
}
/**
* Verify that a webhook is queued when intended to be delivered synchronously. This allows us to then execute them
* all in a `register_shutdown_function` after the request has processed. Since async jobs are handled in
* this way, we can be more confident that it is consistent.
*/
public function test_woocommerce_webhook_synchronous_is_queued() {
add_filter( 'woocommerce_webhook_deliver_async', '__return_false' );
$webhook = wc_get_webhook( $this->create_webhook( 'customer.created' )->get_id() );
wc_load_webhooks( 'active' );
add_action( 'woocommerce_webhook_process_delivery', array( $this, 'woocommerce_webhook_process_delivery' ), 1, 2 );
$customer = WC_Helper_Customer::create_customer( 'test1', 'pw1', 'user1@example.com' );
global $wc_queued_sync_webhooks;
$this->assertCount( 1, $wc_queued_sync_webhooks );
$this->assertEquals( $webhook->get_id(), $wc_queued_sync_webhooks[0]['webhook']->get_id() );
$this->assertEquals( $customer->get_id(), $wc_queued_sync_webhooks[0]['arg'] );
$wc_queued_sync_webhooks = null;
remove_filter( 'woocommerce_webhook_deliver_async', '__return_false' );
$webhook->delete( true );
$customer->delete( true );
}
/**
* Helper function to keep track of which webhook (and corresponding arg) has been delivered
* within the current request.

View File

@ -0,0 +1,88 @@
<?php
/**
* Class WC_AJAX_Test file.
*
* @package WooCommerce|Tests|WC_AJAX.
*/
/**
* Class WC_AJAX_Test file.
*/
class WC_AJAX_Test extends \WC_Unit_Test_Case {
/**
* Stock should not be reduced from AJAX when an item is added to an order.
*/
public function test_add_item_to_pending_payment_order() {
$product = WC_Helper_Product::create_simple_product();
$product->set_manage_stock( true );
$product->set_stock_quantity( 1000 );
$product->save();
$order = WC_Helper_Order::create_order();
$data = array(
array(
'id' => $product->get_id(),
'qty' => 10,
),
);
// Call private method `maybe_add_order_item`.
$maybe_add_order_item_func = function () use ( $order, $data ) {
return static::maybe_add_order_item( $order->get_id(), '', $data );
};
$maybe_add_order_item_func->call( new WC_AJAX() );
// Refresh from DB.
$product = wc_get_product( $product->get_id() );
// Stock should not have been reduced because order status is 'pending'.
$this->assertEquals( 1000, $product->get_stock_quantity() );
$line_items = $order->get_items();
foreach ( $line_items as $line_item ) {
if ( $line_item->get_product_id() === $product->get_id() ) {
$this->assertEquals( false, $line_item->get_meta( '_reduced_stock', true ) );
}
}
}
/**
* Stock should be reduced from AJAX when an item is added to an order, when status is being changed
*/
public function test_add_item_to_processing_order() {
$product = WC_Helper_Product::create_simple_product();
$product->set_manage_stock( true );
$product->set_stock_quantity( 1000 );
$product->save();
$order = WC_Helper_Order::create_order();
$order->set_status( 'pending' );
$order->save();
$data = array(
array(
'id' => $product->get_id(),
'qty' => 10,
),
);
// Call private method `maybe_add_order_item`.
$maybe_add_order_item_func = function () use ( $order, $data ) {
return static::maybe_add_order_item( $order->get_id(), '', $data );
};
$maybe_add_order_item_func->call( new WC_AJAX() );
$order->set_status( 'processing' );
$order->save();
// Refresh from DB.
$product = wc_get_product( $product->get_id() );
$this->assertEquals( 990, $product->get_stock_quantity() );
$line_items = $order->get_items();
foreach ( $line_items as $line_item ) {
if ( $line_item->get_product_id() === $product->get_id() ) {
$this->assertEquals( 10, $line_item->get_meta( '_reduced_stock', true ) );
}
}
}
}

View File

@ -0,0 +1,213 @@
<?php
/**
* AbstractServiceProviderTests class file.
*
* @package Automattic\WooCommerce\Tests\Internal\DependencyManagement
*/
namespace Automattic\WooCommerce\Tests\Internal\DependencyManagement;
use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;
use Automattic\WooCommerce\Internal\DependencyManagement\ContainerException;
use Automattic\WooCommerce\Internal\DependencyManagement\ExtendedContainer;
use Automattic\WooCommerce\Tests\Internal\DependencyManagement\ExampleClasses\ClassWithConstructorArgumentWithoutTypeHint;
use Automattic\WooCommerce\Tests\Internal\DependencyManagement\ExampleClasses\ClassWithDependencies;
use Automattic\WooCommerce\Tests\Internal\DependencyManagement\ExampleClasses\ClassWithPrivateConstructor;
use Automattic\WooCommerce\Tests\Internal\DependencyManagement\ExampleClasses\ClassWithScalarConstructorArgument;
use Automattic\WooCommerce\Tests\Internal\DependencyManagement\ExampleClasses\DependencyClass;
use League\Container\Definition\DefinitionInterface;
/**
* Tests for AbstractServiceProvider.
*/
class AbstractServiceProviderTest extends \WC_Unit_Test_Case {
/**
* The system under test.
*
* @var AbstractServiceProvider
*/
private $sut;
/**
* The container used for tests.
*
* @var ExtendedContainer
*/
private $container;
/**
* Runs before each test.
*/
public function setUp() {
$this->container = new ExtendedContainer();
$this->sut = new class() extends AbstractServiceProvider {
// phpcs:disable
/**
* Public version of add_with_auto_arguments, which is usually protected.
*/
public function add_with_auto_arguments( string $class_name, $concrete = null, bool $shared = false ) : DefinitionInterface {
return parent::add_with_auto_arguments( $class_name, $concrete, $shared );
}
/**
* The mandatory 'register' method (defined in the base class as abstract).
* Not implemented because this class is tested on its own, not as a service provider actually registered on a container.
*/
public function register() {}
// phpcs:enable
};
$this->sut->setContainer( $this->container );
}
/**
* Runs before all the tests of the class.
*/
public static function setUpBeforeClass() {
/**
* Return a new instance of ClassWithDependencies.
*
* @param DependencyClass $dependency The dependency to inject.
* @return ClassWithDependencies The new instance.
*/
function get_new_dependency_class( DependencyClass $dependency ) {
return new ClassWithDependencies( $dependency );
};
}
/**
* @testdox 'add_with_auto_arguments' should throw an exception if an invalid class name is passed as class name.
*/
public function test_add_with_auto_arguments_throws_on_non_class_passed_as_class_name() {
$this->expectException( ContainerException::class );
$this->expectExceptionMessage( "AbstractServiceProvider::add_with_auto_arguments: error when reflecting class 'foobar': Class foobar does not exist" );
$this->sut->add_with_auto_arguments( 'foobar' );
}
/**
* @testdox 'add_with_auto_arguments' should throw an exception if the passed class has a private constructor.
*/
public function test_add_with_auto_arguments_throws_on_class_private_constructor() {
$this->expectException( ContainerException::class );
$this->expectExceptionMessage( "AbstractServiceProvider::add_with_auto_arguments: constructor of class '" . ClassWithPrivateConstructor::class . "' isn't public, instances can't be created." );
$this->sut->add_with_auto_arguments( ClassWithPrivateConstructor::class );
}
/**
* @testdox 'add_with_auto_arguments' should throw an exception if the passed concrete is a class with a private constructor.
*/
public function test_add_with_auto_arguments_throws_on_concrete_private_constructor() {
$this->expectException( ContainerException::class );
$this->expectExceptionMessage( "AbstractServiceProvider::add_with_auto_arguments: constructor of class '" . ClassWithPrivateConstructor::class . "' isn't public, instances can't be created." );
$this->sut->add_with_auto_arguments( ClassWithDependencies::class, ClassWithPrivateConstructor::class );
}
/**
* @testdox 'add_with_auto_arguments' should throw an exception if the passed class has a constructor argument without type hint.
*/
public function test_add_with_auto_arguments_throws_on_constructor_argument_without_type_hint() {
$this->expectException( ContainerException::class );
$this->expectExceptionMessage( "AbstractServiceProvider::add_with_auto_arguments: constructor argument 'argument_without_type_hint' of class '" . ClassWithConstructorArgumentWithoutTypeHint::class . "' doesn't have a type hint or has one that doesn't specify a class." );
$this->sut->add_with_auto_arguments( ClassWithConstructorArgumentWithoutTypeHint::class );
}
/**
* @testdox 'add_with_auto_arguments' should throw an exception if the passed class has a constructor argument with a scalar type hint.
*/
public function test_add_with_auto_arguments_throws_on_constructor_argument_with_scalar_type_hint() {
$this->expectException( ContainerException::class );
$this->expectExceptionMessage( "AbstractServiceProvider::add_with_auto_arguments: constructor argument 'scalar_argument_without_default_value' of class '" . ClassWithScalarConstructorArgument::class . "' doesn't have a type hint or has one that doesn't specify a class." );
$this->sut->add_with_auto_arguments( ClassWithScalarConstructorArgument::class );
}
/**
* @testdox 'add_with_auto_arguments' should properly register the supplied class when no concrete is passed.
*
* @testWith [true, 1]
* [false, 2]
*
* @param bool $shared Whether to register the test class as shared or not.
* @param int $expected_constructions_count Expected number of times that the test class will have been instantiated.
*/
public function test_add_with_auto_arguments_works_as_expected_with_no_concrete( bool $shared, int $expected_constructions_count ) {
ClassWithDependencies::$instances_count = 0;
$this->container->share( DependencyClass::class );
$this->sut->add_with_auto_arguments( ClassWithDependencies::class, null, $shared );
$this->container->get( ClassWithDependencies::class );
$resolved = $this->container->get( ClassWithDependencies::class );
// A new instance is created for each resolution or not, depending on $shared.
$this->assertEquals( $expected_constructions_count, ClassWithDependencies::$instances_count );
// Arguments with default values are honored.
$this->assertEquals( ClassWithDependencies::SOME_NUMBER, $resolved->some_number );
// Constructor arguments are filled as expected.
$this->assertSame( $this->container->get( DependencyClass::class ), $resolved->dependency_class );
}
/**
* @testdox 'add_with_auto_arguments' should properly register the supplied class when a concrete representing a class name is passed.
*/
public function test_add_with_auto_arguments_works_as_expected_when_concrete_is_class_name() {
$this->sut->add_with_auto_arguments( ClassWithDependencies::class, DependencyClass::class );
$resolved = $this->container->get( ClassWithDependencies::class );
$this->assertInstanceOf( DependencyClass::class, $resolved );
}
/**
* @testdox 'add_with_auto_arguments' should properly register the supplied class when a concrete that is an object is passed.
*/
public function test_add_with_auto_arguments_works_as_expected_when_concrete_is_object() {
$object = new DependencyClass();
$this->sut->add_with_auto_arguments( ClassWithDependencies::class, $object );
$resolved = $this->container->get( ClassWithDependencies::class );
$this->assertSame( $object, $resolved );
}
/**
* @testdox 'add_with_auto_arguments' should properly register the supplied class when a concrete that is a closure is passed.
*/
public function test_add_with_auto_arguments_works_as_expected_when_concrete_is_a_closure() {
$this->container->share( DependencyClass::class );
$callable = function( DependencyClass $dependency ) {
return new ClassWithDependencies( $dependency );
};
$this->sut->add_with_auto_arguments( ClassWithDependencies::class, $callable );
$resolved = $this->container->get( ClassWithDependencies::class );
$this->assertInstanceOf( ClassWithDependencies::class, $resolved );
}
/**
* @testdox 'add_with_auto_arguments' should properly register the supplied class when a concrete that is a function name is passed.
*/
public function test_add_with_auto_arguments_works_as_expected_when_concrete_is_a_function_name() {
$this->container->share( DependencyClass::class );
$this->sut->add_with_auto_arguments( ClassWithDependencies::class, __NAMESPACE__ . '\get_new_dependency_class' );
$resolved = $this->container->get( ClassWithDependencies::class );
$this->assertInstanceOf( ClassWithDependencies::class, $resolved );
}
}

View File

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

View File

@ -0,0 +1,52 @@
<?php
/**
* ClassWithDependencies class file.
*
* @package Automattic\WooCommerce\Tests\Internal\DependencyManagement\ExampleClasses
*/
namespace Automattic\WooCommerce\Tests\Internal\DependencyManagement\ExampleClasses;
/**
* An example of a class with dependencies that are supplied via constructor arguments.
*/
class ClassWithDependencies {
/**
* Default value for $some_number argument.
*/
const SOME_NUMBER = 34;
/**
* Count of instances of the class created so far.
*
* @var int
*/
public static $instances_count = 0;
/**
* Value supplied to constructor in $some_number argument.
*
* @var int
*/
public $some_number = 0;
/**
* Value supplied to constructor in $dependency_class argument.
*
* @var DependencyClass
*/
public $dependency_class = null;
/**
* Class constructor.
*
* @param DependencyClass $dependency_class A class we depend on.
* @param int $some_number Some number we need for some reason.
*/
public function __construct( DependencyClass $dependency_class, int $some_number = self::SOME_NUMBER ) {
self::$instances_count++;
$this->dependency_class = $dependency_class;
$this->some_number = $some_number;
}
}

View File

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

View File

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

View File

@ -0,0 +1,39 @@
<?php
/**
* ClassWithSingleton class file.
*
* @package Automattic\WooCommerce\Tests\Internal\DependencyManagement\ExampleClasses
*/
// This class is in the root namespace on purpose, since it simulates being a legacy class in the 'includes' directory.
/**
* An example of a class that holds a singleton instance.
*/
class ClassWithSingleton {
/**
* @var ClassWithSingleton The singleton instance of the class.
*/
public static $instance;
/**
* @var array The arguments supplied to 'instance'.
*/
public static $instance_args;
/**
* Gets the singleton instance of the class.
*
* @param mixed ...$args Any arguments required by the method.
*
* @return ClassWithSingleton The singleton instance of the class.
*/
public static function instance( ...$args ) {
if ( is_null( self::$instance ) ) {
self::$instance = new ClassWithSingleton();
self::$instance_args = $args;
}
return self::$instance;
}
}

View File

@ -0,0 +1,25 @@
<?php
/**
* DependencyClass class file.
*
* @package Automattic\WooCommerce\Tests\Internal\DependencyManagement\ExampleClasses
*/
namespace Automattic\WooCommerce\Tests\Internal\DependencyManagement\ExampleClasses;
/**
* An example of a class other classes depend on.
*/
class DependencyClass {
/**
* Concatenates the supplied string parts just for fun.
*
* @param mixed ...$parts The parts.
*
* @return string The resulting concatenated string.
*/
public static function concat( ...$parts ) {
return 'Parts: ' . join( ', ', $parts );
}
}

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