From 086aa829c99d190c3b190a7e93add2774f29ef6b Mon Sep 17 00:00:00 2001 From: Steve Grunwell Date: Thu, 4 Jan 2018 18:13:45 +0000 Subject: [PATCH 01/52] Install WooCommerce as a dev dependency via Composer, and ensure its test framework is available. Since WooCommerce requires WordPress to be loaded in a certain way, we're better off extending WC_Unit_Test_Case instead of WP_UnitTestCase, since that's the base test case for WooCommerce core. This commit also includes composer.lock in the repo, as it's designed to be included in version control. --- .gitignore | 4 +- composer.json | 19 +++- composer.lock | 214 +++++++++++++++++++++++++++++++++++++++++++++ tests/testcase.php | 2 +- 4 files changed, 235 insertions(+), 4 deletions(-) create mode 100644 composer.lock diff --git a/.gitignore b/.gitignore index dfd6caa..22fa3f5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ -/vendor -composer.lock \ No newline at end of file +tests/coverage +vendor diff --git a/composer.json b/composer.json index 4ba4bc5..fe66f2f 100644 --- a/composer.json +++ b/composer.json @@ -7,8 +7,18 @@ "composer/installers": "^1.4", "xrstf/composer-php52": "^1.0" }, + "require-dev": { + "woocommerce/woocommerce": "dev-master" + }, "autoload": { - "classmap": ["includes"] + "classmap": [ + "includes" + ] + }, + "autoload-dev": { + "classmap": [ + "vendor/woocommerce/woocommerce/tests/framework" + ] }, "scripts": { "post-install-cmd": [ @@ -20,5 +30,12 @@ "post-autoload-dump": [ "xrstf\\Composer52\\Generator::onPostInstallCmd" ] + }, + "extra": { + "installer-paths": { + "vendor/{$vendor}/{$name}": [ + "woocommerce/woocommerce" + ] + } } } diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..95c9636 --- /dev/null +++ b/composer.lock @@ -0,0 +1,214 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "This file is @generated automatically" + ], + "content-hash": "173c6a9e5a12528c9f0f8762e81e63a9", + "packages": [ + { + "name": "composer/installers", + "version": "v1.5.0", + "source": { + "type": "git", + "url": "https://github.com/composer/installers.git", + "reference": "049797d727261bf27f2690430d935067710049c2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/installers/zipball/049797d727261bf27f2690430d935067710049c2", + "reference": "049797d727261bf27f2690430d935067710049c2", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0" + }, + "replace": { + "roundcube/plugin-installer": "*", + "shama/baton": "*" + }, + "require-dev": { + "composer/composer": "1.0.*@dev", + "phpunit/phpunit": "^4.8.36" + }, + "type": "composer-plugin", + "extra": { + "class": "Composer\\Installers\\Plugin", + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Installers\\": "src/Composer/Installers" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kyle Robinson Young", + "email": "kyle@dontkry.com", + "homepage": "https://github.com/shama" + } + ], + "description": "A multi-framework Composer library installer", + "homepage": "https://composer.github.io/installers/", + "keywords": [ + "Craft", + "Dolibarr", + "Eliasis", + "Hurad", + "ImageCMS", + "Kanboard", + "Lan Management System", + "MODX Evo", + "Mautic", + "Maya", + "OXID", + "Plentymarkets", + "Porto", + "RadPHP", + "SMF", + "Thelia", + "WolfCMS", + "agl", + "aimeos", + "annotatecms", + "attogram", + "bitrix", + "cakephp", + "chef", + "cockpit", + "codeigniter", + "concrete5", + "croogo", + "dokuwiki", + "drupal", + "eZ Platform", + "elgg", + "expressionengine", + "fuelphp", + "grav", + "installer", + "itop", + "joomla", + "kohana", + "laravel", + "lavalite", + "lithium", + "magento", + "majima", + "mako", + "mediawiki", + "modulework", + "modx", + "moodle", + "osclass", + "phpbb", + "piwik", + "ppi", + "puppet", + "pxcms", + "reindex", + "roundcube", + "shopware", + "silverstripe", + "sydes", + "symfony", + "typo3", + "wordpress", + "yawik", + "zend", + "zikula" + ], + "time": "2017-12-29T09:13:20+00:00" + }, + { + "name": "xrstf/composer-php52", + "version": "v1.0.20", + "source": { + "type": "git", + "url": "https://github.com/composer-php52/composer-php52.git", + "reference": "bd41459d5e27df8d33057842b32377c39e97a5a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer-php52/composer-php52/zipball/bd41459d5e27df8d33057842b32377c39e97a5a8", + "reference": "bd41459d5e27df8d33057842b32377c39e97a5a8", + "shasum": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-default": "1.x-dev" + } + }, + "autoload": { + "psr-0": { + "xrstf\\Composer52": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "time": "2016-04-16T21:52:24+00:00" + } + ], + "packages-dev": [ + { + "name": "woocommerce/woocommerce", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/woocommerce/woocommerce.git", + "reference": "ae9a98b0a01ba7767907aa2c3e98f006a10cfaad" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/woocommerce/woocommerce/zipball/ae9a98b0a01ba7767907aa2c3e98f006a10cfaad", + "reference": "ae9a98b0a01ba7767907aa2c3e98f006a10cfaad", + "shasum": "" + }, + "require": { + "composer/installers": "~1.2" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.4.3", + "phpunit/phpunit": "6.2.3", + "squizlabs/php_codesniffer": "*", + "wimg/php-compatibility": "^8.0", + "woocommerce/woocommerce-git-hooks": "*", + "woocommerce/woocommerce-sniffs": "*", + "wp-coding-standards/wpcs": "^0.14" + }, + "type": "wordpress-plugin", + "extra": { + "scripts-description": { + "test": "Run unit tests", + "phpcs": "Analyze code against the WordPress coding standards with PHP_CodeSniffer", + "phpcbf": "Fix coding standards warnings/errors automatically with PHP Code Beautifier" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0+" + ], + "description": "An eCommerce toolkit that helps you sell anything. Beautifully.", + "homepage": "https://woocommerce.com/", + "time": "2018-01-04T16:17:30+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": { + "woocommerce/woocommerce": 20 + }, + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} diff --git a/tests/testcase.php b/tests/testcase.php index 8a9466b..c606be9 100644 --- a/tests/testcase.php +++ b/tests/testcase.php @@ -6,7 +6,7 @@ * @author Liquid Web */ -class TestCase extends WP_UnitTestCase { +class TestCase extends WC_Unit_Test_Case { /** * Determine if the custom orders table exists. From 21ad54303ed63f929938e9c5aa20ee782df39346 Mon Sep 17 00:00:00 2001 From: Steve Grunwell Date: Thu, 4 Jan 2018 23:09:11 +0000 Subject: [PATCH 02/52] Simplify the test bootstrapping tremendously by using WooCommerce's test framework. Fundamentally, this plugin should seamlessly integrate with WooCommerce; by loading the WooCommerce test framework, we can explicitly run the platform's entire test suite against an active instance of our plugin. --- phpunit.xml.dist | 9 +++- tests/bootstrap.php | 89 ++++++++-------------------------------- tests/test-bootstrap.php | 18 +------- 3 files changed, 27 insertions(+), 89 deletions(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index a3da0e2..4addb41 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -6,11 +6,18 @@ convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" + defaultTestSuite="plugin" > - + + ./tests/ tests/test-sample.php + + + + ./vendor/woocommerce/woocommerce/tests/unit-tests + diff --git a/tests/bootstrap.php b/tests/bootstrap.php index f845485..de68c0e 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,90 +1,35 @@ get_error_code() ) { - include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; - - echo PHP_EOL . "WooCommerce is not currently installed in the test environment, attempting to install..."; - - // Retrieve information about WooCommerce. - $plugin_data = wp_remote_get( 'https://api.wordpress.org/plugins/info/1.0/woocommerce.json' ); - - if ( ! is_wp_error( $plugin_data ) ) { - $plugin_data = json_decode( wp_remote_retrieve_body( $plugin_data ) ); - $plugin_url = $plugin_data->download_link; - } else { - $plugin_url = false; - } - - // Download the plugin from the WordPress.org repository. - $upgrader = new Plugin_Upgrader( new Automatic_Upgrader_Skin() ); - $installed = $upgrader->install( $plugin_url ); - - if ( true === $installed ) { - echo "\033[0;32mOK\033[0;m" . PHP_EOL . PHP_EOL; - } else { - echo "\033[0;31mFAIL\033[0;m" . PHP_EOL; - - if ( is_wp_error( $installed ) ) { - printf( 'Unable to install WooCommerce: %s.', $installed->get_error_message() ); - } - - printf( - 'Please download and install WooCommerce into %s' . PHP_EOL, - trailingslashit(dirname( dirname( $_tests_dir ) ) ) - ); - - exit( 1 ); - } - - // Try once again to activate. - $activated = activate_plugin( 'woocommerce/woocommerce.php' ); -} - -// Nothing more we can do, unfortunately. -if ( is_wp_error( $activated ) ) { - echo PHP_EOL . 'WooCommerce could not automatically be activated in the test environment:'; - echo PHP_EOL . $activated->get_error_message(); - echo PHP_EOL . PHP_EOL . "\033[0;31mUnable to proceed with tests, aborting.\033[0m"; - echo PHP_EOL; - exit( 1 ); -} +// Finally, Start up the WP testing environment. +require_once $_bootstrap; +require_once __DIR__ . '/testcase.php'; diff --git a/tests/test-bootstrap.php b/tests/test-bootstrap.php index 700ba75..f7ec170 100644 --- a/tests/test-bootstrap.php +++ b/tests/test-bootstrap.php @@ -8,25 +8,11 @@ class BootstrapTest extends TestCase { - /** - * Tear down the plugin after each test run. - * - * @before - */ - public function tear_down_plugin() { - global $wc_custom_order_table; - - // Destroy the global $wc_custom_order_table instance. - unset( $wc_custom_order_table ); - } - public function test_plugin_only_loads_after_woocommerce() { global $wc_custom_order_table; - $this->assertNull( - $wc_custom_order_table, - 'Before bootstrapping, $wc_custom_order_table should be empty.' - ); + // Test bootstrapping may have initialized it for us. + $wc_custom_order_table = null; do_action( 'woocommerce_init' ); From 0dd2906b99ca7c1f7e1f5e3dc5e6f6140773f7c9 Mon Sep 17 00:00:00 2001 From: Steve Grunwell Date: Fri, 5 Jan 2018 18:27:29 +0000 Subject: [PATCH 03/52] Add a helper method to retrieve the unmodified contents of a woocommerce_orders table row, based on the order ID --- tests/testcase.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/testcase.php b/tests/testcase.php index 000871e..67e1679 100644 --- a/tests/testcase.php +++ b/tests/testcase.php @@ -31,6 +31,24 @@ protected static function factory() { return $factory; } + /** + * Retrieve a single row from the Orders table. + * + * @global $wpdb + * + * @param int $order_id The order ID to retrieve. + * + * @return array|null The contents of the database row or null if the given row doesn't exist. + */ + protected function get_order_row( $order_id ) { + global $wpdb; + + return $wpdb->get_row( $wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}woocommerce_orders WHERE order_id = %d", + $order_id + ), ARRAY_A ); + } + /** * Determine if the custom orders table exists. * From 8a316a6a412dfe2b3fd90e5eb0ce94cd2c51ca82 Mon Sep 17 00:00:00 2001 From: Steve Grunwell Date: Fri, 5 Jan 2018 18:28:17 +0000 Subject: [PATCH 04/52] For consistency, use wc_custom_order_table()->get_table_name() to retrieve the custom table name --- includes/class-wc-order-data-store-custom-table.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-wc-order-data-store-custom-table.php b/includes/class-wc-order-data-store-custom-table.php index e047b41..9210c16 100644 --- a/includes/class-wc-order-data-store-custom-table.php +++ b/includes/class-wc-order-data-store-custom-table.php @@ -228,7 +228,7 @@ protected function update_post_meta( &$order ) { if ( $this->creating ) { $wpdb->insert( - "{$wpdb->prefix}woocommerce_orders", + wc_custom_order_table()->get_table_name(), array_merge( array( 'order_id' => $order->get_id(), ), $edit_data ) From f62ce2418dbb255a42eee8e59d3e4d6b0432509f Mon Sep 17 00:00:00 2001 From: Steve Grunwell Date: Fri, 5 Jan 2018 18:29:22 +0000 Subject: [PATCH 05/52] When retrieving the date columns from a WC_Order object, explicitly cast them as strings. The WC_DateTime class explicitly defines a __toString() magic method, but according to the PHP documentation: > Since PHP 5.2.0, it is called in any string context (e.g. in printf() with %s modifier) but not in other types contexts (e.g. with %d modifier). Since WordPress' $wpdb->prepare() method isn't a true sprintf() replacement (as we saw with WordPress 4.8.*), the __toString() magic method isn't necessarily called, meaning we're attempting to shove a WC_DateTime instance into a "%s" placeholder and trying to stuff that into a DATETIME column in the table. Without being cast to strings, the WooCommerce core test suite gets filled with warnings like this: Unexpected incorrect usage notice for wpdb::prepare Failed asserting that an array is empty. --- ...class-wc-order-data-store-custom-table.php | 4 +- tests/test-data-store.php | 56 +++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/includes/class-wc-order-data-store-custom-table.php b/includes/class-wc-order-data-store-custom-table.php index 9210c16..89d39ce 100644 --- a/includes/class-wc-order-data-store-custom-table.php +++ b/includes/class-wc-order-data-store-custom-table.php @@ -185,8 +185,8 @@ protected function update_post_meta( &$order ) { 'customer_ip_address' => $order->get_customer_ip_address( 'edit' ), 'customer_user_agent' => $order->get_customer_user_agent( 'edit' ), 'created_via' => $order->get_created_via( 'edit' ), - 'date_completed' => $order->get_date_completed( 'edit' ), - 'date_paid' => $order->get_date_paid( 'edit' ), + 'date_completed' => (string) $order->get_date_completed( 'edit' ), + 'date_paid' => (string) $order->get_date_paid( 'edit' ), 'cart_hash' => $order->get_cart_hash( 'edit' ), 'billing_first_name' => $order->get_billing_first_name( 'edit' ), diff --git a/tests/test-data-store.php b/tests/test-data-store.php index c313f7e..e2f6575 100644 --- a/tests/test-data-store.php +++ b/tests/test-data-store.php @@ -40,6 +40,43 @@ public function test_create() { $this->assertEquals( 'wc_' . $order_key, $order->get_order_key() ); } + public function test_update_post_meta_for_new_order() { + $order = new WC_Order( wp_insert_post( array( + 'post_type' => 'product', + ) ) ); + $order->set_currency( 'USD' ); + $order->set_prices_include_tax( false ); + $order->set_customer_ip_address( '127.0.0.1' ); + $order->set_customer_user_agent( 'PHPUnit' ); + + $this->invoke_update_post_meta( $order, true ); + + $row = $this->get_order_row( $order->get_id() ); + + $this->assertEquals( 'USD', $row['currency'] ); + $this->assertEquals( '127.0.0.1', $row['customer_ip_address'] ); + $this->assertEquals( 'PHPUnit', $row['customer_user_agent'] ); + } + + /** + * When inserting rows into the database, $wpdb->prepare() can't accept WC_DateTime objects. + */ + public function test_update_post_meta_casts_dates_as_strings() { + $order = new WC_Order( wp_insert_post( array( + 'post_type' => 'product', + ) ) ); + $time = time(); + $order->set_date_paid( $time ); + $order->set_date_completed( $time ); + + $this->invoke_update_post_meta( $order, true ); + + $row = $this->get_order_row( $order->get_id() ); + + $this->assertEquals( $time, strtotime( $row['date_paid'] ) ); + $this->assertEquals( $time, strtotime( $row['date_completed'] ) ); + } + public function test_get_order_count() { $instance = new WC_Order_Data_Store_Custom_Table(); $orders = $this->factory()->order->create_many( 5, array( @@ -191,4 +228,23 @@ public function order_type_provider() { return $types; } + + /** + * Shortcut for setting up reflection methods + properties for update_post_meta(). + * + * @param WC_Order $order The order object, passed by reference. + * @param bool $creating Optional. The value 'creating' property in the new instance. + * Default is false. + */ + protected function invoke_update_post_meta( &$order, $creating = false ) { + $instance = new WC_Order_Data_Store_Custom_Table(); + + $property = new ReflectionProperty( $instance, 'creating' ); + $property->setAccessible( true ); + $property->setValue( $instance, (bool) $creating ); + + $method = new ReflectionMethod( $instance, 'update_post_meta' ); + $method->setAccessible( true ); + $method->invokeArgs( $instance, array( &$order ) ); + } } From 8e2b43cf871395ffbc4eee0587396c639cd7f99a Mon Sep 17 00:00:00 2001 From: Steve Grunwell Date: Fri, 5 Jan 2018 19:27:37 +0000 Subject: [PATCH 06/52] Instead of extending Abstract_WC_Order_Data_Store_CPT directly, extend the existing WC_Order_Data_Store_CPT class. This class already contains a lot of the funtionality we need, and it's much cleaner to extend the existing, core class with one that overrides only what we need to change. --- includes/class-wc-order-data-store-custom-table.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-wc-order-data-store-custom-table.php b/includes/class-wc-order-data-store-custom-table.php index 89d39ce..aa74701 100644 --- a/includes/class-wc-order-data-store-custom-table.php +++ b/includes/class-wc-order-data-store-custom-table.php @@ -12,7 +12,7 @@ * * Orders are still treated as posts within WordPress, but the data is stored in a separate table. */ -class WC_Order_Data_Store_Custom_Table extends Abstract_WC_Order_Data_Store_CPT implements WC_Object_Data_Store_Interface, WC_Order_Data_Store_Interface { +class WC_Order_Data_Store_Custom_Table extends WC_Order_Data_Store_CPT { /** * Set to true when creating so we know to insert meta data. From b44367ac0b0528f6889ffee0b66bf95e9a54273a Mon Sep 17 00:00:00 2001 From: Steve Grunwell Date: Fri, 5 Jan 2018 19:39:24 +0000 Subject: [PATCH 07/52] Now that we're extending WC_Order_Data_Store_CPT, remove any code that was wholesale copied + pasted from the parent class --- ...class-wc-order-data-store-custom-table.php | 242 ------------------ tests/test-data-store.php | 50 ---- 2 files changed, 292 deletions(-) diff --git a/includes/class-wc-order-data-store-custom-table.php b/includes/class-wc-order-data-store-custom-table.php index aa74701..722e02e 100644 --- a/includes/class-wc-order-data-store-custom-table.php +++ b/includes/class-wc-order-data-store-custom-table.php @@ -261,17 +261,6 @@ protected function update_post_meta( &$order ) { do_action( 'woocommerce_order_object_updated_props', $order, $updated_props ); } - /** - * Excerpt for post. - * - * @param WC_Order $order The order object. - * - * @return string The post excerpt. - */ - protected function get_post_excerpt( $order ) { - return $order->get_customer_note(); - } - /** * Get amount already refunded. * @@ -359,24 +348,6 @@ public function get_order_id_by_order_key( $order_key ) { ) ); } - /** - * Return count of orders with a specific status. - * - * @global $wpdb - * - * @param string $status The post_status to filter orders by. - * - * @return int The number of orders with that status. - */ - public function get_order_count( $status ) { - global $wpdb; - - return absint( $wpdb->get_var( $wpdb->prepare( - "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = 'shop_order' AND post_status = %s", - $status - ) ) ); - } - /** * Get all orders matching the passed in args. * @@ -476,74 +447,6 @@ public function get_orders( $args = array() ) { } } - /** - * Generate meta query for wc_get_orders. - * - * @param array $values Values to populate the meta query. - * @param string $relation Optional The query relationship, either "and" or "or". Default is "or". - * - * @return array An array suitable for passing to WP_Query's meta_query argument. - */ - private function get_orders_generate_customer_meta_query( $values, $relation = 'or' ) { - $meta_query = array( - 'relation' => strtoupper( $relation ), - 'customer_emails' => array( - 'key' => '_billing_email', - 'value' => array(), - 'compare' => 'IN', - ), - 'customer_ids' => array( - 'key' => '_customer_user', - 'value' => array(), - 'compare' => 'IN', - ), - ); - - foreach ( $values as $value ) { - if ( is_array( $value ) ) { - $meta_query[] = $this->get_orders_generate_customer_meta_query( $value, 'and' ); - } elseif ( is_email( $value ) ) { - $meta_query['customer_emails']['value'][] = sanitize_email( $value ); - } else { - $meta_query['customer_ids']['value'][] = strval( absint( $value ) ); - } - } - - if ( empty( $meta_query['customer_emails']['value'] ) ) { - unset( $meta_query['customer_emails'] ); - unset( $meta_query['relation'] ); - } - - if ( empty( $meta_query['customer_ids']['value'] ) ) { - unset( $meta_query['customer_ids'] ); - unset( $meta_query['relation'] ); - } - - return $meta_query; - } - - /** - * Get unpaid orders after a certain date. - * - * @param int $date The Unix timestamp used for date filtering. - * - * @return array An array of unpaid orders. - */ - public function get_unpaid_orders( $date ) { - global $wpdb; - - $order_types = wc_get_order_types(); - - return $wpdb->get_col( $wpdb->prepare( - "SELECT ID - FROM $wpdb->posts - WHERE post_type IN (" . implode( ',', array_fill( 0, count( $order_types ), '%s' ) ) . ") - AND post_status = 'wc-pending' - AND post_modified < %s", - array_merge( $order_types, array( date( 'Y-m-d H:i:s', (int) $date ) ) ) - ) ); - } - /** * Search order data for a term and return ids. * @@ -600,118 +503,6 @@ public function search_orders( $term ) { ) ); } - /** - * Gets information about whether permissions were generated yet. - * - * @param WC_Order|int $order The order object or ID. - * - * @return bool - */ - public function get_download_permissions_granted( $order ) { - $order_id = WC_Order_Factory::get_order_id( $order ); - - return wc_string_to_bool( get_post_meta( $order_id, '_download_permissions_granted', true ) ); - } - - /** - * Stores information about whether permissions were generated yet. - * - * @param WC_Order|int $order The order object or ID. - * @param bool $set Whether or not the permissions have been generated. - */ - public function set_download_permissions_granted( $order, $set ) { - $order_id = WC_Order_Factory::get_order_id( $order ); - - update_post_meta( $order_id, '_download_permissions_granted', wc_bool_to_string( $set ) ); - } - - /** - * Gets information about whether sales were recorded. - * - * @param WC_Order|int $order The order object or ID. - * - * @return bool - */ - public function get_recorded_sales( $order ) { - $order_id = WC_Order_Factory::get_order_id( $order ); - - return wc_string_to_bool( get_post_meta( $order_id, '_recorded_sales', true ) ); - } - - /** - * Stores information about whether sales were recorded. - * - * @param WC_Order|int $order The order object or ID. - * @param bool $set Whether or not the sales have been recorded. - */ - public function set_recorded_sales( $order, $set ) { - $order_id = WC_Order_Factory::get_order_id( $order ); - - update_post_meta( $order_id, '_recorded_sales', wc_bool_to_string( $set ) ); - } - - /** - * Gets information about whether coupon counts were updated. - * - * @param WC_Order|int $order The order object or ID. - * - * @return bool - */ - public function get_recorded_coupon_usage_counts( $order ) { - $order_id = WC_Order_Factory::get_order_id( $order ); - - return wc_string_to_bool( get_post_meta( $order_id, '_recorded_coupon_usage_counts', true ) ); - } - - /** - * Stores information about whether coupon counts were updated. - * - * @param WC_Order|int $order The order object or ID. - * @param bool $set Whether or not the coupon counts have been updated. - */ - public function set_recorded_coupon_usage_counts( $order, $set ) { - $order_id = WC_Order_Factory::get_order_id( $order ); - - update_post_meta( $order_id, '_recorded_coupon_usage_counts', wc_bool_to_string( $set ) ); - } - - /** - * Gets information about whether stock was reduced. - * - * @param WC_Order|int $order The order object or ID. - * - * @return bool - */ - public function get_stock_reduced( $order ) { - $order_id = WC_Order_Factory::get_order_id( $order ); - - return wc_string_to_bool( get_post_meta( $order_id, '_order_stock_reduced', true ) ); - } - - /** - * Stores information about whether stock was reduced. - * - * @param WC_Order|int $order The order object or ID. - * - * @param bool $set Whether or not stock has been reduced. - */ - public function set_stock_reduced( $order, $set ) { - $order_id = WC_Order_Factory::get_order_id( $order ); - - update_post_meta( $order_id, '_order_stock_reduced', wc_bool_to_string( $set ) ); - } - - /** - * Get the order type based on Order ID. - * - * @param int $order_id The order ID. - * - * @return string The order post type. - */ - public function get_order_type( $order_id ) { - return get_post_type( $order_id ); - } - /** * Populate custom table with data from postmeta, for migrations. * @@ -779,37 +570,4 @@ public function backfill_postmeta( &$order ) { } } } - - /** - * Query for Orders matching specific criteria. - * - * @param array $query_vars Query arguments from a WC_Order_Query. - * - * @return array|object - */ - public function query( $query_vars ) { - $args = $this->get_wp_query_args( $query_vars ); - - if ( ! empty( $args['errors'] ) ) { - $query = (object) array( - 'posts' => array(), - 'found_posts' => 0, - 'max_num_pages' => 0, - ); - } else { - $query = new WP_Query( $args ); - } - - $orders = ( isset( $query_vars['return'] ) && 'ids' === $query_vars['return'] ) ? $query->posts : array_filter( array_map( 'wc_get_order', $query->posts ) ); - - if ( isset( $query_vars['paginate'] ) && $query_vars['paginate'] ) { - return (object) array( - 'orders' => $orders, - 'total' => $query->found_posts, - 'max_num_pages' => $query->max_num_pages, - ); - } - - return $orders; - } } diff --git a/tests/test-data-store.php b/tests/test-data-store.php index e2f6575..d4c55c1 100644 --- a/tests/test-data-store.php +++ b/tests/test-data-store.php @@ -77,56 +77,6 @@ public function test_update_post_meta_casts_dates_as_strings() { $this->assertEquals( $time, strtotime( $row['date_completed'] ) ); } - public function test_get_order_count() { - $instance = new WC_Order_Data_Store_Custom_Table(); - $orders = $this->factory()->order->create_many( 5, array( - 'post_status' => 'wc-pending', - ) ); - - $this->assertEquals( - count( $orders ), - $instance->get_order_count( 'wc-pending' ) - ); - } - - public function test_get_order_count_filters_by_status() { - $instance = new WC_Order_Data_Store_Custom_Table(); - $this->factory()->order->create( array( - 'post_status' => 'not_a_pending_status', - ) ); - - $this->assertEquals( - 0, - $instance->get_order_count( 'wc-pending' ), - 'The get_order_count() method should only count records matching $status.' - ); - } - - public function test_get_unpaid_orders() { - $instance = new WC_Order_Data_Store_Custom_Table(); - $order = $this->factory()->order->create( array( - 'post_status' => 'wc-pending', - ) ); - $pending = $instance->get_unpaid_orders( time() + DAY_IN_SECONDS ); - - $this->assertCount( 1, $pending, 'There should be only one unpaid order.' ); - $this->assertEquals( - $order, - array_shift( $pending ), - 'The ID of the one unpaid order should be that of $order.' - ); - } - - public function test_get_unpaid_orders_uses_date_filtering() { - $instance = new WC_Order_Data_Store_Custom_Table(); - $order = $this->factory()->order->create( array( - 'post_status' => 'wc-pending', - ) ); - $pending = $instance->get_unpaid_orders( time() - HOUR_IN_SECONDS ); - - $this->assertEmpty( $pending, 'No unpaid orders should match the time window.' ); - } - public function test_search_orders_can_search_by_order_id() { $instance = new WC_Order_Data_Store_Custom_Table(); From 86700f5694c83d0f79617af10931331ad55ce71c Mon Sep 17 00:00:00 2001 From: Steve Grunwell Date: Fri, 5 Jan 2018 19:43:40 +0000 Subject: [PATCH 08/52] Update the class comment for WC_Order_Data_Store_Custom_Table --- includes/class-wc-order-data-store-custom-table.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/includes/class-wc-order-data-store-custom-table.php b/includes/class-wc-order-data-store-custom-table.php index 722e02e..3caf815 100644 --- a/includes/class-wc-order-data-store-custom-table.php +++ b/includes/class-wc-order-data-store-custom-table.php @@ -7,10 +7,10 @@ */ /** - * Extension of the Abstract_WC_Order_Data_Store_CPT class, designed to map data between - * WooCommerce and the custom database table. + * Extend the WC_Order_Data_Store_CPT class, overloading methods that require database access in + * order to use the new table. * - * Orders are still treated as posts within WordPress, but the data is stored in a separate table. + * Orders are still treated as posts within WordPress, but the meta is stored in a separate table. */ class WC_Order_Data_Store_Custom_Table extends WC_Order_Data_Store_CPT { From fbf04b12a3e11a558975f49602578d929f579833 Mon Sep 17 00:00:00 2001 From: Steve Grunwell Date: Fri, 5 Jan 2018 20:05:53 +0000 Subject: [PATCH 09/52] Simplify WC_Order_Data_Store_Custom_Table::create(), only setting the 'created' property --- .../class-wc-order-data-store-custom-table.php | 8 -------- tests/test-data-store.php | 18 ++++++++++++------ 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/includes/class-wc-order-data-store-custom-table.php b/includes/class-wc-order-data-store-custom-table.php index 3caf815..6f2491a 100644 --- a/includes/class-wc-order-data-store-custom-table.php +++ b/includes/class-wc-order-data-store-custom-table.php @@ -88,14 +88,6 @@ public function get_postmeta_mapping() { * @param WC_Order $order The order object, passed by reference. */ public function create( &$order ) { - /** - * Filter the generated order ID. - * - * @param string $order_key The uniquely-generated ID for this order. - */ - $order_key = apply_filters( 'woocommerce_generate_order_key', uniqid( 'order_' ) ); - - $order->set_order_key( 'wc_' . $order_key ); $this->creating = true; parent::create( $order ); diff --git a/tests/test-data-store.php b/tests/test-data-store.php index d4c55c1..793acad 100644 --- a/tests/test-data-store.php +++ b/tests/test-data-store.php @@ -27,17 +27,23 @@ public function remove_filter_callbacks() { } public function test_create() { - $instance = new WC_Order_Data_Store_Custom_Table(); - $order = $this->factory()->order->create_and_get(); - $order_key = 'my_custom_order_key'; + $instance = new WC_Order_Data_Store_Custom_Table(); + $property = new ReflectionProperty( $instance, 'creating' ); + $property->setAccessible( true ); + $order = new WC_Order( wp_insert_post( array( + 'post_type' => 'product', + ) ) ); - add_filter( 'woocommerce_generate_order_key', function () use ( $order_key ) { - return $order_key; + add_action( 'wp_insert_post', function () use ( $property, $instance ) { + $this->assertTrue( + $property->getValue( $instance ), + 'As an order is being created, WC_Order_Data_Store_Custom_Table::$creating should be true' + ); } ); $instance->create( $order ); - $this->assertEquals( 'wc_' . $order_key, $order->get_order_key() ); + $this->assertEquals( 1, did_action( 'wp_insert_post' ), 'Expected the "wp_insert_post" action to have been fired.' ); } public function test_update_post_meta_for_new_order() { From f6089b40d8194fc3891d82dc59f0decc46de3569 Mon Sep 17 00:00:00 2001 From: Steve Grunwell Date: Fri, 5 Jan 2018 20:13:01 +0000 Subject: [PATCH 10/52] Remove more methods (and related tests) that are directly copied from the parent class --- ...class-wc-order-data-store-custom-table.php | 71 ------------------- tests/test-data-store.php | 28 -------- 2 files changed, 99 deletions(-) diff --git a/includes/class-wc-order-data-store-custom-table.php b/includes/class-wc-order-data-store-custom-table.php index 6f2491a..6e03b93 100644 --- a/includes/class-wc-order-data-store-custom-table.php +++ b/includes/class-wc-order-data-store-custom-table.php @@ -253,77 +253,6 @@ protected function update_post_meta( &$order ) { do_action( 'woocommerce_order_object_updated_props', $order, $updated_props ); } - /** - * Get amount already refunded. - * - * @global $wpdb - * - * @param WC_Order $order The order object. - * - * @return string The refund amount. - */ - public function get_total_refunded( $order ) { - global $wpdb; - - $total = $wpdb->get_var( $wpdb->prepare( " - SELECT SUM( postmeta.meta_value ) - FROM $wpdb->postmeta AS postmeta - INNER JOIN $wpdb->posts AS posts ON ( posts.post_type = 'shop_order_refund' AND posts.post_parent = %d ) - WHERE postmeta.meta_key = '_refund_amount' - AND postmeta.post_id = posts.ID - ", $order->get_id() ) ); - - return $total; - } - - /** - * Get the total tax refunded. - * - * @global $wpdb - * - * @param WC_Order $order The order object. - * - * @return float The total refunded tax. - */ - public function get_total_tax_refunded( $order ) { - global $wpdb; - - $total = $wpdb->get_var( $wpdb->prepare( " - SELECT SUM( order_itemmeta.meta_value ) - FROM {$wpdb->prefix}woocommerce_order_itemmeta AS order_itemmeta - INNER JOIN $wpdb->posts AS posts ON ( posts.post_type = 'shop_order_refund' AND posts.post_parent = %d ) - INNER JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON ( order_items.order_id = posts.ID AND order_items.order_item_type = 'tax' ) - WHERE order_itemmeta.order_item_id = order_items.order_item_id - AND order_itemmeta.meta_key IN ('tax_amount', 'shipping_tax_amount') - ", $order->get_id() ) ); - - return abs( $total ); - } - - /** - * Get the total shipping refunded. - * - * @global $wpdb - * - * @param WC_Order $order The order object. - * - * @return float The total refunded shipping. - */ - public function get_total_shipping_refunded( $order ) { - global $wpdb; - - $total = $wpdb->get_var( $wpdb->prepare( " - SELECT SUM( order_itemmeta.meta_value ) - FROM {$wpdb->prefix}woocommerce_order_itemmeta AS order_itemmeta - INNER JOIN $wpdb->posts AS posts ON ( posts.post_type = 'shop_order_refund' AND posts.post_parent = %d ) - INNER JOIN {$wpdb->prefix}woocommerce_order_items AS order_items ON ( order_items.order_id = posts.ID AND order_items.order_item_type = 'shipping' ) - WHERE order_itemmeta.order_item_id = order_items.order_item_id - AND order_itemmeta.meta_key IN ('cost') - ", $order->get_id() ) ); - - return abs( $total ); - } - /** * Finds an order ID based on its order key. * diff --git a/tests/test-data-store.php b/tests/test-data-store.php index 793acad..15b2c0a 100644 --- a/tests/test-data-store.php +++ b/tests/test-data-store.php @@ -157,34 +157,6 @@ public function test_search_orders_checks_table_for_product_item_matches_with_li ); } - /** - * @dataProvider order_type_provider() - */ - public function test_get_order_type( $order_type ) { - $instance = new WC_Order_Data_Store_Custom_Table(); - $order = $this->factory()->order->create( array( - 'post_type' => $order_type, - ) ); - - $this->assertEquals( - $order_type, - $instance->get_order_type( $order ) - ); - } - - /** - * Provide a list of all available order types. - */ - public function order_type_provider() { - $types = array(); - - foreach ( wc_get_order_types() as $type ) { - $types[ $type ] = array( $type ); - } - - return $types; - } - /** * Shortcut for setting up reflection methods + properties for update_post_meta(). * From 58a7140288d7501188b6db4c5633b56a92c0fbd0 Mon Sep 17 00:00:00 2001 From: Steve Grunwell Date: Fri, 5 Jan 2018 20:19:16 +0000 Subject: [PATCH 11/52] Update the test-coverage Composer script to only run the 'plugin' test suite. Additionally, fix a merge conflict that produced two 'autoload-dev' nodes. --- composer.json | 6 ++---- composer.lock | 14 ++++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/composer.json b/composer.json index addfba4..42d7f99 100644 --- a/composer.json +++ b/composer.json @@ -18,12 +18,10 @@ }, "autoload-dev": { "classmap": [ + "tests/test-tools", "vendor/woocommerce/woocommerce/tests/framework" ] }, - "autoload-dev": { - "classmap": ["tests/test-tools"] - }, "scripts": { "post-install-cmd": [ "xrstf\\Composer52\\Generator::onPostInstallCmd" @@ -35,7 +33,7 @@ "xrstf\\Composer52\\Generator::onPostInstallCmd" ], "test-coverage": [ - "phpunit --coverage-html=tests/coverage" + "phpunit --testsuite=plugin --coverage-html=tests/coverage" ] }, "extra": { diff --git a/composer.lock b/composer.lock index 95c9636..0ee0683 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "173c6a9e5a12528c9f0f8762e81e63a9", + "content-hash": "fe30de23a7822b2387c810e945f9be02", "packages": [ { "name": "composer/installers", @@ -165,12 +165,12 @@ "source": { "type": "git", "url": "https://github.com/woocommerce/woocommerce.git", - "reference": "ae9a98b0a01ba7767907aa2c3e98f006a10cfaad" + "reference": "aad66effb7ae796321348664b47117fd6fcb60f2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/woocommerce/woocommerce/zipball/ae9a98b0a01ba7767907aa2c3e98f006a10cfaad", - "reference": "ae9a98b0a01ba7767907aa2c3e98f006a10cfaad", + "url": "https://api.github.com/repos/woocommerce/woocommerce/zipball/aad66effb7ae796321348664b47117fd6fcb60f2", + "reference": "aad66effb7ae796321348664b47117fd6fcb60f2", "shasum": "" }, "require": { @@ -199,7 +199,7 @@ ], "description": "An eCommerce toolkit that helps you sell anything. Beautifully.", "homepage": "https://woocommerce.com/", - "time": "2018-01-04T16:17:30+00:00" + "time": "2018-01-05T20:03:12+00:00" } ], "aliases": [], @@ -209,6 +209,8 @@ }, "prefer-stable": false, "prefer-lowest": false, - "platform": [], + "platform": { + "php": ">=5.2" + }, "platform-dev": [] } From 22f2fa3fcaf10428643e83359c5da67bbbc44710 Mon Sep 17 00:00:00 2001 From: Steve Grunwell Date: Fri, 5 Jan 2018 20:29:27 +0000 Subject: [PATCH 12/52] Remove a giant get_order() method which, based on the parent class, has been deprecated and isn't even being called --- ...class-wc-order-data-store-custom-table.php | 99 ------------------- 1 file changed, 99 deletions(-) diff --git a/includes/class-wc-order-data-store-custom-table.php b/includes/class-wc-order-data-store-custom-table.php index 6e03b93..51646eb 100644 --- a/includes/class-wc-order-data-store-custom-table.php +++ b/includes/class-wc-order-data-store-custom-table.php @@ -269,105 +269,6 @@ public function get_order_id_by_order_key( $order_key ) { ) ); } - /** - * Get all orders matching the passed in args. - * - * @see wc_get_orders() - * - * @param array $args { - * Query arguments. All arguments are optional. - * - * @var string $type The post type. Default is 'shop_order'. - * @var - * } - * @return object|array array of orders - */ - public function get_orders( $args = array() ) { - /** - * Generate WP_Query args. This logic will change if orders are moved to - * custom tables in the future. - */ - $wp_query_args = array( - 'post_type' => $args['type'] ? $args['type'] : 'shop_order', - 'post_status' => $args['status'], - 'posts_per_page' => $args['limit'], - 'meta_query' => array(), - 'fields' => 'ids', - 'orderby' => $args['orderby'], - 'order' => $args['order'], - ); - $wc_customer_query = array(); - - if ( ! empty( $args['customer'] ) ) { - $values = is_array( $args['customer'] ) ? $args['customer'] : array( $args['customer'] ); - $wc_customer_query = array_merge( $wc_customer_query, $values ); - } - - if ( ! empty( $args['email'] ) ) { - $values = is_array( $args['email'] ) ? $args['email'] : array( $args['email'] ); - $wc_customer_query = array_merge( $wc_customer_query, $values ); - } - - if ( ! empty( $wc_customer_query ) ) { - $wp_query_args['wc_customer_query'] = $wc_customer_query; - } - - /** - * Standard Args - */ - if ( ! is_null( $args['parent'] ) ) { - $wp_query_args['post_parent'] = absint( $args['parent'] ); - } - - if ( ! is_null( $args['offset'] ) ) { - $wp_query_args['offset'] = absint( $args['offset'] ); - } else { - $wp_query_args['paged'] = absint( $args['page'] ); - } - - if ( ! empty( $args['exclude'] ) ) { - $wp_query_args['post__not_in'] = array_map( 'absint', $args['exclude'] ); - } - - if ( ! $args['paginate'] ) { - $wp_query_args['no_found_rows'] = true; - } - - if ( ! empty( $args['date_before'] ) ) { - $wp_query_args['date_query']['before'] = $args['date_before']; - } - - if ( ! empty( $args['date_after'] ) ) { - $wp_query_args['date_query']['after'] = $args['date_after']; - } - - /** - * Filter WP_Query arguments when retrieving orders from the database. - * - * @param array $wp_query_args The current WP_Query arguments. - * @param array $args Raw arguments passed to WC_Order_Data_Store_Custom_Table::get_orders(). - * @param WC_Order_Data_Store_Custom_Table $instance The current instance of the WC_Order_Data_Store_Custom_Table class. - */ - $wp_query_args = apply_filters( 'woocommerce_order_data_store_cpt_get_orders_query', $wp_query_args, $args, $this ); - $orders = new WP_Query( $wp_query_args ); - - if ( 'objects' === $args['return'] ) { - $return = array_map( 'wc_get_order', $orders->posts ); - } else { - $return = $orders->posts; - } - - if ( $args['paginate'] ) { - return (object) array( - 'orders' => $return, - 'total' => $orders->found_posts, - 'max_num_pages' => $orders->max_num_pages, - ); - } else { - return $return; - } - } - /** * Search order data for a term and return ids. * From 6af8a5e8cc15e617cbebeda6b366db8aa009be98 Mon Sep 17 00:00:00 2001 From: Steve Grunwell Date: Fri, 5 Jan 2018 20:37:12 +0000 Subject: [PATCH 13/52] Add a test for get_order_id_by_order_key() --- tests/test-data-store.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test-data-store.php b/tests/test-data-store.php index 15b2c0a..583d8fa 100644 --- a/tests/test-data-store.php +++ b/tests/test-data-store.php @@ -83,6 +83,13 @@ public function test_update_post_meta_casts_dates_as_strings() { $this->assertEquals( $time, strtotime( $row['date_completed'] ) ); } + public function test_get_order_id_by_order_key() { + $order = WC_Helper_Order::create_order(); + $instance = new WC_Order_Data_Store_Custom_Table(); + + $this->assertEquals( $order->get_id(), $instance->get_order_id_by_order_key( $order->get_order_key() ) ); + } + public function test_search_orders_can_search_by_order_id() { $instance = new WC_Order_Data_Store_Custom_Table(); From c918d3fcd79df35f5e4b6deaacddedc5f655df7c Mon Sep 17 00:00:00 2001 From: Steve Grunwell Date: Fri, 5 Jan 2018 21:14:06 +0000 Subject: [PATCH 14/52] Add tests around delete() and clean up the method a bit --- ...class-wc-order-data-store-custom-table.php | 10 +++------ tests/test-data-store.php | 22 +++++++++++++++++++ 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/includes/class-wc-order-data-store-custom-table.php b/includes/class-wc-order-data-store-custom-table.php index 51646eb..7611ad8 100644 --- a/includes/class-wc-order-data-store-custom-table.php +++ b/includes/class-wc-order-data-store-custom-table.php @@ -108,13 +108,9 @@ public function delete( &$order, $args = array() ) { parent::delete( $order, $args ); - if ( $args['force_delete'] || 0 == $order->get_id() ) { - $wpdb->delete( - "{$wpdb->prefix}woocommerce_orders", - array( - 'order_id' => $order_id, - ) - ); + // Delete the database row if force_delete is true. + if ( isset( $args['force_delete'] ) && $args['force_delete'] ) { + $wpdb->delete("{$wpdb->prefix}woocommerce_orders", array( 'order_id' => $order_id ) ); } } diff --git a/tests/test-data-store.php b/tests/test-data-store.php index 583d8fa..2062d03 100644 --- a/tests/test-data-store.php +++ b/tests/test-data-store.php @@ -46,6 +46,28 @@ public function test_create() { $this->assertEquals( 1, did_action( 'wp_insert_post' ), 'Expected the "wp_insert_post" action to have been fired.' ); } + public function test_delete() { + $instance = new WC_Order_Data_Store_Custom_Table(); + $order = WC_Helper_Order::create_order(); + + $instance->delete( $order, array( 'force_delete' => false ) ); + + $this->assertNotNull( + $this->get_order_row( $order->get_id() ), + 'Unless force_delete is true, the table row should not be removed.' + ); + } + + public function test_delete_can_force_delete() { + $instance = new WC_Order_Data_Store_Custom_Table(); + $order = WC_Helper_Order::create_order(); + $order_id = $order->get_id(); + + $instance->delete( $order, array( 'force_delete' => true ) ); + + $this->assertNull( $this->get_order_row( $order_id ), 'When force deleting, the table row should be removed.' ); + } + public function test_update_post_meta_for_new_order() { $order = new WC_Order( wp_insert_post( array( 'post_type' => 'product', From f7679e8c8402f495502440ec320454d0d73d3d5e Mon Sep 17 00:00:00 2001 From: Steve Grunwell Date: Tue, 9 Jan 2018 02:55:36 +0000 Subject: [PATCH 15/52] There's a lot going on here, but the general goal is to replace meta queries with calls to the custom table, when available. If a column doesn't exist, WordPress should fall back to the meta queries --- includes/class-wc-custom-order-table.php | 79 -------- ...class-wc-order-data-store-custom-table.php | 170 ++++++++++++++++++ 2 files changed, 170 insertions(+), 79 deletions(-) diff --git a/includes/class-wc-custom-order-table.php b/includes/class-wc-custom-order-table.php index 08fb2f2..d5acd9c 100644 --- a/includes/class-wc-custom-order-table.php +++ b/includes/class-wc-custom-order-table.php @@ -30,7 +30,6 @@ public function setup() { // Inject the plugin into order processing. add_filter( 'woocommerce_order_data_store', array( $this, 'order_data_store' ) ); - add_filter( 'posts_join', array( $this, 'wp_query_customer_query' ), 10, 2 ); // Register the CLI command if we're running WP_CLI. if ( defined( 'WP_CLI' ) && WP_CLI ) { @@ -60,82 +59,4 @@ public function get_table_name() { public function order_data_store() { return 'WC_Order_Data_Store_Custom_Table'; } - - /** - * Modify posts_join queries when the query includes wc_customer_query. - * - * @global $wpdb - * - * @param string $join The SQL JOIN statement. - * @param WP_Query $wp_query The current WP_Query object. - * - * @return string The [potentially] filtered JOIN statement. - */ - public function wp_query_customer_query( $join, $wp_query ) { - global $wpdb; - - // If there is no wc_customer_query then no need to process anything. - if ( ! isset( $wp_query->query_vars['wc_customer_query'] ) ) { - return $join; - } - - $customer_query = $this->generate_wc_customer_query( $wp_query->query_vars['wc_customer_query'] ); - $query_parts = array(); - - if ( ! empty( $customer_query['emails'] ) ) { - $emails = '\'' . implode( '\', \'', array_unique( $customer_query['emails'] ) ) . '\''; - $query_parts[] = "{$this->get_table_name()}.billing_email IN ( {$emails} )"; - } - - if ( ! empty( $customer_query['users'] ) ) { - $users = implode( ',', array_unique( $customer_query['users'] ) ); - $query_parts[] = "{$this->get_table_name()}.customer_id IN ( {$users} )"; - } - - if ( ! empty( $query_parts ) ) { - $query = '( ' . implode( ') OR (', $query_parts ) . ' )'; - $join .= "JOIN {$this->get_table_name()} ON - ( {$wpdb->posts}.ID = {$this->get_table_name()}.order_id ) - AND ( {$query} )"; - } - - return $join; - } - - /** - * Given a wc_customer_query argument, construct an array of customers grouped by either email - * address or user ID. - * - * @param array $values Query arguments from WP_Query->query_vars['wc_customer_query']. - * - * @return array A complex array with two keys: "emails" and "users". - */ - public function generate_wc_customer_query( $values ) { - $customer_query = array( - 'emails' => array(), - 'users' => array(), - ); - - foreach ( $values as $value ) { - // If the value is an array, call this method recursively and merge the results. - if ( is_array( $value ) ) { - $query = $this->generate_wc_customer_query( $value ); - - if ( is_array( $query['emails'] ) ) { - $customer_query['emails'] = array_merge( $customer_query['emails'], $query['emails'] ); - } - - if ( is_array( $query['users'] ) ) { - $customer_query['users'] = array_merge( $customer_query['users'], $query['users'] ); - } - } elseif ( is_email( $value ) ) { - $customer_query['emails'][] = sanitize_email( $value ); - - } else { - $customer_query['users'][] = strval( absint( $value ) ); - } - } - - return $customer_query; - } } diff --git a/includes/class-wc-order-data-store-custom-table.php b/includes/class-wc-order-data-store-custom-table.php index 7611ad8..213fc49 100644 --- a/includes/class-wc-order-data-store-custom-table.php +++ b/includes/class-wc-order-data-store-custom-table.php @@ -73,6 +73,10 @@ class WC_Order_Data_Store_Custom_Table extends WC_Order_Data_Store_CPT { 'prices_include_tax' => '_prices_include_tax', ); + public function __construct() { + add_filter( 'woocommerce_order_data_store_cpt_get_orders_query', array( $this, 'filter_database_queries' ), 10, 2 ); + } + /** * Retrieve the database table column => post_meta mapping. * @@ -138,6 +142,49 @@ protected function read_order_data( &$order, $post_object ) { } } + /** + * Determine if any filters are required on the MySQL query and, if so, apply them. + * + * @param array $query_args The arguments to be passed to WP_Query. + * @param array $query_vars The raw query vars passed to build the query. + * + * @return array The potentially-filtered $query_args array. + */ + public function filter_database_queries( $query_args, $query_vars ) { + + // No 'meta_query' node means no changes are needed. + if ( ! isset( $query_args['meta_query'] ) || empty( $query_args['meta_query'] ) ) { + return $query_args; + } + + // Chances are we'll have some modifications. + $query_args['wc_order_meta_query'] = array(); + + /* + * Remove some of the query adjustments that may have been made in the + * WC_Order_Data_Store_CPT::get_wp_query_args() method. + */ + foreach ( $query_args['meta_query'] as $index => $meta_query ) { + if ( isset( $meta_query['customer_ids'] ) || isset( $meta_query['customer_emails'] ) ) { + unset( $query_args['meta_query'][ $index ] ); + add_filter( 'posts_where', __CLASS__ . '::customer_query_where', 10, 2 ); + + } elseif ( $column = array_search( $meta_query['key'], $this->get_postmeta_mapping(), true ) ) { + $query_args['wc_order_meta_query'][] = array_merge( $meta_query, array( + 'key' => $column, + '_old_key' => $meta_query['key'], + ) ); + + } + } + + // Add filters to address specific portions of the query. + add_filter( 'posts_join', __CLASS__ . '::posts_join' ); + add_filter( 'posts_where', __CLASS__ . '::meta_query_where', 100, 2 ); + + return $query_args; + } + /** * Retrieve a single order from the database. * @@ -388,4 +435,127 @@ public function backfill_postmeta( &$order ) { } } } + + /** + * Filter the JOIN statement generated by WP_Query. + * + * @global $wpdb + * + * @param string $join The MySQL JOIN statement. + * + * @return string The filtered JOIN statement. + */ + public static function posts_join( $join ) { + global $wpdb; + + remove_filter( 'posts_join', __CLASS__ . '::posts_join' ); + + $table = wc_custom_order_table()->get_table_name(); + $join .= " JOIN {$table} ON ( {$wpdb->posts}.ID = {$table}.order_id ) "; + + return $join; + } + + /** + * Given 'customer' arguments, build a corresponding WHERE query fragment. + * + * @global $wpdb + * + * @param string $where The MySQL WHERE statement. + * @param WP_Query $query The WP_Query object, passed by reference. + * + * @return string The [potentially-] filtered JOIN statement. + */ + public static function customer_query_where( $where, $wp_query ) { + global $wpdb; + + $customer = (array) $wp_query->get( 'customer' ); + $table = wc_custom_order_table()->get_table_name(); + $query_parts = array( + 'ids' => array(), + 'emails' => array(), + ); + $id_query = ''; + $email_query = ''; + + // Return early if there's no 'customer' parameter. + if ( ! $customer ) { + return $where; + } + + foreach ( $customer as $term ) { + if ( is_email( $term ) ) { + $query_parts['emails'][] = $term; + } else { + $query_parts['ids'][] = (int) $term; + } + } + + // Return early if we didn't get anything out of the loop. + if ( empty( $query_parts['ids'] ) && empty( $query_parts['emails'] ) ) { + return $where; + } + + // Build the ID portion of the query. + if ( ! empty( $query_parts['ids'] ) ) { + $id_query = $wpdb->prepare( + "{$table}.customer_id IN (" . implode( ', ', array_fill( 0, count( $query_parts['ids'] ), '%d' ) ) . ')', + $query_parts['ids'] + ); + } + + // Build the email portion of the query. + if ( ! empty( $query_parts['emails'] ) ) { + $email_query = $wpdb->prepare( + "{$table}.billing_email IN (" . implode( ', ', array_fill( 0, count( $query_parts['emails'] ), '%s' ) ) . ')', + $query_parts['emails'] + ); + } + + if ( $id_query && $email_query ) { + $where .= " AND ( $id_query OR $email_query ) "; + } else { + $where .= " AND ( $id_query $email_query )"; + } + + // Ensure this doesn't affect all subsequent queries. + remove_filter( 'posts_where', __CLASS__ . '::customer_query_where', 10, 2 ); + + return $where; + } + + /** + * Filter the "WHERE" portion of the MySQL query to look at the custom orders table instead of + * post meta. + * + * @global $wpdb + * + * @param string $where The MySQL WHERE statement. + * @param WP_Query $query The WP_Query object, passed by reference. + * + * @return string The [potentially-] filtered WHERE statement. + */ + public static function meta_query_where( $where, $wp_query ) { + global $wpdb; + + $meta_query = $wp_query->get( 'wc_order_meta_query' ); + $table = wc_custom_order_table()->get_table_name(); + + if ( empty( $meta_query ) ) { + return $where; + } + + foreach ( $meta_query as $query ) { + $where = str_replace( + $wpdb->prepare( "( {$wpdb->postmeta}.meta_key = %s AND {$wpdb->postmeta}.meta_value ", $query['_old_key'] ), + "( {$table}.{$query['key']} ", + $where + ); + } + + // Ensure this doesn't affect all subsequent queries. + remove_filter( 'posts_where', __CLASS__ . '::meta_query_where', 100, 2 ); + + return $where; + } } From ce90a8e364c7db9919d9b15329e25638f48049ef Mon Sep 17 00:00:00 2001 From: Steve Grunwell Date: Tue, 9 Jan 2018 17:46:01 +0000 Subject: [PATCH 16/52] Rework some of the non-instance-dependent methods to be static. Methods that will be used as filter callbacks and never change based on the state of the object are really not appropriate as non-static methods, especially when we need to be able to hook and un-hook them from filter callbacks. For example, get_postmeta_mapping() will always return the same, hard-coded array, as nothing within the data store class was altering the (now-removed) $postmeta_mapping property. Having a static reference makes it much easier to add/remove actions and filters, since there's only a single instance to worry about. --- ...class-wc-order-data-store-custom-table.php | 224 +++++++++--------- 1 file changed, 115 insertions(+), 109 deletions(-) diff --git a/includes/class-wc-order-data-store-custom-table.php b/includes/class-wc-order-data-store-custom-table.php index 213fc49..22a39b0 100644 --- a/includes/class-wc-order-data-store-custom-table.php +++ b/includes/class-wc-order-data-store-custom-table.php @@ -21,60 +21,10 @@ class WC_Order_Data_Store_Custom_Table extends WC_Order_Data_Store_CPT { */ protected $creating = false; - /** - * Map table columns to related postmeta keys. - * - * @var array - */ - protected $postmeta_mapping = array( - 'order_key' => '_order_key', - 'customer_id' => '_customer_user', - 'payment_method' => '_payment_method', - 'payment_method_title' => '_payment_method_title', - 'transaction_id' => '_transaction_id', - 'customer_ip_address' => '_customer_ip_address', - 'customer_user_agent' => '_customer_user_agent', - 'created_via' => '_created_via', - 'date_completed' => '_date_completed', - 'date_paid' => '_date_paid', - 'cart_hash' => '_cart_hash', - - 'billing_first_name' => '_billing_first_name', - 'billing_last_name' => '_billing_last_name', - 'billing_company' => '_billing_company', - 'billing_address_1' => '_billing_address_1', - 'billing_address_2' => '_billing_address_2', - 'billing_city' => '_billing_city', - 'billing_state' => '_billing_state', - 'billing_postcode' => '_billing_postcode', - 'billing_country' => '_billing_country', - 'billing_email' => '_billing_email', - 'billing_phone' => '_billing_phone', - - 'shipping_first_name' => '_shipping_first_name', - 'shipping_last_name' => '_shipping_last_name', - 'shipping_company' => '_shipping_company', - 'shipping_address_1' => '_shipping_address_1', - 'shipping_address_2' => '_shipping_address_2', - 'shipping_city' => '_shipping_city', - 'shipping_state' => '_shipping_state', - 'shipping_postcode' => '_shipping_postcode', - 'shipping_country' => '_shipping_country', - - 'discount_total' => '_cart_discount', - 'discount_tax' => '_cart_discount_tax', - 'shipping_total' => '_order_shipping', - 'shipping_tax' => '_order_shipping_tax', - 'cart_tax' => '_order_tax', - 'total' => '_order_total', - - 'version' => '_order_version', - 'currency' => '_order_currency', - 'prices_include_tax' => '_prices_include_tax', - ); - public function __construct() { - add_filter( 'woocommerce_order_data_store_cpt_get_orders_query', array( $this, 'filter_database_queries' ), 10, 2 ); + + // When creating a WooCommerce order data store request, filter the MySQL query. + add_filter( 'woocommerce_order_data_store_cpt_get_orders_query', __CLASS__ . '::filter_database_queries', 10, 2 ); } /** @@ -82,8 +32,53 @@ public function __construct() { * * @return array An array of database columns and their corresponding post_meta keys. */ - public function get_postmeta_mapping() { - return $this->postmeta_mapping; + public static function get_postmeta_mapping() { + return array( + 'order_key' => '_order_key', + 'customer_id' => '_customer_user', + 'payment_method' => '_payment_method', + 'payment_method_title' => '_payment_method_title', + 'transaction_id' => '_transaction_id', + 'customer_ip_address' => '_customer_ip_address', + 'customer_user_agent' => '_customer_user_agent', + 'created_via' => '_created_via', + 'date_completed' => '_date_completed', + 'date_paid' => '_date_paid', + 'cart_hash' => '_cart_hash', + + 'billing_first_name' => '_billing_first_name', + 'billing_last_name' => '_billing_last_name', + 'billing_company' => '_billing_company', + 'billing_address_1' => '_billing_address_1', + 'billing_address_2' => '_billing_address_2', + 'billing_city' => '_billing_city', + 'billing_state' => '_billing_state', + 'billing_postcode' => '_billing_postcode', + 'billing_country' => '_billing_country', + 'billing_email' => '_billing_email', + 'billing_phone' => '_billing_phone', + + 'shipping_first_name' => '_shipping_first_name', + 'shipping_last_name' => '_shipping_last_name', + 'shipping_company' => '_shipping_company', + 'shipping_address_1' => '_shipping_address_1', + 'shipping_address_2' => '_shipping_address_2', + 'shipping_city' => '_shipping_city', + 'shipping_state' => '_shipping_state', + 'shipping_postcode' => '_shipping_postcode', + 'shipping_country' => '_shipping_country', + + 'discount_total' => '_cart_discount', + 'discount_tax' => '_cart_discount_tax', + 'shipping_total' => '_order_shipping', + 'shipping_tax' => '_order_shipping_tax', + 'cart_tax' => '_order_tax', + 'total' => '_order_total', + + 'version' => '_order_version', + 'currency' => '_order_currency', + 'prices_include_tax' => '_prices_include_tax', + ); } /** @@ -142,49 +137,6 @@ protected function read_order_data( &$order, $post_object ) { } } - /** - * Determine if any filters are required on the MySQL query and, if so, apply them. - * - * @param array $query_args The arguments to be passed to WP_Query. - * @param array $query_vars The raw query vars passed to build the query. - * - * @return array The potentially-filtered $query_args array. - */ - public function filter_database_queries( $query_args, $query_vars ) { - - // No 'meta_query' node means no changes are needed. - if ( ! isset( $query_args['meta_query'] ) || empty( $query_args['meta_query'] ) ) { - return $query_args; - } - - // Chances are we'll have some modifications. - $query_args['wc_order_meta_query'] = array(); - - /* - * Remove some of the query adjustments that may have been made in the - * WC_Order_Data_Store_CPT::get_wp_query_args() method. - */ - foreach ( $query_args['meta_query'] as $index => $meta_query ) { - if ( isset( $meta_query['customer_ids'] ) || isset( $meta_query['customer_emails'] ) ) { - unset( $query_args['meta_query'][ $index ] ); - add_filter( 'posts_where', __CLASS__ . '::customer_query_where', 10, 2 ); - - } elseif ( $column = array_search( $meta_query['key'], $this->get_postmeta_mapping(), true ) ) { - $query_args['wc_order_meta_query'][] = array_merge( $meta_query, array( - 'key' => $column, - '_old_key' => $meta_query['key'], - ) ); - - } - } - - // Add filters to address specific portions of the query. - add_filter( 'posts_join', __CLASS__ . '::posts_join' ); - add_filter( 'posts_where', __CLASS__ . '::meta_query_where', 100, 2 ); - - return $query_args; - } - /** * Retrieve a single order from the database. * @@ -387,7 +339,7 @@ public function populate_from_meta( &$order, $save = true, $delete = false ) { $this->creating = true; } - foreach ( $this->get_postmeta_mapping() as $column => $meta_key ) { + foreach ( self::get_postmeta_mapping() as $column => $meta_key ) { $meta = get_post_meta( $order->get_id(), $meta_key, true ); if ( empty( $table_data->$column ) && ! empty( $meta ) ) { @@ -407,7 +359,7 @@ public function populate_from_meta( &$order, $save = true, $delete = false ) { } if ( true === $delete ) { - foreach ( $this->get_postmeta_mapping() as $column => $meta_key ) { + foreach ( self::get_postmeta_mapping() as $column => $meta_key ) { delete_post_meta( $order->get_id(), $meta_key ); } } @@ -429,30 +381,87 @@ public function backfill_postmeta( &$order ) { return; } - foreach ( $this->get_postmeta_mapping() as $column => $meta_key ) { + foreach ( self::get_postmeta_mapping() as $column => $meta_key ) { if ( isset( $data->$column ) ) { update_post_meta( $order->get_id(), $meta_key, $data->$column ); } } } + /** + * Determine if any filters are required on the MySQL query and, if so, apply them. + * + * @param array $query_args The arguments to be passed to WP_Query. + * @param array $query_vars The raw query vars passed to build the query. + * + * @return array The potentially-filtered $query_args array. + */ + public static function filter_database_queries( $query_args, $query_vars ) { + $query_args['wc_order_meta_query'] = array(); + $query_args['_wc_has_meta_columns'] = false; + + // Iterate over the meta_query to find special cases. + if ( isset( $query_args['meta_query'] ) ) { + foreach ( $query_args['meta_query'] as $index => $meta_query ) { + + // Customer-related queries get their own add-on for the WHERE query. + if ( isset( $meta_query['customer_ids'] ) || isset( $meta_query['customer_emails'] ) ) { + unset( $query_args['meta_query'][ $index ] ); + add_filter( 'posts_where', __CLASS__ . '::customer_query_where', 10, 2 ); + + // If the key matches a known column, copy the meta query to wc_order_meta_query. + } elseif ( isset( $meta_query['key'] ) && ( $column = array_search( $meta_query['key'], self::get_postmeta_mapping(), true ) ) ) { + $query_args['wc_order_meta_query'][] = array_merge( $meta_query, array( + 'key' => $column, + '_old_key' => $meta_query['key'], + ) ); + + // Let this meta query pass through unaltered. + } else { + $query_args['_wc_has_meta_columns'] = true; + } + } + } + + // Add filters to address specific portions of the query. + add_filter( 'posts_join', __CLASS__ . '::posts_join', 10, 2 ); + add_filter( 'posts_where', __CLASS__ . '::meta_query_where', 100, 2 ); + + return $query_args; + } + /** * Filter the JOIN statement generated by WP_Query. * * @global $wpdb * - * @param string $join The MySQL JOIN statement. + * @param string $join The MySQL JOIN statement. + * @param WP_Query $query The WP_Query object, passed by reference. * * @return string The filtered JOIN statement. */ - public static function posts_join( $join ) { + public static function posts_join( $join, $wp_query ) { global $wpdb; - remove_filter( 'posts_join', __CLASS__ . '::posts_join' ); + /* + * Remove the now-unnecessary INNER JOIN with the post_meta table unless there's some post + * meta that doesn't have a column in the custom table. + * + * @see WP_Meta_Query::get_sql_for_clause() + */ + if ( ! $wp_query->get( '_wc_has_meta_columns', false ) ) { + // Match the post_meta table INNER JOIN, with or without an alias. + $regex = "/\sINNER\sJOIN\s{$wpdb->postmeta}\s+(AS\s[^\s]+)?\s*ON\s\([^\)]+\)/i"; + + $join = preg_replace( $regex, '', $join ); + } $table = wc_custom_order_table()->get_table_name(); $join .= " JOIN {$table} ON ( {$wpdb->posts}.ID = {$table}.order_id ) "; + // Don't necessarily apply this to subsequent posts_join filter callbacks. + remove_filter( 'posts_join', __CLASS__ . '::posts_join', 10, 2 ); + return $join; } @@ -546,11 +555,8 @@ public static function meta_query_where( $where, $wp_query ) { } foreach ( $meta_query as $query ) { - $where = str_replace( - $wpdb->prepare( "( {$wpdb->postmeta}.meta_key = %s AND {$wpdb->postmeta}.meta_value ", $query['_old_key'] ), - "( {$table}.{$query['key']} ", - $where - ); + $regex = $wpdb->prepare( "/\(\s?(\w+\.)?meta_key = %s AND (\w+\.)?meta_value /i", $query['_old_key'] ); + $where = preg_replace( $regex, "( {$table}.{$query['key']} ", $where ); } // Ensure this doesn't affect all subsequent queries. From 7b27060733df67d0f71bdd1078140c334639dfac Mon Sep 17 00:00:00 2001 From: Steve Grunwell Date: Tue, 9 Jan 2018 17:53:45 +0000 Subject: [PATCH 17/52] Use a LEFT JOIN instead of a regular JOIN for the custom orders table --- includes/class-wc-order-data-store-custom-table.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-wc-order-data-store-custom-table.php b/includes/class-wc-order-data-store-custom-table.php index 22a39b0..2090b6c 100644 --- a/includes/class-wc-order-data-store-custom-table.php +++ b/includes/class-wc-order-data-store-custom-table.php @@ -457,7 +457,7 @@ public static function posts_join( $join, $wp_query ) { } $table = wc_custom_order_table()->get_table_name(); - $join .= " JOIN {$table} ON ( {$wpdb->posts}.ID = {$table}.order_id ) "; + $join .= " LEFT JOIN {$table} ON ( {$wpdb->posts}.ID = {$table}.order_id ) "; // Don't necessarily apply this to subsequent posts_join filter callbacks. remove_filter( 'posts_join', __CLASS__ . '::posts_join', 10, 2 ); From eae6f270351e4898a807ebd5072e9ee90fee15c4 Mon Sep 17 00:00:00 2001 From: Steve Grunwell Date: Tue, 9 Jan 2018 18:28:02 +0000 Subject: [PATCH 18/52] While using a numeric database column is better for doing math, the custom order table is designed to replace the postmeta table, which stores values in VARCHAR(255). In order to preserve compatibility with WooCommerce queries, these values will need to be treated as strings for now. --- includes/class-wc-custom-order-table-install.php | 14 +++++++------- .../class-wc-order-data-store-custom-table.php | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/includes/class-wc-custom-order-table-install.php b/includes/class-wc-custom-order-table-install.php index 09c3882..32d973d 100644 --- a/includes/class-wc-custom-order-table-install.php +++ b/includes/class-wc-custom-order-table-install.php @@ -79,15 +79,15 @@ protected static function install_tables() { shipping_country varchar(100) NOT NULL, payment_method varchar(100) NOT NULL, payment_method_title varchar(100) NOT NULL, - discount_total float NOT NULL DEFAULT 0, - discount_tax float NOT NULL DEFAULT 0, - shipping_total float NOT NULL DEFAULT 0, - shipping_tax float NOT NULL DEFAULT 0, - cart_tax float NOT NULL DEFAULT 0, - total float NOT NULL DEFAULT 0, + discount_total varchar(100) NOT NULL DEFAULT 0, + discount_tax varchar(100) NOT NULL DEFAULT 0, + shipping_total varchar(100) NOT NULL DEFAULT 0, + shipping_tax varchar(100) NOT NULL DEFAULT 0, + cart_tax varchar(100) NOT NULL DEFAULT 0, + total varchar(100) NOT NULL DEFAULT 0, version varchar(16) NOT NULL, currency varchar(3) NOT NULL, - prices_include_tax tinyint(1) NOT NULL, + prices_include_tax varchar(3) NOT NULL, transaction_id varchar(200) NOT NULL, customer_ip_address varchar(40) NOT NULL, customer_user_agent varchar(200) NOT NULL, diff --git a/includes/class-wc-order-data-store-custom-table.php b/includes/class-wc-order-data-store-custom-table.php index 2090b6c..d37a57f 100644 --- a/includes/class-wc-order-data-store-custom-table.php +++ b/includes/class-wc-order-data-store-custom-table.php @@ -208,7 +208,7 @@ protected function update_post_meta( &$order ) { 'version' => $order->get_version( 'edit' ), 'currency' => $order->get_currency( 'edit' ), - 'prices_include_tax' => $order->get_prices_include_tax( 'edit' ), + 'prices_include_tax' => wc_bool_to_string( $order->get_prices_include_tax( 'edit' ) ), ); $changes = array(); From 5a172baae4c73a7378add4bfbdd61cbe715b8345 Mon Sep 17 00:00:00 2001 From: Steve Grunwell Date: Tue, 9 Jan 2018 18:28:55 +0000 Subject: [PATCH 19/52] Ensure the custom table is always installed when running the test suite --- tests/bootstrap.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/bootstrap.php b/tests/bootstrap.php index de68c0e..1913214 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -27,6 +27,8 @@ // Manually load the plugin on muplugins_loaded. function _manually_load_plugin() { require dirname( dirname( __FILE__ ) ) . '/wc-custom-order-table.php'; + + WC_Custom_Order_Table_Install::activate(); } tests_add_filter( 'muplugins_loaded', '_manually_load_plugin' ); From 89418f7be4144b1f5b79547d049eee4018d1e91f Mon Sep 17 00:00:00 2001 From: Steve Grunwell Date: Tue, 9 Jan 2018 19:38:07 +0000 Subject: [PATCH 20/52] Use the same WHERE clause rewriting for customer queries --- ...class-wc-order-data-store-custom-table.php | 93 ++++--------------- 1 file changed, 19 insertions(+), 74 deletions(-) diff --git a/includes/class-wc-order-data-store-custom-table.php b/includes/class-wc-order-data-store-custom-table.php index d37a57f..a1df6cd 100644 --- a/includes/class-wc-order-data-store-custom-table.php +++ b/includes/class-wc-order-data-store-custom-table.php @@ -404,13 +404,26 @@ public static function filter_database_queries( $query_args, $query_vars ) { if ( isset( $query_args['meta_query'] ) ) { foreach ( $query_args['meta_query'] as $index => $meta_query ) { - // Customer-related queries get their own add-on for the WHERE query. - if ( isset( $meta_query['customer_ids'] ) || isset( $meta_query['customer_emails'] ) ) { - unset( $query_args['meta_query'][ $index ] ); - add_filter( 'posts_where', __CLASS__ . '::customer_query_where', 10, 2 ); + // Flatten complex meta queries. + if ( is_array( $meta_query ) && 1 === count( $meta_query ) && is_array( current( $meta_query ) ) ) { + $meta_query = current( $meta_query ); + } + + if ( isset( $meta_query['customer_emails'] ) ) { + $query_args['wc_order_meta_query'][] = array_merge( $meta_query['customer_emails'], array( + 'key' => 'billing_email', + '_old_key' => $meta_query['customer_emails']['key'], + ) ); + } - // If the key matches a known column, copy the meta query to wc_order_meta_query. - } elseif ( isset( $meta_query['key'] ) && ( $column = array_search( $meta_query['key'], self::get_postmeta_mapping(), true ) ) ) { + if ( isset( $meta_query['customer_ids'] ) ) { + $query_args['wc_order_meta_query'][] = array_merge( $meta_query['customer_ids'], array( + 'key' => 'customer_id', + '_old_key' => $meta_query['customer_ids']['key'], + ) ); + } + + if ( isset( $meta_query['key'] ) && ( $column = array_search( $meta_query['key'], self::get_postmeta_mapping(), true ) ) ) { $query_args['wc_order_meta_query'][] = array_merge( $meta_query, array( 'key' => $column, '_old_key' => $meta_query['key'], @@ -465,74 +478,6 @@ public static function posts_join( $join, $wp_query ) { return $join; } - /** - * Given 'customer' arguments, build a corresponding WHERE query fragment. - * - * @global $wpdb - * - * @param string $where The MySQL WHERE statement. - * @param WP_Query $query The WP_Query object, passed by reference. - * - * @return string The [potentially-] filtered JOIN statement. - */ - public static function customer_query_where( $where, $wp_query ) { - global $wpdb; - - $customer = (array) $wp_query->get( 'customer' ); - $table = wc_custom_order_table()->get_table_name(); - $query_parts = array( - 'ids' => array(), - 'emails' => array(), - ); - $id_query = ''; - $email_query = ''; - - // Return early if there's no 'customer' parameter. - if ( ! $customer ) { - return $where; - } - - foreach ( $customer as $term ) { - if ( is_email( $term ) ) { - $query_parts['emails'][] = $term; - } else { - $query_parts['ids'][] = (int) $term; - } - } - - // Return early if we didn't get anything out of the loop. - if ( empty( $query_parts['ids'] ) && empty( $query_parts['emails'] ) ) { - return $where; - } - - // Build the ID portion of the query. - if ( ! empty( $query_parts['ids'] ) ) { - $id_query = $wpdb->prepare( - "{$table}.customer_id IN (" . implode( ', ', array_fill( 0, count( $query_parts['ids'] ), '%d' ) ) . ')', - $query_parts['ids'] - ); - } - - // Build the email portion of the query. - if ( ! empty( $query_parts['emails'] ) ) { - $email_query = $wpdb->prepare( - "{$table}.billing_email IN (" . implode( ', ', array_fill( 0, count( $query_parts['emails'] ), '%s' ) ) . ')', - $query_parts['emails'] - ); - } - - if ( $id_query && $email_query ) { - $where .= " AND ( $id_query OR $email_query ) "; - } else { - $where .= " AND ( $id_query $email_query )"; - } - - // Ensure this doesn't affect all subsequent queries. - remove_filter( 'posts_where', __CLASS__ . '::customer_query_where', 10, 2 ); - - return $where; - } - /** * Filter the "WHERE" portion of the MySQL query to look at the custom orders table instead of * post meta. From ed8ca1a99a63fe0958ea56d8025488ece2d66249 Mon Sep 17 00:00:00 2001 From: Steve Grunwell Date: Tue, 9 Jan 2018 19:38:24 +0000 Subject: [PATCH 21/52] Prevent the test environment from enqueueing email actions --- tests/bootstrap.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 1913214..027a048 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -29,6 +29,8 @@ function _manually_load_plugin() { require dirname( dirname( __FILE__ ) ) . '/wc-custom-order-table.php'; WC_Custom_Order_Table_Install::activate(); + + add_filter( 'woocommerce_email_actions', '__return_empty_array' ); } tests_add_filter( 'muplugins_loaded', '_manually_load_plugin' ); From b66f7673d29310f411845e233fa73a603ae6b8e5 Mon Sep 17 00:00:00 2001 From: Steve Grunwell Date: Tue, 9 Jan 2018 21:31:09 +0000 Subject: [PATCH 22/52] Extend the data store used for Customers as well, as the stock version has hard-coded queries against the postmeta table. --- includes/class-wc-custom-order-table.php | 10 + ...ss-wc-customer-data-store-custom-table.php | 189 ++++++++++++++++++ 2 files changed, 199 insertions(+) create mode 100644 includes/class-wc-customer-data-store-custom-table.php diff --git a/includes/class-wc-custom-order-table.php b/includes/class-wc-custom-order-table.php index d5acd9c..d8f9985 100644 --- a/includes/class-wc-custom-order-table.php +++ b/includes/class-wc-custom-order-table.php @@ -29,6 +29,7 @@ public function setup() { $this->table_name = $wpdb->prefix . 'woocommerce_orders'; // Inject the plugin into order processing. + add_filter( 'woocommerce_customer_data_store', array( $this, 'customer_data_store' ) ); add_filter( 'woocommerce_order_data_store', array( $this, 'order_data_store' ) ); // Register the CLI command if we're running WP_CLI. @@ -51,6 +52,15 @@ public function get_table_name() { return apply_filters( 'wc_customer_order_table_name', $this->table_name ); } + /** + * Retrieve the class name of the WooCommerce customer data store. + * + * @return string The data store class name. + */ + public function customer_data_store() { + return 'WC_Customer_Data_Store_Custom_Table'; + } + /** * Retrieve the class name of the WooCommerce order data store. * diff --git a/includes/class-wc-customer-data-store-custom-table.php b/includes/class-wc-customer-data-store-custom-table.php new file mode 100644 index 0000000..b3af89e --- /dev/null +++ b/includes/class-wc-customer-data-store-custom-table.php @@ -0,0 +1,189 @@ +get_table_name(); + $last_order = $wpdb->get_var( "SELECT posts.ID + FROM $wpdb->posts AS posts + LEFT JOIN $table AS meta on posts.ID = meta.order_id + WHERE meta.customer_id = '" . esc_sql( $customer->get_id() ) . "' + AND posts.post_type = 'shop_order' + AND posts.post_status IN ( '" . implode( "','", array_map( 'esc_sql', array_keys( wc_get_order_statuses() ) ) ) . "' ) + ORDER BY posts.ID DESC + " ); + + return $last_order ? wc_get_order( (int) $last_order ) : false; + } + + /** + * Return the number of orders this customer has. + * + * @global $wpdb + * + * @param WC_Customer $customer The WC_Customer object, passed by reference. + * + * @return int The number of orders for this customer. + */ + public function get_order_count( &$customer ) { + global $wpdb; + + $count = get_user_meta( $customer->get_id(), '_order_count', true ); + + if ( '' === $count ) { + $table = wc_custom_order_table()->get_table_name(); + $count = $wpdb->get_var( "SELECT COUNT(*) + FROM $wpdb->posts as posts + LEFT JOIN $table AS meta ON posts.ID = meta.order_id + WHERE meta.customer_id = '" . esc_sql( $customer->get_id() ) . "' + AND posts.post_type = 'shop_order' + AND posts.post_status IN ( '" . implode( "','", array_map( 'esc_sql', array_keys( wc_get_order_statuses() ) ) ) . "' ) + " ); + update_user_meta( $customer->get_id(), '_order_count', $count ); + } + + return (int) $count; + } + + /** + * Return how much money this customer has spent. + * + * @global $wpdb + * + * @param WC_Customer $customer The WC_Customer object, passed by reference. + * + * @return float The total amount spent by the customer. + */ + public function get_total_spent( &$customer ) { + global $wpdb; + + $spent = apply_filters( 'woocommerce_customer_get_total_spent', get_user_meta( $customer->get_id(), '_money_spent', true ), $customer ); + + if ( '' === $spent ) { + $table = wc_custom_order_table()->get_table_name(); + $statuses = array_map( 'esc_sql', wc_get_is_paid_statuses() ); + $spent = $wpdb->get_var( apply_filters( 'woocommerce_customer_get_total_spent_query', "SELECT SUM(meta.total) + FROM $wpdb->posts as posts + LEFT JOIN {$table} AS meta ON posts.ID = meta.order_id + WHERE meta.customer_id = '" . esc_sql( $customer->get_id() ) . "' + AND posts.post_type = 'shop_order' + AND posts.post_status IN ( 'wc-" . implode( "','wc-", $statuses ) . "' ) + ", $customer ) ); + + if ( ! $spent ) { + $spent = 0; + } + update_user_meta( $customer->get_id(), '_money_spent', $spent ); + } + + return wc_format_decimal( $spent, 2 ); + } + + /** + * Short-circuit the wc_customer_bought_product() function. + * + * @see wc_customer_bought_product() + * + * @global $wpdb + * + * @param bool $purchased Whether or not the customer has purchased the product. + * @param string $customer_email Customer email to check. + * @param int $user_id User ID to check. + * @param int $product_id Product ID to check. + * + * @return bool Whether or not the customer has already purchased the given product ID. + */ + public static function pre_customer_bought_product( $purchased, $customer_email, $user_id, $product_id ) { + global $wpdb; + + $transient_name = 'wc_cbp_' . md5( $customer_email . $user_id . WC_Cache_Helper::get_transient_version( 'orders' ) ); + + if ( false === ( $result = get_transient( $transient_name ) ) ) { + $customer_data = array( $user_id ); + + if ( $user_id ) { + $user = get_user_by( 'id', $user_id ); + + if ( isset( $user->user_email ) ) { + $customer_data[] = $user->user_email; + } + } + + if ( is_email( $customer_email ) ) { + $customer_data[] = $customer_email; + } + + $customer_data = array_map( 'esc_sql', array_filter( array_unique( $customer_data ) ) ); + $statuses = array_map( 'esc_sql', wc_get_is_paid_statuses() ); + + if ( sizeof( $customer_data ) == 0 ) { + return false; + } + + $table = wc_custom_order_table()->get_table_name(); + $result = $wpdb->get_col( " + SELECT im.meta_value FROM {$wpdb->posts} AS p + INNER JOIN {$table} AS pm ON p.ID = pm.order_id + INNER JOIN {$wpdb->prefix}woocommerce_order_items AS i ON p.ID = i.order_id + INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS im ON i.order_item_id = im.order_item_id + WHERE p.post_status IN ( 'wc-" . implode( "','wc-", $statuses ) . "' ) + AND ( + pm.billing_email IN ( '" . implode( "','", $customer_data ) . "' ) + OR pm.customer_id IN ( '" . implode( "','", $customer_data ) . "' ) + ) + AND im.meta_key IN ( '_product_id', '_variation_id' ) + AND im.meta_value != 0 + " ); + $result = array_map( 'absint', $result ); + + set_transient( $transient_name, $result, DAY_IN_SECONDS * 30 ); + } + + return in_array( (int) $product_id, $result ); + } + + /** + * Reset customer_id on orders when a user is deleted. + * + * @param int $user_id + */ + public static function reset_order_customer_id_on_deleted_user( $user_id ) { + global $wpdb; + + $wpdb->update( + wc_custom_order_table()->get_table_name(), + array( 'customer_id' => 0 ), + array( 'customer_id' => $user_id ) + ); + } +} From 0511f792485bdcd6455617aaf5fb066025feabe6 Mon Sep 17 00:00:00 2001 From: Steve Grunwell Date: Tue, 9 Jan 2018 22:10:52 +0000 Subject: [PATCH 23/52] Ensure the woocommerce_orders table is truncated after each test --- tests/testcase.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/testcase.php b/tests/testcase.php index 67e1679..afeed59 100644 --- a/tests/testcase.php +++ b/tests/testcase.php @@ -8,6 +8,19 @@ class TestCase extends WC_Unit_Test_Case { + /** + * Delete all data from the orders table after each test. + * + * @after + * + * @global $wpdb + */ + function truncate_table() { + global $wpdb; + + $wpdb->query( "DELETE FROM {$wpdb->prefix}woocommerce_orders" ); + } + /** * Retrieve the core test suite's factory object, but add extra factories. * From d3dbfc5cced8ae370c5ef55f3f27f2b63109bbe8 Mon Sep 17 00:00:00 2001 From: Steve Grunwell Date: Tue, 9 Jan 2018 22:30:07 +0000 Subject: [PATCH 24/52] The cart_tax and total_tax properties are different within WooCommerce, the latter being cart_tax + shipping_tax, per WC_Abstract_Order::set_cart_tax() --- includes/class-wc-order-data-store-custom-table.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-wc-order-data-store-custom-table.php b/includes/class-wc-order-data-store-custom-table.php index a1df6cd..a89d02e 100644 --- a/includes/class-wc-order-data-store-custom-table.php +++ b/includes/class-wc-order-data-store-custom-table.php @@ -203,7 +203,7 @@ protected function update_post_meta( &$order ) { 'discount_tax' => $order->get_discount_tax( 'edit' ), 'shipping_total' => $order->get_shipping_total( 'edit' ), 'shipping_tax' => $order->get_shipping_tax( 'edit' ), - 'cart_tax' => $order->get_total_tax( 'edit' ), + 'cart_tax' => $order->get_cart_tax( 'edit' ), 'total' => $order->get_total( 'edit' ), 'version' => $order->get_version( 'edit' ), From 6866839d4266be51c77c3c5ba28529c00822c550 Mon Sep 17 00:00:00 2001 From: Steve Grunwell Date: Wed, 10 Jan 2018 14:31:06 +0000 Subject: [PATCH 25/52] WooCommerce saves date_completed and date_paid as Unix timestamps, not as YYYY-MM-DD HH:MM:SS datetimes. To remain compatible, we should treat these database columns the same way until such point that we have tests and can reliably convert them to a more proper column type. --- .../class-wc-custom-order-table-install.php | 4 ++-- ...class-wc-order-data-store-custom-table.php | 11 +++++++++-- tests/test-data-store.php | 19 ------------------- 3 files changed, 11 insertions(+), 23 deletions(-) diff --git a/includes/class-wc-custom-order-table-install.php b/includes/class-wc-custom-order-table-install.php index 32d973d..a98578f 100644 --- a/includes/class-wc-custom-order-table-install.php +++ b/includes/class-wc-custom-order-table-install.php @@ -92,8 +92,8 @@ protected static function install_tables() { customer_ip_address varchar(40) NOT NULL, customer_user_agent varchar(200) NOT NULL, created_via varchar(200) NOT NULL, - date_completed datetime DEFAULT NULL, - date_paid datetime DEFAULT NULL, + date_completed varchar(20) DEFAULT NULL, + date_paid varchar(20) DEFAULT NULL, cart_hash varchar(32) NOT NULL, PRIMARY KEY (order_id), UNIQUE KEY `order_key` (`order_key`), diff --git a/includes/class-wc-order-data-store-custom-table.php b/includes/class-wc-order-data-store-custom-table.php index a89d02e..ac14449 100644 --- a/includes/class-wc-order-data-store-custom-table.php +++ b/includes/class-wc-order-data-store-custom-table.php @@ -172,8 +172,8 @@ protected function update_post_meta( &$order ) { 'customer_ip_address' => $order->get_customer_ip_address( 'edit' ), 'customer_user_agent' => $order->get_customer_user_agent( 'edit' ), 'created_via' => $order->get_created_via( 'edit' ), - 'date_completed' => (string) $order->get_date_completed( 'edit' ), - 'date_paid' => (string) $order->get_date_paid( 'edit' ), + 'date_completed' => $order->get_date_completed( 'edit' ), + 'date_paid' => $order->get_date_paid( 'edit' ), 'cart_hash' => $order->get_cart_hash( 'edit' ), 'billing_first_name' => $order->get_billing_first_name( 'edit' ), @@ -211,6 +211,13 @@ protected function update_post_meta( &$order ) { 'prices_include_tax' => wc_bool_to_string( $order->get_prices_include_tax( 'edit' ) ), ); + // Convert dates to timestamps, if they exist. + foreach ( array( 'date_completed', 'date_paid' ) as $date ) { + if ( $edit_data[ $date ] instanceof WC_DateTime ) { + $edit_data[ $date ] = $edit_data[ $date ]->getTimestamp(); + } + } + $changes = array(); if ( $this->creating ) { diff --git a/tests/test-data-store.php b/tests/test-data-store.php index 2062d03..3bffd89 100644 --- a/tests/test-data-store.php +++ b/tests/test-data-store.php @@ -86,25 +86,6 @@ public function test_update_post_meta_for_new_order() { $this->assertEquals( 'PHPUnit', $row['customer_user_agent'] ); } - /** - * When inserting rows into the database, $wpdb->prepare() can't accept WC_DateTime objects. - */ - public function test_update_post_meta_casts_dates_as_strings() { - $order = new WC_Order( wp_insert_post( array( - 'post_type' => 'product', - ) ) ); - $time = time(); - $order->set_date_paid( $time ); - $order->set_date_completed( $time ); - - $this->invoke_update_post_meta( $order, true ); - - $row = $this->get_order_row( $order->get_id() ); - - $this->assertEquals( $time, strtotime( $row['date_paid'] ) ); - $this->assertEquals( $time, strtotime( $row['date_completed'] ) ); - } - public function test_get_order_id_by_order_key() { $order = WC_Helper_Order::create_order(); $instance = new WC_Order_Data_Store_Custom_Table(); From dd1315f02911b7c40077bf352c62f7ab9837f2e0 Mon Sep 17 00:00:00 2001 From: Steve Grunwell Date: Wed, 10 Jan 2018 14:49:47 +0000 Subject: [PATCH 26/52] Clean up the DataStoreTest class to remove some leakage on the 'woocommerce_shop_order_search_fields' filter --- tests/test-data-store.php | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/tests/test-data-store.php b/tests/test-data-store.php index 3bffd89..19101db 100644 --- a/tests/test-data-store.php +++ b/tests/test-data-store.php @@ -8,24 +8,6 @@ class DataStoreTest extends TestCase { - /** - * Fire the necessary actions to bootstrap WordPress. - * - * @before - */ - public function init() { - do_action( 'init' ); - } - - /** - * Remove any closures that have been assigned to filters. - * - * @after - */ - public function remove_filter_callbacks() { - remove_all_filters( 'woocommerce_shop_order_search_fields' ); - } - public function test_create() { $instance = new WC_Order_Data_Store_Custom_Table(); $property = new ReflectionProperty( $instance, 'creating' ); @@ -111,6 +93,7 @@ public function test_search_orders_can_check_post_meta() { add_post_meta( $order, 'some_custom_meta_key', $term ); add_filter( 'woocommerce_shop_order_search_fields', function () { + remove_filter( 'woocommerce_shop_order_search_fields', __FUNCTION__ ); return array( 'some_custom_meta_key' ); } ); From a713df48a79b8f25332b98e9af5d5fd52e4bd748 Mon Sep 17 00:00:00 2001 From: Steve Grunwell Date: Wed, 10 Jan 2018 17:59:17 +0000 Subject: [PATCH 27/52] Add two new table columns: billing_index and shipping_index. These map to the _billing_address_index and _shipping_address_index meta keys, respectively, and contain space-delimited contents of the billing/shipping fields. Recent versions of WooCommerce do this to allow a single LIKE query rather than explicitly querying each and every column. --- .../class-wc-custom-order-table-install.php | 2 + ...class-wc-order-data-store-custom-table.php | 50 +++++++++++-------- 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/includes/class-wc-custom-order-table-install.php b/includes/class-wc-custom-order-table-install.php index a98578f..03d37c9 100644 --- a/includes/class-wc-custom-order-table-install.php +++ b/includes/class-wc-custom-order-table-install.php @@ -57,6 +57,7 @@ protected static function install_tables() { order_id BIGINT UNSIGNED NOT NULL, order_key varchar(100) NOT NULL, customer_id BIGINT UNSIGNED NOT NULL, + billing_index varchar(255) NOT NULL, billing_first_name varchar(100) NOT NULL, billing_last_name varchar(100) NOT NULL, billing_company varchar(100) NOT NULL, @@ -68,6 +69,7 @@ protected static function install_tables() { billing_country varchar(100) NOT NULL, billing_email varchar(200) NOT NULL, billing_phone varchar(200) NOT NULL, + shipping_index varchar(255) NOT NULL, shipping_first_name varchar(100) NOT NULL, shipping_last_name varchar(100) NOT NULL, shipping_company varchar(100) NOT NULL, diff --git a/includes/class-wc-order-data-store-custom-table.php b/includes/class-wc-order-data-store-custom-table.php index ac14449..439fde4 100644 --- a/includes/class-wc-order-data-store-custom-table.php +++ b/includes/class-wc-order-data-store-custom-table.php @@ -46,6 +46,7 @@ public static function get_postmeta_mapping() { 'date_paid' => '_date_paid', 'cart_hash' => '_cart_hash', + 'billing_index' => '_billing_address_index', 'billing_first_name' => '_billing_first_name', 'billing_last_name' => '_billing_last_name', 'billing_company' => '_billing_company', @@ -58,6 +59,7 @@ public static function get_postmeta_mapping() { 'billing_email' => '_billing_email', 'billing_phone' => '_billing_phone', + 'shipping_index' => '_shipping_address_index', 'shipping_first_name' => '_shipping_first_name', 'shipping_last_name' => '_shipping_last_name', 'shipping_company' => '_shipping_company', @@ -163,7 +165,10 @@ public function get_order_data_from_table( $order ) { protected function update_post_meta( &$order ) { global $wpdb; - $edit_data = array( + $table = wc_custom_order_table()->get_table_name(); + $changes = array(); + $order_data = array( + 'order_id' => $order->get_id( 'edit' ), 'order_key' => $order->get_order_key( 'edit' ), 'customer_id' => $order->get_customer_id( 'edit' ), 'payment_method' => $order->get_payment_method( 'edit' ), @@ -176,6 +181,7 @@ protected function update_post_meta( &$order ) { 'date_paid' => $order->get_date_paid( 'edit' ), 'cart_hash' => $order->get_cart_hash( 'edit' ), + 'billing_index' => implode( ' ', $order->get_address( 'billing' ) ), 'billing_first_name' => $order->get_billing_first_name( 'edit' ), 'billing_last_name' => $order->get_billing_last_name( 'edit' ), 'billing_company' => $order->get_billing_company( 'edit' ), @@ -189,6 +195,7 @@ protected function update_post_meta( &$order ) { 'billing_email' => $order->get_billing_email( 'edit' ), 'billing_phone' => $order->get_billing_phone( 'edit' ), + 'shipping_index' => implode( ' ', $order->get_address( 'shipping' ) ), 'shipping_first_name' => $order->get_shipping_first_name( 'edit' ), 'shipping_last_name' => $order->get_shipping_last_name( 'edit' ), 'shipping_company' => $order->get_shipping_company( 'edit' ), @@ -213,34 +220,35 @@ protected function update_post_meta( &$order ) { // Convert dates to timestamps, if they exist. foreach ( array( 'date_completed', 'date_paid' ) as $date ) { - if ( $edit_data[ $date ] instanceof WC_DateTime ) { - $edit_data[ $date ] = $edit_data[ $date ]->getTimestamp(); + if ( $order_data[ $date ] instanceof WC_DateTime ) { + $order_data[ $date ] = $order_data[ $date ]->getTimestamp(); } } - $changes = array(); - + // Insert or update the database record. if ( $this->creating ) { - $wpdb->insert( - wc_custom_order_table()->get_table_name(), - array_merge( array( - 'order_id' => $order->get_id(), - ), $edit_data ) - ); - - // We are no longer creating the order, it is created. + $wpdb->insert( $table, $order_data ); + $this->creating = false; + } else { - $changes = array_intersect_key( $edit_data, $order->get_changes() ); + $changes = array_intersect_key( $order_data, $order->get_changes() ); + + /* + * WC_Order::get_changes() will mark all address fields as changed if one has changed. + * + * If any of these fields are present, be sure we update the index column. + */ + if ( isset( $changes['billing_first_name'] ) ) { + $changes['billing_index'] = $order_data['billing_index']; + } + + if ( isset( $changes['shipping_first_name'] ) ) { + $changes['shipping_index'] = $order_data['shipping_index']; + } if ( ! empty( $changes ) ) { - $wpdb->update( - "{$wpdb->prefix}woocommerce_orders", - $changes, - array( - 'order_id' => $order->get_id(), - ) - ); + $wpdb->update( $table, $changes, array( 'order_id' => $order->get_id() ) ); } } From 2a96d543b19dc7f16aea83a6985da67b46b6e938 Mon Sep 17 00:00:00 2001 From: Steve Grunwell Date: Wed, 10 Jan 2018 18:02:12 +0000 Subject: [PATCH 28/52] Rewrite search_orders() to search all of the following: - Exact order IDs - The custom orders table (billing/shipping index, billing_last_name, and billing_email by default) - Post meta (if the 'woocommerce_shop_order_search_fields' filter returns any fields that don't match to columns in the order table) - Order item titles --- ...class-wc-order-data-store-custom-table.php | 98 ++++++++++++------- 1 file changed, 63 insertions(+), 35 deletions(-) diff --git a/includes/class-wc-order-data-store-custom-table.php b/includes/class-wc-order-data-store-custom-table.php index 439fde4..4e0c77d 100644 --- a/includes/class-wc-order-data-store-custom-table.php +++ b/includes/class-wc-order-data-store-custom-table.php @@ -289,50 +289,78 @@ public function get_order_id_by_order_key( $order_key ) { public function search_orders( $term ) { global $wpdb; - $order_ids = array(); + /** + * Searches on meta data can be slow - this lets you choose what fields to search. + * 3.0.0 added _billing_address and _shipping_address meta which contains all address data to make this faster. + * This however won't work on older orders unless updated, so search a few others (expand this using the filter if needed). + * + * @var array + */ + $search_fields = array_map( + 'wc_clean', apply_filters( + 'woocommerce_shop_order_search_fields', array( + '_billing_address_index', + '_shipping_address_index', + '_billing_last_name', + '_billing_email', + ) + ) + ); + $term = wc_clean( $term ); + $order_ids = array(); // Treat a numeric search term as an order ID. if ( is_numeric( $term ) ) { $order_ids[] = absint( $term ); } - // Search given post meta columns for the query. - $postmeta_search = array(); + // Search for order meta fields. + if ( ! empty( $search_fields ) ) { + $mapping = self::get_postmeta_mapping(); + $in_table = array_intersect( $search_fields, $mapping ); + $meta_keys = array_diff( $search_fields, $in_table ); + $table = wc_custom_order_table()->get_table_name(); - /** - * Searches on meta data can be slow - this lets you choose what fields to search. - * - * WooCommerce 2.7.0 added _billing_address and _shipping_address meta which contains all - * address data to make this faster. However, this won't work on older orders unless they - * are updated, so search a few others (expand this using the filter if needed). - */ - $meta_search_fields = array_map( 'wc_clean', apply_filters( 'woocommerce_shop_order_search_fields', array() ) ); - - // If we were given meta fields to search, make it happen. - if ( ! empty( $meta_search_fields ) ) { - $postmeta_search = $wpdb->get_col( $wpdb->prepare( " - SELECT DISTINCT post_id - FROM {$wpdb->postmeta} - WHERE meta_key IN (" . implode( ',', array_fill( 0, count( $meta_search_fields ), '%s' ) ) . ') - AND meta_value LIKE %s - ', - array_merge( $meta_search_fields, array( '%' . $wpdb->esc_like( $term ) . '%' ) ) - ) ); + // Find results based on search fields that map to table columns. + if ( ! empty( $in_table ) ) { + $columns = array_keys( array_intersect( $mapping, $in_table ) ); + $where = array(); + + foreach ( $columns as $column ) { + $where[] = "{$column} LIKE %s"; + } + + $order_ids = array_merge( $order_ids, $wpdb->get_col( $wpdb->prepare( " + SELECT DISTINCT order_id FROM {$table} WHERE " . implode( ' OR ', $where ), + array_fill( 0, count( $where ), '%' . $wpdb->esc_like( $term ) . '%' ) + ) ) ); + } + + // For anything else, fall back to postmeta. + if ( ! empty( $meta_keys ) ) { + $order_ids = array_merge( $order_ids, $wpdb->get_col( $wpdb->prepare( " + SELECT DISTINCT post_id FROM {$wpdb->postmeta} + WHERE meta_value LIKE %s + AND meta_key IN (" . implode( ',', array_fill( 0, count( $meta_keys ), '%s' ) ) . ')', + array_merge( + array( '%' . $wpdb->esc_like( $term ) . '%' ), + $meta_keys + ) + ) ) ); + } } - return array_unique( array_merge( - $order_ids, - $postmeta_search, - $wpdb->get_col( - $wpdb->prepare( " - SELECT order_id - FROM {$wpdb->prefix}woocommerce_order_items as order_items - WHERE order_item_name LIKE %s - ", - '%' . $wpdb->esc_like( $term ) . '%' - ) - ) - ) ); + // Search item names. + $order_ids = array_merge( $order_ids, $wpdb->get_col( $wpdb->prepare( " + SELECT order_id FROM {$wpdb->prefix}woocommerce_order_items + WHERE order_item_name LIKE %s", + '%' . $wpdb->esc_like( $term ) . '%' + ) ) ); + + // Reduce the array of order IDs to unique values. + $order_ids = array_unique( $order_ids ); + + return apply_filters( 'woocommerce_shop_order_search_results', $order_ids, $term, $search_fields ); } /** From 09bcff678ebbf4bb9863b6e5854eb69c5a213fe4 Mon Sep 17 00:00:00 2001 From: Steve Grunwell Date: Wed, 10 Jan 2018 18:55:03 +0000 Subject: [PATCH 29/52] Use the liquidweb/woocommerce fork as the dev dependency. The fix/get-customer-last-order branch has been submitted as a PR to WooCommerce (https://github.com/woocommerce/woocommerce/pull/18417); once merged, we'll be able to revert to using WooCommerce's dev-master for in composer.json. --- composer.json | 8 +++++++- composer.lock | 41 +++++++++++++++++++++++++++++++++-------- 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/composer.json b/composer.json index 42d7f99..839fc04 100644 --- a/composer.json +++ b/composer.json @@ -3,13 +3,19 @@ "description": "Store WooCommerce order data in a custom table.", "type": "wordpress-plugin", "license": "GPL-2.0", + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/liquidweb/woocommerce" + } + ], "require": { "php": ">=5.2", "composer/installers": "^1.4", "xrstf/composer-php52": "^1.0" }, "require-dev": { - "woocommerce/woocommerce": "dev-master" + "woocommerce/woocommerce": "dev-fix/get-customer-last-order" }, "autoload": { "classmap": [ diff --git a/composer.lock b/composer.lock index 0ee0683..00a37b3 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "fe30de23a7822b2387c810e945f9be02", + "content-hash": "144174375d25fca49925f1402b0cca31", "packages": [ { "name": "composer/installers", @@ -161,16 +161,16 @@ "packages-dev": [ { "name": "woocommerce/woocommerce", - "version": "dev-master", + "version": "dev-fix/get-customer-last-order", "source": { "type": "git", - "url": "https://github.com/woocommerce/woocommerce.git", - "reference": "aad66effb7ae796321348664b47117fd6fcb60f2" + "url": "https://github.com/liquidweb/woocommerce.git", + "reference": "493298504d00ddb7bbd7c32475657ad4e866120c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/woocommerce/woocommerce/zipball/aad66effb7ae796321348664b47117fd6fcb60f2", - "reference": "aad66effb7ae796321348664b47117fd6fcb60f2", + "url": "https://api.github.com/repos/liquidweb/woocommerce/zipball/493298504d00ddb7bbd7c32475657ad4e866120c", + "reference": "493298504d00ddb7bbd7c32475657ad4e866120c", "shasum": "" }, "require": { @@ -193,13 +193,38 @@ "phpcbf": "Fix coding standards warnings/errors automatically with PHP Code Beautifier" } }, - "notification-url": "https://packagist.org/downloads/", + "scripts": { + "pre-update-cmd": [ + "WooCommerce\\GitHooks\\Hooks::preHooks" + ], + "pre-install-cmd": [ + "WooCommerce\\GitHooks\\Hooks::preHooks" + ], + "post-install-cmd": [ + "WooCommerce\\GitHooks\\Hooks::postHooks" + ], + "post-update-cmd": [ + "WooCommerce\\GitHooks\\Hooks::postHooks" + ], + "test": [ + "phpunit" + ], + "phpcs": [ + "phpcs -s -p" + ], + "phpcbf": [ + "phpcbf -p" + ] + }, "license": [ "GPL-2.0+" ], "description": "An eCommerce toolkit that helps you sell anything. Beautifully.", "homepage": "https://woocommerce.com/", - "time": "2018-01-05T20:03:12+00:00" + "support": { + "source": "https://github.com/liquidweb/woocommerce/tree/fix/get-customer-last-order" + }, + "time": "2018-01-10T18:39:35+00:00" } ], "aliases": [], From 7e8eff7fc2930292c3ab230a35404f2c5fcf9499 Mon Sep 17 00:00:00 2001 From: Steve Grunwell Date: Wed, 10 Jan 2018 22:11:04 +0000 Subject: [PATCH 30/52] Ensure get_order_data_from_table() uses the get_table_name() helper --- includes/class-wc-order-data-store-custom-table.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/includes/class-wc-order-data-store-custom-table.php b/includes/class-wc-order-data-store-custom-table.php index 4e0c77d..951804b 100644 --- a/includes/class-wc-order-data-store-custom-table.php +++ b/includes/class-wc-order-data-store-custom-table.php @@ -151,7 +151,12 @@ protected function read_order_data( &$order, $post_object ) { public function get_order_data_from_table( $order ) { global $wpdb; - return $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}woocommerce_orders WHERE order_id = %d;", $order->get_id() ) ); + $table = wc_custom_order_table()->get_table_name(); + + return $wpdb->get_row( $wpdb->prepare( + "SELECT * FROM {$table} WHERE order_id = %d", + $order->get_id() + ) ); } /** From dcb685d9cf23f44ab3e3e6c7a0339cb4e87cb09c Mon Sep 17 00:00:00 2001 From: Steve Grunwell Date: Thu, 11 Jan 2018 01:29:38 +0000 Subject: [PATCH 31/52] WooCommerce *stores* prices_include_tax as a 'yes' or 'no', but the handling of it within WooCommerce is as a Boolean. As such, update_post_meta() should convert it to a string, while get_order_data_from_table() should coerce it back into a Boolean value. This commit represents several hours of the ugliest debugging you've ever seen, crawling through the deepest, darkest corners of the WooCommerce codebase. It also represents the last failing test from the WooCommerce core test suite. --- includes/class-wc-order-data-store-custom-table.php | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/includes/class-wc-order-data-store-custom-table.php b/includes/class-wc-order-data-store-custom-table.php index 951804b..ccb30f8 100644 --- a/includes/class-wc-order-data-store-custom-table.php +++ b/includes/class-wc-order-data-store-custom-table.php @@ -124,8 +124,6 @@ public function delete( &$order, $args = array() ) { protected function read_order_data( &$order, $post_object ) { global $wpdb; - parent::read_order_data( $order, $post_object ); - $data = $this->get_order_data_from_table( $order ); if ( ! empty( $data ) ) { @@ -152,11 +150,15 @@ public function get_order_data_from_table( $order ) { global $wpdb; $table = wc_custom_order_table()->get_table_name(); - - return $wpdb->get_row( $wpdb->prepare( + $data = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$table} WHERE order_id = %d", $order->get_id() - ) ); + ), ARRAY_A ); + + // Expand anything that might need assistance. + $data['prices_include_tax'] = wc_string_to_bool( $data['prices_include_tax'] ); + + return $data; } /** From e869bc0a335ceb868ced4ca33c2b2d8f1e7eab2e Mon Sep 17 00:00:00 2001 From: Steve Grunwell Date: Thu, 11 Jan 2018 01:51:36 +0000 Subject: [PATCH 32/52] Use the WooCommerce helpers for creating dummy products/orders instead of the custom factories --- composer.json | 1 - tests/test-data-store.php | 22 +++++++------- ...lass-wp-unittest-factory-for-customers.php | 27 ----------------- .../class-wp-unittest-factory-for-order.php | 28 ------------------ .../class-wp-unittest-factory-for-product.php | 29 ------------------- tests/testcase.php | 23 --------------- 6 files changed, 11 insertions(+), 119 deletions(-) delete mode 100644 tests/test-tools/class-wp-unittest-factory-for-customers.php delete mode 100644 tests/test-tools/class-wp-unittest-factory-for-order.php delete mode 100644 tests/test-tools/class-wp-unittest-factory-for-product.php diff --git a/composer.json b/composer.json index 839fc04..5386513 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,6 @@ }, "autoload-dev": { "classmap": [ - "tests/test-tools", "vendor/woocommerce/woocommerce/tests/framework" ] }, diff --git a/tests/test-data-store.php b/tests/test-data-store.php index 19101db..e994849 100644 --- a/tests/test-data-store.php +++ b/tests/test-data-store.php @@ -87,10 +87,10 @@ public function test_search_orders_can_search_by_order_id() { public function test_search_orders_can_check_post_meta() { $instance = new WC_Order_Data_Store_Custom_Table(); - $order = $this->factory()->order->create(); + $order = WC_Helper_Order::create_order(); $term = uniqid( 'search term ' ); - add_post_meta( $order, 'some_custom_meta_key', $term ); + add_post_meta( $order->get_id(), 'some_custom_meta_key', $term ); add_filter( 'woocommerce_shop_order_search_fields', function () { remove_filter( 'woocommerce_shop_order_search_fields', __FUNCTION__ ); @@ -98,7 +98,7 @@ public function test_search_orders_can_check_post_meta() { } ); $this->assertEquals( - array( $order ), + array( $order->get_id() ), $instance->search_orders( $term ), 'If post meta keys are specified, they should also be searched.' ); @@ -109,10 +109,10 @@ public function test_search_orders_can_check_post_meta() { */ public function test_search_orders_only_checks_post_meta_if_specified() { $instance = new WC_Order_Data_Store_Custom_Table(); - $order = $this->factory()->order->create(); + $order = WC_Helper_Order::create_order(); $term = uniqid( 'search term ' ); - add_post_meta( $order, 'some_custom_meta_key', $term ); + add_post_meta( $order->get_id(), 'some_custom_meta_key', $term ); $this->assertEmpty( $instance->search_orders( $term ), @@ -122,8 +122,8 @@ public function test_search_orders_only_checks_post_meta_if_specified() { public function test_search_orders_checks_table_for_product_item_matches() { $instance = new WC_Order_Data_Store_Custom_Table(); - $product = $this->factory()->product->create_and_get(); - $order = $this->factory()->order->create_and_get(); + $product = WC_Helper_Product::create_simple_product(); + $order = WC_Helper_Order::create_order(); $order->add_product( $product ); $order->save(); @@ -136,10 +136,10 @@ public function test_search_orders_checks_table_for_product_item_matches() { public function test_search_orders_checks_table_for_product_item_matches_with_like_comparison() { $instance = new WC_Order_Data_Store_Custom_Table(); - $product = $this->factory()->product->create_and_get( array( - 'post_title' => 'foo bar baz', - ) ); - $order = $this->factory()->order->create_and_get(); + $product = WC_Helper_Product::create_simple_product(); + $product->set_name( 'Foo Bar Baz' ); + $product->save(); + $order = WC_Helper_Order::create_order(); $order->add_product( $product ); $order->save(); diff --git a/tests/test-tools/class-wp-unittest-factory-for-customers.php b/tests/test-tools/class-wp-unittest-factory-for-customers.php deleted file mode 100644 index 3cb8df7..0000000 --- a/tests/test-tools/class-wp-unittest-factory-for-customers.php +++ /dev/null @@ -1,27 +0,0 @@ -default_generation_definitions = array( - 'user_login' => new WP_UnitTest_Generator_Sequence( 'Customer %s' ), - 'user_pass' => 'password', - 'user_email' => new WP_UnitTest_Generator_Sequence( 'customer_%s@example.org' ), - ); - } - - function get_object_by_id( $user_id ) { - return new WC_Customer( $user_id ); - } -} diff --git a/tests/test-tools/class-wp-unittest-factory-for-order.php b/tests/test-tools/class-wp-unittest-factory-for-order.php deleted file mode 100644 index 4758661..0000000 --- a/tests/test-tools/class-wp-unittest-factory-for-order.php +++ /dev/null @@ -1,28 +0,0 @@ -default_generation_definitions = array( - 'post_status' => 'wc-pending', - 'post_title' => new WP_UnitTest_Generator_Sequence( 'Order %s' ), - 'post_password' => uniqid( 'wc_' ), - 'post_type' => 'shop_order', - ); - } - - function get_object_by_id( $order_id ) { - return wc_get_order( $order_id ); - } -} diff --git a/tests/test-tools/class-wp-unittest-factory-for-product.php b/tests/test-tools/class-wp-unittest-factory-for-product.php deleted file mode 100644 index 9edaccc..0000000 --- a/tests/test-tools/class-wp-unittest-factory-for-product.php +++ /dev/null @@ -1,29 +0,0 @@ -default_generation_definitions = array( - 'post_status' => 'publish', - 'post_title' => new WP_UnitTest_Generator_Sequence( 'Product name %s' ), - 'post_content' => new WP_UnitTest_Generator_Sequence( 'Product description %s' ), - 'post_excerpt' => new WP_UnitTest_Generator_Sequence( 'Product short description %s' ), - 'post_type' => 'product', - ); - } - - function get_object_by_id( $product_id ) { - return wc_get_product( $product_id ); - } -} diff --git a/tests/testcase.php b/tests/testcase.php index afeed59..575305e 100644 --- a/tests/testcase.php +++ b/tests/testcase.php @@ -21,29 +21,6 @@ function truncate_table() { $wpdb->query( "DELETE FROM {$wpdb->prefix}woocommerce_orders" ); } - /** - * Retrieve the core test suite's factory object, but add extra factories. - * - * @return WP_UnitTest_Factory - */ - protected static function factory() { - static $factory = null; - - if ( ! $factory ) { - $instance = new WP_UnitTest_Factory(); - - // Add additional factories. - $instance->customer = new WP_UnitTest_Factory_For_Customer( $instance ); - $instance->order = new WP_UnitTest_Factory_For_Order( $instance ); - $instance->product = new WP_UnitTest_Factory_For_Product( $instance ); - - // Save the instance in the static $factory variable. - $factory = $instance; - } - - return $factory; - } - /** * Retrieve a single row from the Orders table. * From 0f2462d935498c6e310199bdadbc4e4c0a07b432 Mon Sep 17 00:00:00 2001 From: Steve Grunwell Date: Thu, 11 Jan 2018 01:56:54 +0000 Subject: [PATCH 33/52] Update the inline comments for the WC_Custom_Order_Table class --- includes/class-wc-custom-order-table.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/class-wc-custom-order-table.php b/includes/class-wc-custom-order-table.php index d8f9985..264f505 100644 --- a/includes/class-wc-custom-order-table.php +++ b/includes/class-wc-custom-order-table.php @@ -28,11 +28,11 @@ public function setup() { $this->table_name = $wpdb->prefix . 'woocommerce_orders'; - // Inject the plugin into order processing. + // Use the plugin's custom data stores for customers and orders. add_filter( 'woocommerce_customer_data_store', array( $this, 'customer_data_store' ) ); add_filter( 'woocommerce_order_data_store', array( $this, 'order_data_store' ) ); - // Register the CLI command if we're running WP_CLI. + // If we're in a WP-CLI context, load the WP-CLI command. if ( defined( 'WP_CLI' ) && WP_CLI ) { WP_CLI::add_command( 'wc-order-table', 'WC_Custom_Order_Table_CLI' ); } From 84bd22c546c29349011992e36c6aea16df0d5bb3 Mon Sep 17 00:00:00 2001 From: Steve Grunwell Date: Thu, 11 Jan 2018 02:50:51 +0000 Subject: [PATCH 34/52] Clean up the customer data store so it passes coding standards checks. A good portion of the code in that file was copied directly from WC_Customer_Data_Store, so it may be worth taking some time to clean things up properly in WooCommerce core, as well. --- ...ss-wc-customer-data-store-custom-table.php | 134 ++++++++++++------ 1 file changed, 88 insertions(+), 46 deletions(-) diff --git a/includes/class-wc-customer-data-store-custom-table.php b/includes/class-wc-customer-data-store-custom-table.php index b3af89e..c3c47da 100644 --- a/includes/class-wc-customer-data-store-custom-table.php +++ b/includes/class-wc-customer-data-store-custom-table.php @@ -1,17 +1,20 @@ get_table_name(); - $last_order = $wpdb->get_var( "SELECT posts.ID - FROM $wpdb->posts AS posts - LEFT JOIN $table AS meta on posts.ID = meta.order_id - WHERE meta.customer_id = '" . esc_sql( $customer->get_id() ) . "' + $statuses = wc_get_order_statuses(); + $last_order = $wpdb->get_var( $wpdb->prepare( " + SELECT posts.ID FROM $wpdb->posts AS posts + LEFT JOIN " . esc_sql( $table ) . " AS meta on posts.ID = meta.order_id + WHERE meta.customer_id = %d AND posts.post_type = 'shop_order' - AND posts.post_status IN ( '" . implode( "','", array_map( 'esc_sql', array_keys( wc_get_order_statuses() ) ) ) . "' ) - ORDER BY posts.ID DESC - " ); + AND posts.post_status IN (" . implode( ', ', array_fill( 0, count( $statuses ), '%s' ) ) . ') + ORDER BY posts.ID DESC LIMIT 1', + array_merge( array( $customer->get_id() ), array_keys( $statuses ) ) + ) ); return $last_order ? wc_get_order( (int) $last_order ) : false; } @@ -61,14 +66,16 @@ public function get_order_count( &$customer ) { $count = get_user_meta( $customer->get_id(), '_order_count', true ); if ( '' === $count ) { - $table = wc_custom_order_table()->get_table_name(); - $count = $wpdb->get_var( "SELECT COUNT(*) - FROM $wpdb->posts as posts - LEFT JOIN $table AS meta ON posts.ID = meta.order_id - WHERE meta.customer_id = '" . esc_sql( $customer->get_id() ) . "' - AND posts.post_type = 'shop_order' - AND posts.post_status IN ( '" . implode( "','", array_map( 'esc_sql', array_keys( wc_get_order_statuses() ) ) ) . "' ) - " ); + $table = wc_custom_order_table()->get_table_name(); + $statuses = wc_get_order_statuses(); + $count = $wpdb->get_var( $wpdb->prepare( " + SELECT COUNT(*) FROM $wpdb->posts as posts + LEFT JOIN " . esc_sql( $table ) . " AS meta ON posts.ID = meta.order_id + WHERE meta.customer_id = %d + AND posts.post_type = 'shop_order' + AND posts.post_status IN (" . implode( ', ', array_fill( 0, count( $statuses ), '%s' ) ) . ')', + array_merge( array( $customer->get_id() ), array_keys( $statuses ) ) + ) ); update_user_meta( $customer->get_id(), '_order_count', $count ); } @@ -87,22 +94,38 @@ public function get_order_count( &$customer ) { public function get_total_spent( &$customer ) { global $wpdb; - $spent = apply_filters( 'woocommerce_customer_get_total_spent', get_user_meta( $customer->get_id(), '_money_spent', true ), $customer ); + $spent = get_user_meta( $customer->get_id(), '_money_spent', true ); + /** + * Filter the total amount spent by the given customer across all orders. + * + * @param float $spent The total of all orders for this customer. + * @param WC_Customer $customer The customer being queried. + */ + $spent = apply_filters( 'woocommerce_customer_get_total_spent', $spent, $customer ); + + // If there's no saved value, attempt to calculate one. if ( '' === $spent ) { $table = wc_custom_order_table()->get_table_name(); - $statuses = array_map( 'esc_sql', wc_get_is_paid_statuses() ); - $spent = $wpdb->get_var( apply_filters( 'woocommerce_customer_get_total_spent_query', "SELECT SUM(meta.total) - FROM $wpdb->posts as posts - LEFT JOIN {$table} AS meta ON posts.ID = meta.order_id - WHERE meta.customer_id = '" . esc_sql( $customer->get_id() ) . "' - AND posts.post_type = 'shop_order' - AND posts.post_status IN ( 'wc-" . implode( "','wc-", $statuses ) . "' ) - ", $customer ) ); - - if ( ! $spent ) { - $spent = 0; - } + $statuses = array_map( 'self::prefix_wc_status', wc_get_is_paid_statuses() ); + $sql = $wpdb->prepare( " + SELECT SUM(meta.total) FROM $wpdb->posts as posts + LEFT JOIN " . esc_sql( $table ) . " AS meta ON posts.ID = meta.order_id + WHERE meta.customer_id = %d + AND posts.post_type = 'shop_order' + AND posts.post_status IN (" . implode( ', ', array_fill( 0, count( $statuses ), '%s' ) ) . ')', + array_merge( array( $customer->get_id() ), $statuses ) + ); + + /** + * Filter the MySQL query used to determine how much a customer has spent across all orders. + * + * @param string $sql The prepared MySQL statement. + * @param WC_Customer $customer The customer being queried. + */ + $sql = apply_filters( 'woocommerce_customer_get_total_spent_query', $sql, $customer ); + $spent = (float) $wpdb->get_var( $sql ); // WPCS: Unprepared SQL OK. + update_user_meta( $customer->get_id(), '_money_spent', $spent ); } @@ -118,17 +141,18 @@ public function get_total_spent( &$customer ) { * * @param bool $purchased Whether or not the customer has purchased the product. * @param string $customer_email Customer email to check. - * @param int $user_id User ID to check. - * @param int $product_id Product ID to check. - * - * @return bool Whether or not the customer has already purchased the given product ID. - */ + * @param int $user_id User ID to check. + * @param int $product_id Product ID to check. + * + * @return bool Whether or not the customer has already purchased the given product ID. + */ public static function pre_customer_bought_product( $purchased, $customer_email, $user_id, $product_id ) { global $wpdb; $transient_name = 'wc_cbp_' . md5( $customer_email . $user_id . WC_Cache_Helper::get_transient_version( 'orders' ) ); + $result = get_transient( $transient_name ); - if ( false === ( $result = get_transient( $transient_name ) ) ) { + if ( false === $result ) { $customer_data = array( $user_id ); if ( $user_id ) { @@ -144,26 +168,27 @@ public static function pre_customer_bought_product( $purchased, $customer_email, } $customer_data = array_map( 'esc_sql', array_filter( array_unique( $customer_data ) ) ); - $statuses = array_map( 'esc_sql', wc_get_is_paid_statuses() ); + $statuses = array_map( 'self::prefix_wc_status', wc_get_is_paid_statuses() ); if ( sizeof( $customer_data ) == 0 ) { return false; } $table = wc_custom_order_table()->get_table_name(); - $result = $wpdb->get_col( " + $result = $wpdb->get_col( $wpdb->prepare( " SELECT im.meta_value FROM {$wpdb->posts} AS p - INNER JOIN {$table} AS pm ON p.ID = pm.order_id + INNER JOIN " . esc_sql( $table ) . " AS pm ON p.ID = pm.order_id INNER JOIN {$wpdb->prefix}woocommerce_order_items AS i ON p.ID = i.order_id INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS im ON i.order_item_id = im.order_item_id - WHERE p.post_status IN ( 'wc-" . implode( "','wc-", $statuses ) . "' ) + WHERE p.post_status IN (" . implode( ', ', array_fill( 0, count( $statuses ), '%s' ) ) . ") AND ( - pm.billing_email IN ( '" . implode( "','", $customer_data ) . "' ) - OR pm.customer_id IN ( '" . implode( "','", $customer_data ) . "' ) + pm.billing_email IN (" . implode( ', ', array_fill( 0, count( $customer_data ), '%s' ) ) . ") + OR pm.customer_id IN (" . implode( ', ', array_fill( 0, count( $customer_data ), '%s' ) ) . ") ) AND im.meta_key IN ( '_product_id', '_variation_id' ) - AND im.meta_value != 0 - " ); + AND im.meta_value != 0", + array_merge( $statuses, $customer_data, $customer_data ) + ) ); $result = array_map( 'absint', $result ); set_transient( $transient_name, $result, DAY_IN_SECONDS * 30 ); @@ -175,7 +200,7 @@ public static function pre_customer_bought_product( $purchased, $customer_email, /** * Reset customer_id on orders when a user is deleted. * - * @param int $user_id + * @param int $user_id The ID of the deleted user. */ public static function reset_order_customer_id_on_deleted_user( $user_id ) { global $wpdb; @@ -186,4 +211,21 @@ public static function reset_order_customer_id_on_deleted_user( $user_id ) { array( 'customer_id' => $user_id ) ); } + + /** + * Helper function to prefix a status with 'wc-'. + * + * Statuses that already contain the prefix will be skipped. + * + * @param string $status The status to prefix. + * + * @return string The status with 'wc-' prefixed. + */ + public static function prefix_wc_status( $status ) { + if ( 'wc-' === substr( $status, 0, 3 ) ) { + return $status; + } + + return 'wc-' . $status; + } } From 0a0840e9408186fb319eb1c3c1d3e27547a7a763 Mon Sep 17 00:00:00 2001 From: Steve Grunwell Date: Thu, 11 Jan 2018 03:08:32 +0000 Subject: [PATCH 35/52] Minor PHP_CodeSniffer-whitespace adjustments --- includes/class-wc-order-data-store-custom-table.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/includes/class-wc-order-data-store-custom-table.php b/includes/class-wc-order-data-store-custom-table.php index ccb30f8..130711e 100644 --- a/includes/class-wc-order-data-store-custom-table.php +++ b/includes/class-wc-order-data-store-custom-table.php @@ -111,7 +111,7 @@ public function delete( &$order, $args = array() ) { // Delete the database row if force_delete is true. if ( isset( $args['force_delete'] ) && $args['force_delete'] ) { - $wpdb->delete("{$wpdb->prefix}woocommerce_orders", array( 'order_id' => $order_id ) ); + $wpdb->delete( "{$wpdb->prefix}woocommerce_orders", array( 'order_id' => $order_id ) ); } } @@ -479,8 +479,8 @@ public static function filter_database_queries( $query_args, $query_vars ) { '_old_key' => $meta_query['key'], ) ); - // Let this meta query pass through unaltered. } else { + // Let this meta query pass through unaltered. $query_args['_wc_has_meta_columns'] = true; } } @@ -550,7 +550,7 @@ public static function meta_query_where( $where, $wp_query ) { } foreach ( $meta_query as $query ) { - $regex = $wpdb->prepare( "/\(\s?(\w+\.)?meta_key = %s AND (\w+\.)?meta_value /i", $query['_old_key'] ); + $regex = $wpdb->prepare( '/\(\s?(\w+\.)?meta_key = %s AND (\w+\.)?meta_value /i', $query['_old_key'] ); $where = preg_replace( $regex, "( {$table}.{$query['key']} ", $where ); } From 0caaf350beea3dfda93d0ff0b4a10e87e3fc66c2 Mon Sep 17 00:00:00 2001 From: Steve Grunwell Date: Thu, 11 Jan 2018 03:08:53 +0000 Subject: [PATCH 36/52] Ensure the test suite can run on PHP 5.3. --- tests/test-data-store.php | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/tests/test-data-store.php b/tests/test-data-store.php index e994849..791efad 100644 --- a/tests/test-data-store.php +++ b/tests/test-data-store.php @@ -8,6 +8,9 @@ class DataStoreTest extends TestCase { + /** + * @requires PHP 5.4 In order to support inline closures for hook callbacks. + */ public function test_create() { $instance = new WC_Order_Data_Store_Custom_Table(); $property = new ReflectionProperty( $instance, 'creating' ); @@ -92,16 +95,24 @@ public function test_search_orders_can_check_post_meta() { add_post_meta( $order->get_id(), 'some_custom_meta_key', $term ); - add_filter( 'woocommerce_shop_order_search_fields', function () { - remove_filter( 'woocommerce_shop_order_search_fields', __FUNCTION__ ); - return array( 'some_custom_meta_key' ); - } ); + add_filter( 'woocommerce_shop_order_search_fields', __CLASS__ . '::return_array_for_test_search_orders_can_check_post_meta' ); $this->assertEquals( array( $order->get_id() ), $instance->search_orders( $term ), 'If post meta keys are specified, they should also be searched.' ); + + remove_filter( 'woocommerce_shop_order_search_fields', __CLASS__ . '::return_array_for_test_search_orders_can_check_post_meta' ); + } + + /** + * Filter callback for test_search_orders_can_check_post_meta(). + * + * Can be dropped once PHP 5.3 isn't a requirement, as closures are far nicer. + */ + public static function return_array_for_test_search_orders_can_check_post_meta() { + return array( 'some_custom_meta_key' ); } /** From adb7c2e528f13c4f06370b5936a9f8dbcef206d3 Mon Sep 17 00:00:00 2001 From: Steve Grunwell Date: Thu, 11 Jan 2018 14:13:28 +0000 Subject: [PATCH 37/52] With https://github.com/woocommerce/woocommerce/pull/18417 merged into WooCommerce's master branch, restore WooCommerce as a dev dependency (instead of the Liquid Web fork) --- composer.json | 8 +------- composer.lock | 41 ++++++++--------------------------------- 2 files changed, 9 insertions(+), 40 deletions(-) diff --git a/composer.json b/composer.json index 5386513..ac2f08b 100644 --- a/composer.json +++ b/composer.json @@ -3,19 +3,13 @@ "description": "Store WooCommerce order data in a custom table.", "type": "wordpress-plugin", "license": "GPL-2.0", - "repositories": [ - { - "type": "vcs", - "url": "https://github.com/liquidweb/woocommerce" - } - ], "require": { "php": ">=5.2", "composer/installers": "^1.4", "xrstf/composer-php52": "^1.0" }, "require-dev": { - "woocommerce/woocommerce": "dev-fix/get-customer-last-order" + "woocommerce/woocommerce": "dev-master" }, "autoload": { "classmap": [ diff --git a/composer.lock b/composer.lock index 00a37b3..ae579a2 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "144174375d25fca49925f1402b0cca31", + "content-hash": "fe30de23a7822b2387c810e945f9be02", "packages": [ { "name": "composer/installers", @@ -161,16 +161,16 @@ "packages-dev": [ { "name": "woocommerce/woocommerce", - "version": "dev-fix/get-customer-last-order", + "version": "dev-master", "source": { "type": "git", - "url": "https://github.com/liquidweb/woocommerce.git", - "reference": "493298504d00ddb7bbd7c32475657ad4e866120c" + "url": "https://github.com/woocommerce/woocommerce.git", + "reference": "fbbbc7a8df9aaa44d19381325b6e7482c83367d2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/liquidweb/woocommerce/zipball/493298504d00ddb7bbd7c32475657ad4e866120c", - "reference": "493298504d00ddb7bbd7c32475657ad4e866120c", + "url": "https://api.github.com/repos/woocommerce/woocommerce/zipball/fbbbc7a8df9aaa44d19381325b6e7482c83367d2", + "reference": "fbbbc7a8df9aaa44d19381325b6e7482c83367d2", "shasum": "" }, "require": { @@ -193,38 +193,13 @@ "phpcbf": "Fix coding standards warnings/errors automatically with PHP Code Beautifier" } }, - "scripts": { - "pre-update-cmd": [ - "WooCommerce\\GitHooks\\Hooks::preHooks" - ], - "pre-install-cmd": [ - "WooCommerce\\GitHooks\\Hooks::preHooks" - ], - "post-install-cmd": [ - "WooCommerce\\GitHooks\\Hooks::postHooks" - ], - "post-update-cmd": [ - "WooCommerce\\GitHooks\\Hooks::postHooks" - ], - "test": [ - "phpunit" - ], - "phpcs": [ - "phpcs -s -p" - ], - "phpcbf": [ - "phpcbf -p" - ] - }, + "notification-url": "https://packagist.org/downloads/", "license": [ "GPL-2.0+" ], "description": "An eCommerce toolkit that helps you sell anything. Beautifully.", "homepage": "https://woocommerce.com/", - "support": { - "source": "https://github.com/liquidweb/woocommerce/tree/fix/get-customer-last-order" - }, - "time": "2018-01-10T18:39:35+00:00" + "time": "2018-01-11T13:46:09+00:00" } ], "aliases": [], From 19a3e8223f6c7db14e6caee780f5114543564e0b Mon Sep 17 00:00:00 2001 From: Steve Grunwell Date: Thu, 11 Jan 2018 14:36:04 +0000 Subject: [PATCH 38/52] Set the minimum *development* version to PHP 7.0. WooCommerce core requires PHPUnit 6.2.3 as a development dependency which, in turn, requires PHP 7.0+ (https://github.com/sebastianbergmann/phpunit/wiki/Release-Announcement-for-PHPUnit-6.2.0). While the goal is still to support every version that WooCommerce supports (read: PHP >= 5.2), we'll leverage wimg/php-compatibility (as WooCommerce core does) to ensure that the *plugin* code is compatible with necessary versions. Meanwhile, let tests and development tooling use modern technology. --- composer.json | 1 + composer.lock | 14 ++++++++------ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/composer.json b/composer.json index ac2f08b..22d2dbf 100644 --- a/composer.json +++ b/composer.json @@ -9,6 +9,7 @@ "xrstf/composer-php52": "^1.0" }, "require-dev": { + "php": "^7.0", "woocommerce/woocommerce": "dev-master" }, "autoload": { diff --git a/composer.lock b/composer.lock index ae579a2..ebc6fb4 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "fe30de23a7822b2387c810e945f9be02", + "content-hash": "c478d850c4706bb4892b029a2307e74d", "packages": [ { "name": "composer/installers", @@ -165,12 +165,12 @@ "source": { "type": "git", "url": "https://github.com/woocommerce/woocommerce.git", - "reference": "fbbbc7a8df9aaa44d19381325b6e7482c83367d2" + "reference": "45dfb7d015698b7eb4b87a4a7a17247ed62bbe86" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/woocommerce/woocommerce/zipball/fbbbc7a8df9aaa44d19381325b6e7482c83367d2", - "reference": "fbbbc7a8df9aaa44d19381325b6e7482c83367d2", + "url": "https://api.github.com/repos/woocommerce/woocommerce/zipball/45dfb7d015698b7eb4b87a4a7a17247ed62bbe86", + "reference": "45dfb7d015698b7eb4b87a4a7a17247ed62bbe86", "shasum": "" }, "require": { @@ -199,7 +199,7 @@ ], "description": "An eCommerce toolkit that helps you sell anything. Beautifully.", "homepage": "https://woocommerce.com/", - "time": "2018-01-11T13:46:09+00:00" + "time": "2018-01-11T14:31:45+00:00" } ], "aliases": [], @@ -212,5 +212,7 @@ "platform": { "php": ">=5.2" }, - "platform-dev": [] + "platform-dev": { + "php": "^7.0" + } } From 37730110bb2ffc76689c79b7601b0c981179edf5 Mon Sep 17 00:00:00 2001 From: Steve Grunwell Date: Thu, 11 Jan 2018 14:58:12 +0000 Subject: [PATCH 39/52] Install the same tooling + leverage the WooCommerce PHP_CodeSniffer configuration for coding standards + compatibility checks --- composer.json | 6 +- composer.lock | 262 ++++++++++++++++++++++++++++++++++++++++++++++++- phpcs.xml.dist | 7 +- 3 files changed, 265 insertions(+), 10 deletions(-) diff --git a/composer.json b/composer.json index 22d2dbf..35017c6 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,11 @@ }, "require-dev": { "php": "^7.0", - "woocommerce/woocommerce": "dev-master" + "dealerdirect/phpcodesniffer-composer-installer": "^0.4.4", + "wimg/php-compatibility": "^8.1", + "woocommerce/woocommerce": "dev-master", + "woocommerce/woocommerce-sniffs": "^0.0.1", + "wp-coding-standards/wpcs": "^0.14" }, "autoload": { "classmap": [ diff --git a/composer.lock b/composer.lock index ebc6fb4..5ac7941 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "c478d850c4706bb4892b029a2307e74d", + "content-hash": "fd86f99767fa5412fdd10e6971be6a34", "packages": [ { "name": "composer/installers", @@ -159,18 +159,189 @@ } ], "packages-dev": [ + { + "name": "dealerdirect/phpcodesniffer-composer-installer", + "version": "v0.4.4", + "source": { + "type": "git", + "url": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer.git", + "reference": "2e41850d5f7797cbb1af7b030d245b3b24e63a08" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Dealerdirect/phpcodesniffer-composer-installer/zipball/2e41850d5f7797cbb1af7b030d245b3b24e63a08", + "reference": "2e41850d5f7797cbb1af7b030d245b3b24e63a08", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0", + "php": "^5.3|^7", + "squizlabs/php_codesniffer": "*" + }, + "require-dev": { + "composer/composer": "*", + "wimg/php-compatibility": "^8.0" + }, + "suggest": { + "dealerdirect/qa-tools": "All the PHP QA tools you'll need" + }, + "type": "composer-plugin", + "extra": { + "class": "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" + }, + "autoload": { + "psr-4": { + "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Franck Nijhof", + "email": "f.nijhof@dealerdirect.nl", + "homepage": "http://workingatdealerdirect.eu", + "role": "Developer" + } + ], + "description": "PHP_CodeSniffer Standards Composer Installer Plugin", + "homepage": "http://workingatdealerdirect.eu", + "keywords": [ + "PHPCodeSniffer", + "PHP_CodeSniffer", + "code quality", + "codesniffer", + "composer", + "installer", + "phpcs", + "plugin", + "qa", + "quality", + "standard", + "standards", + "style guide", + "stylecheck", + "tests" + ], + "time": "2017-12-06T16:27:17+00:00" + }, + { + "name": "squizlabs/php_codesniffer", + "version": "3.2.2", + "source": { + "type": "git", + "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", + "reference": "d7c00c3000ac0ce79c96fcbfef86b49a71158cd1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/d7c00c3000ac0ce79c96fcbfef86b49a71158cd1", + "reference": "d7c00c3000ac0ce79c96fcbfef86b49a71158cd1", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0" + }, + "bin": [ + "bin/phpcs", + "bin/phpcbf" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Greg Sherwood", + "role": "lead" + } + ], + "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "homepage": "http://www.squizlabs.com/php-codesniffer", + "keywords": [ + "phpcs", + "standards" + ], + "time": "2017-12-19T21:44:46+00:00" + }, + { + "name": "wimg/php-compatibility", + "version": "8.1.0", + "source": { + "type": "git", + "url": "https://github.com/wimg/PHPCompatibility.git", + "reference": "4ac01e4fe8faaa4f8d3b3cd06ea92e5418ce472e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/wimg/PHPCompatibility/zipball/4ac01e4fe8faaa4f8d3b3cd06ea92e5418ce472e", + "reference": "4ac01e4fe8faaa4f8d3b3cd06ea92e5418ce472e", + "shasum": "" + }, + "require": { + "php": ">=5.3", + "squizlabs/php_codesniffer": "^2.2 || ^3.0.2" + }, + "conflict": { + "squizlabs/php_codesniffer": "2.6.2" + }, + "require-dev": { + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0" + }, + "suggest": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.4.3" + }, + "type": "phpcodesniffer-standard", + "autoload": { + "psr-4": { + "PHPCompatibility\\": "PHPCompatibility/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0" + ], + "authors": [ + { + "name": "Wim Godden", + "role": "lead" + } + ], + "description": "A set of sniffs for PHP_CodeSniffer that checks for PHP version compatibility.", + "homepage": "http://techblog.wimgodden.be/tag/codesniffer/", + "keywords": [ + "compatibility", + "phpcs", + "standards" + ], + "time": "2017-12-27T21:58:38+00:00" + }, { "name": "woocommerce/woocommerce", "version": "dev-master", "source": { "type": "git", "url": "https://github.com/woocommerce/woocommerce.git", - "reference": "45dfb7d015698b7eb4b87a4a7a17247ed62bbe86" + "reference": "a887b49bb489852280501d1774dc05058bb47b7d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/woocommerce/woocommerce/zipball/45dfb7d015698b7eb4b87a4a7a17247ed62bbe86", - "reference": "45dfb7d015698b7eb4b87a4a7a17247ed62bbe86", + "url": "https://api.github.com/repos/woocommerce/woocommerce/zipball/a887b49bb489852280501d1774dc05058bb47b7d", + "reference": "a887b49bb489852280501d1774dc05058bb47b7d", "shasum": "" }, "require": { @@ -199,7 +370,88 @@ ], "description": "An eCommerce toolkit that helps you sell anything. Beautifully.", "homepage": "https://woocommerce.com/", - "time": "2018-01-11T14:31:45+00:00" + "time": "2018-01-11T14:34:41+00:00" + }, + { + "name": "woocommerce/woocommerce-sniffs", + "version": "0.0.1", + "source": { + "type": "git", + "url": "https://github.com/woocommerce/woocommerce-sniffs.git", + "reference": "383d5b361c1d7532ae1ca6156fd7619fd37bbc05" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/woocommerce/woocommerce-sniffs/zipball/383d5b361c1d7532ae1ca6156fd7619fd37bbc05", + "reference": "383d5b361c1d7532ae1ca6156fd7619fd37bbc05", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "squizlabs/php_codesniffer": "^3.0.2" + }, + "suggest": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.4.3" + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Claudio Sanches", + "email": "claudio@automattic.com" + } + ], + "description": "WooCommerce sniffs", + "keywords": [ + "phpcs", + "standards", + "woocommerce", + "wordpress" + ], + "time": "2017-12-21T22:52:52+00:00" + }, + { + "name": "wp-coding-standards/wpcs", + "version": "0.14.0", + "source": { + "type": "git", + "url": "https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards.git", + "reference": "8cadf48fa1c70b2381988e0a79e029e011a8f41c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/WordPress-Coding-Standards/WordPress-Coding-Standards/zipball/8cadf48fa1c70b2381988e0a79e029e011a8f41c", + "reference": "8cadf48fa1c70b2381988e0a79e029e011a8f41c", + "shasum": "" + }, + "require": { + "php": ">=5.3", + "squizlabs/php_codesniffer": "^2.9.0 || ^3.0.2" + }, + "suggest": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.4.3" + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Contributors", + "homepage": "https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer rules (sniffs) to enforce WordPress coding conventions", + "keywords": [ + "phpcs", + "standards", + "wordpress" + ], + "time": "2017-11-01T15:10:46+00:00" } ], "aliases": [], diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 56337d6..e0b6074 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -2,8 +2,8 @@ Generally-applicable sniffs for WordPress plugins - - + + @@ -12,7 +12,6 @@ - */node_modules/* - */vendor/* + */tests/* From d6df6116c1bf536e19cdde95b01f764c95037ce7 Mon Sep 17 00:00:00 2001 From: Steve Grunwell Date: Thu, 11 Jan 2018 15:46:58 +0000 Subject: [PATCH 40/52] Simplify the Travis file, removing older versions of PHP and including PHP_CodeSniffer --- .travis.yml | 44 ++++++++++---------------------------------- 1 file changed, 10 insertions(+), 34 deletions(-) diff --git a/.travis.yml b/.travis.yml index 60337b8..5445942 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,7 @@ +language: php sudo: false dist: trusty -language: php - notifications: email: false @@ -11,48 +10,25 @@ cache: - $HOME/.composer/cache matrix: + fast_finish: true include: - php: 7.2 - env: WP_VERSION=trunk + env: WP_VERSION=trunk WP_MULTISITE=0 RUN_PHPCS=1 + - php: 7.2 + env: WP_VERSION=trunk WP_MULTISITE=1 - php: 7.1 - env: WP_VERSION=latest + env: WP_VERSION=latest WP_MULTISITE=0 - php: 7.0 - env: WP_VERSION=latest - - php: 5.6 - env: WP_VERSION=latest - #- php: 5.6 - # env: WP_TRAVISCI=phpcs - - php: 5.3 - env: WP_VERSION=latest - dist: precise + env: WP_VERSION=latest WP_MULTISITE=0 before_script: - export PATH="$HOME/.composer/vendor/bin:$PATH" - - | - if [ -f ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini ]; then - phpenv config-rm xdebug.ini - else - echo "xdebug.ini does not exist" - fi - - | - if [[ ! -z "$WP_VERSION" ]] ; then - bash bin/install-wp-tests.sh wordpress_test root '' localhost $WP_VERSION - composer global require "phpunit/phpunit=4.8.*|5.7.*" - fi - - | - if [[ "$WP_TRAVISCI" == "phpcs" ]] ; then - composer global require wp-coding-standards/wpcs - phpcs --config-set installed_paths $HOME/.composer/vendor/wp-coding-standards/wpcs - fi + - bash tests/bin/install.sh woocommerce_test root '' localhost $WP_VERSION - composer install --prefer-source script: + - phpunit - | - if [[ ! -z "$WP_VERSION" ]] ; then - phpunit - WP_MULTISITE=1 phpunit - fi - - | - if [[ "$WP_TRAVISCI" == "phpcs" ]] ; then + if [[ ${RUN_PHPCS} == 1 ]]; then phpcs fi From 05269e0ee23d757d2202b2be5117fe1beee3d192 Mon Sep 17 00:00:00 2001 From: Steve Grunwell Date: Thu, 11 Jan 2018 15:52:00 +0000 Subject: [PATCH 41/52] Move the bin/ directory under tests/, since the only script in there is only used for testing --- {bin => tests/bin}/install-wp-tests.sh | 0 tests/bootstrap.php | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename {bin => tests/bin}/install-wp-tests.sh (100%) diff --git a/bin/install-wp-tests.sh b/tests/bin/install-wp-tests.sh similarity index 100% rename from bin/install-wp-tests.sh rename to tests/bin/install-wp-tests.sh diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 027a048..c09a5ed 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -17,7 +17,7 @@ exit( 1 ); } elseif ( ! file_exists( $_tests_dir . '/includes/functions.php' ) ) { - echo "\033[0;31mCould not find $_tests_dir/includes/functions.php, have you run `bin/install-wp-tests.sh`?\033[0;m" . PHP_EOL; + echo "\033[0;31mCould not find $_tests_dir/includes/functions.php, have you run `tests/bin/install-wp-tests.sh`?\033[0;m" . PHP_EOL; exit( 1 ); } From 68e58ecde2389751388366d799e965adeae8cdde Mon Sep 17 00:00:00 2001 From: Steve Grunwell Date: Thu, 11 Jan 2018 15:55:18 +0000 Subject: [PATCH 42/52] When referencing a file, it helps if you use the right filename --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 5445942..cae5d4c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,7 +23,7 @@ matrix: before_script: - export PATH="$HOME/.composer/vendor/bin:$PATH" - - bash tests/bin/install.sh woocommerce_test root '' localhost $WP_VERSION + - bash tests/bin/install-wp-tests.sh woocommerce_test root '' localhost $WP_VERSION - composer install --prefer-source script: From 9b761489063dcabd6b75ab670f1ad6774e88b45a Mon Sep 17 00:00:00 2001 From: Steve Grunwell Date: Thu, 11 Jan 2018 16:12:32 +0000 Subject: [PATCH 43/52] Remove the defaultTestSuite property from the PHPUnit configuration --- phpunit.xml.dist | 1 - 1 file changed, 1 deletion(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 2aed6ba..58ff400 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -6,7 +6,6 @@ convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" - defaultTestSuite="plugin" > From 92b4571f4f2f3d5d5806a72a05bd6d3f74ad0863 Mon Sep 17 00:00:00 2001 From: Steve Grunwell Date: Thu, 11 Jan 2018 16:14:00 +0000 Subject: [PATCH 44/52] Specify the exact path to PHP_CodeSniffer --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index cae5d4c..a94e353 100644 --- a/.travis.yml +++ b/.travis.yml @@ -30,5 +30,5 @@ script: - phpunit - | if [[ ${RUN_PHPCS} == 1 ]]; then - phpcs + ./vendor/bin/phpcs fi From 969d1e7814200d1f990883cd25ca050fb3e270d8 Mon Sep 17 00:00:00 2001 From: Steve Grunwell Date: Thu, 11 Jan 2018 16:27:02 +0000 Subject: [PATCH 45/52] Permit multisite tests to fail (for now), as there's a bug within the WooCommerce core test suite that prevents it from passing. When running WooCommerce's core test suite (with or without the custom orders table plugin) with WP_MULTISITE=1, we get the following error: 1) WC_Tests_Setup_Functions::test_wizard_in_cart_payment_gateways Failed asserting that two arrays are equal. --- Expected +++ Actual @@ @@ Array ( - 'paypal' => false + 'stripe' => true + 'ppec_paypal' => true /home/travis/build/liquidweb/woocommerce-order-tables/vendor/woocommerce/woocommerce/tests/unit-tests/setup/functions.php:38 The multisite test should remain in the test matrix as a reminder, but a failure for code we don't control shouldn't break the build. --- .travis.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index a94e353..6a02f1b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,8 +14,12 @@ matrix: include: - php: 7.2 env: WP_VERSION=trunk WP_MULTISITE=0 RUN_PHPCS=1 + # The following WooCommerce core test currently fails in multisite, with or without this + # plugin being active: + # + # WC_Tests_Setup_Functions::test_wizard_in_cart_payment_gateways() - php: 7.2 - env: WP_VERSION=trunk WP_MULTISITE=1 + env: WP_VERSION=trunk WP_MULTISITE=1 TRAVIS_ALLOW_FAILURE=1 - php: 7.1 env: WP_VERSION=latest WP_MULTISITE=0 - php: 7.0 From e2bdc79bf78b9eeb0927b7f663bb51d046c088b3 Mon Sep 17 00:00:00 2001 From: Steve Grunwell Date: Thu, 11 Jan 2018 16:39:20 +0000 Subject: [PATCH 46/52] Use the allow_failures node within the test matrix --- .travis.yml | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6a02f1b..211f928 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,17 +14,22 @@ matrix: include: - php: 7.2 env: WP_VERSION=trunk WP_MULTISITE=0 RUN_PHPCS=1 - # The following WooCommerce core test currently fails in multisite, with or without this - # plugin being active: - # - # WC_Tests_Setup_Functions::test_wizard_in_cart_payment_gateways() - php: 7.2 - env: WP_VERSION=trunk WP_MULTISITE=1 TRAVIS_ALLOW_FAILURE=1 + env: WP_VERSION=trunk WP_MULTISITE=1 - php: 7.1 env: WP_VERSION=latest WP_MULTISITE=0 - php: 7.0 env: WP_VERSION=latest WP_MULTISITE=0 + # The following WooCommerce core test currently fails in multisite, with or without this + # plugin being active: + # + # WC_Tests_Setup_Functions::test_wizard_in_cart_payment_gateways() + allow_failures: + - php: 7.2 + env: WP_VERSION=trunk WP_MULTISITE=1 + + before_script: - export PATH="$HOME/.composer/vendor/bin:$PATH" - bash tests/bin/install-wp-tests.sh woocommerce_test root '' localhost $WP_VERSION From e87d78da33322ef8636bdc7d5f4ef99fd6321c93 Mon Sep 17 00:00:00 2001 From: Steve Grunwell Date: Thu, 11 Jan 2018 16:51:51 +0000 Subject: [PATCH 47/52] Coding standards cleanup: - Enable @author tags within the plugin - Whitelist direct database usage for the customer data store - Fix instances of `require` being called as a function, rather than a language construct - Avoid double-quotes in SQL query fragments that don't need them - Use `count()` instead of `sizeof()` --- ...ss-wc-customer-data-store-custom-table.php | 20 +++++++++---------- phpcs.xml.dist | 5 ++++- wc-custom-order-table.php | 4 ++-- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/includes/class-wc-customer-data-store-custom-table.php b/includes/class-wc-customer-data-store-custom-table.php index c3c47da..e322934 100644 --- a/includes/class-wc-customer-data-store-custom-table.php +++ b/includes/class-wc-customer-data-store-custom-table.php @@ -46,7 +46,7 @@ public function get_last_order( &$customer ) { AND posts.post_status IN (" . implode( ', ', array_fill( 0, count( $statuses ), '%s' ) ) . ') ORDER BY posts.ID DESC LIMIT 1', array_merge( array( $customer->get_id() ), array_keys( $statuses ) ) - ) ); + ) ); // WPCS: DB call OK. return $last_order ? wc_get_order( (int) $last_order ) : false; } @@ -75,7 +75,7 @@ public function get_order_count( &$customer ) { AND posts.post_type = 'shop_order' AND posts.post_status IN (" . implode( ', ', array_fill( 0, count( $statuses ), '%s' ) ) . ')', array_merge( array( $customer->get_id() ), array_keys( $statuses ) ) - ) ); + ) ); // WPCS: DB call OK. update_user_meta( $customer->get_id(), '_order_count', $count ); } @@ -124,7 +124,7 @@ public function get_total_spent( &$customer ) { * @param WC_Customer $customer The customer being queried. */ $sql = apply_filters( 'woocommerce_customer_get_total_spent_query', $sql, $customer ); - $spent = (float) $wpdb->get_var( $sql ); // WPCS: Unprepared SQL OK. + $spent = (float) $wpdb->get_var( $sql ); // WPCS: Unprepared SQL OK, DB call OK. update_user_meta( $customer->get_id(), '_money_spent', $spent ); } @@ -170,7 +170,7 @@ public static function pre_customer_bought_product( $purchased, $customer_email, $customer_data = array_map( 'esc_sql', array_filter( array_unique( $customer_data ) ) ); $statuses = array_map( 'self::prefix_wc_status', wc_get_is_paid_statuses() ); - if ( sizeof( $customer_data ) == 0 ) { + if ( 0 === count( $customer_data ) ) { return false; } @@ -180,21 +180,21 @@ public static function pre_customer_bought_product( $purchased, $customer_email, INNER JOIN " . esc_sql( $table ) . " AS pm ON p.ID = pm.order_id INNER JOIN {$wpdb->prefix}woocommerce_order_items AS i ON p.ID = i.order_id INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS im ON i.order_item_id = im.order_item_id - WHERE p.post_status IN (" . implode( ', ', array_fill( 0, count( $statuses ), '%s' ) ) . ") + WHERE p.post_status IN (" . implode( ', ', array_fill( 0, count( $statuses ), '%s' ) ) . ') AND ( - pm.billing_email IN (" . implode( ', ', array_fill( 0, count( $customer_data ), '%s' ) ) . ") - OR pm.customer_id IN (" . implode( ', ', array_fill( 0, count( $customer_data ), '%s' ) ) . ") + pm.billing_email IN (' . implode( ', ', array_fill( 0, count( $customer_data ), '%s' ) ) . ') + OR pm.customer_id IN (' . implode( ', ', array_fill( 0, count( $customer_data ), '%s' ) ) . ") ) AND im.meta_key IN ( '_product_id', '_variation_id' ) AND im.meta_value != 0", array_merge( $statuses, $customer_data, $customer_data ) - ) ); + ) ); // WPCS: DB call OK. $result = array_map( 'absint', $result ); set_transient( $transient_name, $result, DAY_IN_SECONDS * 30 ); } - return in_array( (int) $product_id, $result ); + return in_array( (int) $product_id, $result, true ); } /** @@ -209,7 +209,7 @@ public static function reset_order_customer_id_on_deleted_user( $user_id ) { wc_custom_order_table()->get_table_name(), array( 'customer_id' => 0 ), array( 'customer_id' => $user_id ) - ); + ); // WPCS: DB call OK. } /** diff --git a/phpcs.xml.dist b/phpcs.xml.dist index e0b6074..242bd78 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -2,7 +2,10 @@ Generally-applicable sniffs for WordPress plugins - + + + + diff --git a/wc-custom-order-table.php b/wc-custom-order-table.php index c16be92..bffa17b 100644 --- a/wc-custom-order-table.php +++ b/wc-custom-order-table.php @@ -22,7 +22,7 @@ /* Load includes via a PHP 5.2-compatible autoloader. */ if ( file_exists( WC_CUSTOM_ORDER_TABLE_PATH . 'vendor/autoload_52.php' ) ) { - require( WC_CUSTOM_ORDER_TABLE_PATH . 'vendor/autoload_52.php' ); + require WC_CUSTOM_ORDER_TABLE_PATH . 'vendor/autoload_52.php'; } /** @@ -43,7 +43,7 @@ function wc_custom_order_table() { global $wc_custom_order_table; if ( ! $wc_custom_order_table instanceof WC_Custom_Order_Table ) { - $wc_custom_order_table = new WC_Custom_Order_Table; + $wc_custom_order_table = new WC_Custom_Order_Table(); $wc_custom_order_table->setup(); } From 46666ddd01d27766aff663a365d0e8480507accb Mon Sep 17 00:00:00 2001 From: Steve Grunwell Date: Thu, 11 Jan 2018 17:05:27 +0000 Subject: [PATCH 48/52] Fix inline comments based on PHPCS report --- includes/class-wc-order-data-store-custom-table.php | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/includes/class-wc-order-data-store-custom-table.php b/includes/class-wc-order-data-store-custom-table.php index 130711e..a694def 100644 --- a/includes/class-wc-order-data-store-custom-table.php +++ b/includes/class-wc-order-data-store-custom-table.php @@ -21,6 +21,9 @@ class WC_Order_Data_Store_Custom_Table extends WC_Order_Data_Store_CPT { */ protected $creating = false; + /** + * Hook into WooCommerce database queries related to orders. + */ public function __construct() { // When creating a WooCommerce order data store request, filter the MySQL query. @@ -498,8 +501,8 @@ public static function filter_database_queries( $query_args, $query_vars ) { * * @global $wpdb * - * @param string $join The MySQL JOIN statement. - * @param WP_Query $query The WP_Query object, passed by reference. + * @param string $join The MySQL JOIN statement. + * @param WP_Query $wp_query The WP_Query object, passed by reference. * * @return string The filtered JOIN statement. */ @@ -534,8 +537,8 @@ public static function posts_join( $join, $wp_query ) { * * @global $wpdb * - * @param string $where The MySQL WHERE statement. - * @param WP_Query $query The WP_Query object, passed by reference. + * @param string $where The MySQL WHERE statement. + * @param WP_Query $wp_query The WP_Query object, passed by reference. * * @return string The [potentially-] filtered WHERE statement. */ From cb5202376d3888da3e616fa54f24a46e06d07a93 Mon Sep 17 00:00:00 2001 From: Steve Grunwell Date: Thu, 11 Jan 2018 17:06:06 +0000 Subject: [PATCH 49/52] Whitelist direct database queries in the order data store --- ...class-wc-order-data-store-custom-table.php | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/includes/class-wc-order-data-store-custom-table.php b/includes/class-wc-order-data-store-custom-table.php index a694def..e9ed504 100644 --- a/includes/class-wc-order-data-store-custom-table.php +++ b/includes/class-wc-order-data-store-custom-table.php @@ -114,7 +114,10 @@ public function delete( &$order, $args = array() ) { // Delete the database row if force_delete is true. if ( isset( $args['force_delete'] ) && $args['force_delete'] ) { - $wpdb->delete( "{$wpdb->prefix}woocommerce_orders", array( 'order_id' => $order_id ) ); + $wpdb->delete( + "{$wpdb->prefix}woocommerce_orders", + array( 'order_id' => $order_id ) + ); // WPCS: DB call OK. } } @@ -154,9 +157,9 @@ public function get_order_data_from_table( $order ) { $table = wc_custom_order_table()->get_table_name(); $data = $wpdb->get_row( $wpdb->prepare( - "SELECT * FROM {$table} WHERE order_id = %d", + 'SELECT * FROM ' . esc_sql( $table ) . ' WHERE order_id = %d LIMIT 1', $order->get_id() - ), ARRAY_A ); + ), ARRAY_A ); // WPCS: DB call OK. // Expand anything that might need assistance. $data['prices_include_tax'] = wc_string_to_bool( $data['prices_include_tax'] ); @@ -237,7 +240,7 @@ protected function update_post_meta( &$order ) { // Insert or update the database record. if ( $this->creating ) { - $wpdb->insert( $table, $order_data ); + $wpdb->insert( $table, $order_data ); // WPCS: DB call OK. $this->creating = false; @@ -258,14 +261,16 @@ protected function update_post_meta( &$order ) { } if ( ! empty( $changes ) ) { - $wpdb->update( $table, $changes, array( 'order_id' => $order->get_id() ) ); + $wpdb->update( $table, $changes, array( 'order_id' => $order->get_id() ) ); // WPCS: DB call OK. } } $updated_props = array_keys( (array) $changes ); // If customer changed, update any downloadable permissions. - if ( in_array( 'customer_user', $updated_props ) || in_array( 'billing_email', $updated_props ) ) { + $customer_props = array( 'customer_user', 'billing_email' ); + + if ( ! empty( array_intersect( $customer_props, $updated_props ) ) ) { $data_store = WC_Data_Store::load( 'customer-download' ); $data_store->update_user_by_order_id( $order->get_id(), $order->get_customer_id(), $order->get_billing_email() ); } @@ -286,7 +291,7 @@ public function get_order_id_by_order_key( $order_key ) { return $wpdb->get_var( $wpdb->prepare( "SELECT order_id FROM {$wpdb->prefix}woocommerce_orders WHERE order_key = %s", $order_key - ) ); + ) ); // WPCS: DB call OK. } /** @@ -356,7 +361,7 @@ public function search_orders( $term ) { array( '%' . $wpdb->esc_like( $term ) . '%' ), $meta_keys ) - ) ) ); + ) ) ); // WPCS: DB call OK. } } @@ -365,7 +370,7 @@ public function search_orders( $term ) { SELECT order_id FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_name LIKE %s", '%' . $wpdb->esc_like( $term ) . '%' - ) ) ); + ) ) ); // WPCS: DB call OK. // Reduce the array of order IDs to unique values. $order_ids = array_unique( $order_ids ); From a4591e5bef204875347ecd5dc3102653823b8d61 Mon Sep 17 00:00:00 2001 From: Steve Grunwell Date: Thu, 11 Jan 2018 17:06:38 +0000 Subject: [PATCH 50/52] Restructure the filter_database_queries() function to avoid variable assignment within a conditional statement --- .../class-wc-order-data-store-custom-table.php | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/includes/class-wc-order-data-store-custom-table.php b/includes/class-wc-order-data-store-custom-table.php index e9ed504..3aa9060 100644 --- a/includes/class-wc-order-data-store-custom-table.php +++ b/includes/class-wc-order-data-store-custom-table.php @@ -481,12 +481,15 @@ public static function filter_database_queries( $query_args, $query_vars ) { ) ); } - if ( isset( $meta_query['key'] ) && ( $column = array_search( $meta_query['key'], self::get_postmeta_mapping(), true ) ) ) { - $query_args['wc_order_meta_query'][] = array_merge( $meta_query, array( - 'key' => $column, - '_old_key' => $meta_query['key'], - ) ); - + if ( isset( $meta_query['key'] ) ) { + $column = array_search( $meta_query['key'], self::get_postmeta_mapping(), true ); + + if ( $column ) { + $query_args['wc_order_meta_query'][] = array_merge( $meta_query, array( + 'key' => $column, + '_old_key' => $meta_query['key'], + ) ); + } } else { // Let this meta query pass through unaltered. $query_args['_wc_has_meta_columns'] = true; From b7804c86f64f404cf96235910b74190ef788b34f Mon Sep 17 00:00:00 2001 From: Steve Grunwell Date: Thu, 11 Jan 2018 17:12:50 +0000 Subject: [PATCH 51/52] Whitelist a more complicated SQL query for search_orders() --- includes/class-wc-order-data-store-custom-table.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/includes/class-wc-order-data-store-custom-table.php b/includes/class-wc-order-data-store-custom-table.php index 3aa9060..85eaf27 100644 --- a/includes/class-wc-order-data-store-custom-table.php +++ b/includes/class-wc-order-data-store-custom-table.php @@ -345,10 +345,10 @@ public function search_orders( $term ) { $where[] = "{$column} LIKE %s"; } - $order_ids = array_merge( $order_ids, $wpdb->get_col( $wpdb->prepare( " - SELECT DISTINCT order_id FROM {$table} WHERE " . implode( ' OR ', $where ), + $order_ids = array_merge( $order_ids, $wpdb->get_col( $wpdb->prepare( + 'SELECT DISTINCT order_id FROM ' . esc_sql( $table ) . ' WHERE ' . implode( ' OR ', $where ), array_fill( 0, count( $where ), '%' . $wpdb->esc_like( $term ) . '%' ) - ) ) ); + ) ) ); // WPCS: DB call OK, Unprepared SQL ok, PreparedSQLPlaceholders replacement count ok. } // For anything else, fall back to postmeta. From 809235a8f3d8c2ffc95742997f0862aeb4b0e688 Mon Sep 17 00:00:00 2001 From: Steve Grunwell Date: Thu, 11 Jan 2018 17:16:55 +0000 Subject: [PATCH 52/52] Remove an empty() call that could cause trouble on PHP < 5.5 --- includes/class-wc-order-data-store-custom-table.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-wc-order-data-store-custom-table.php b/includes/class-wc-order-data-store-custom-table.php index 85eaf27..aa4c5dd 100644 --- a/includes/class-wc-order-data-store-custom-table.php +++ b/includes/class-wc-order-data-store-custom-table.php @@ -270,7 +270,7 @@ protected function update_post_meta( &$order ) { // If customer changed, update any downloadable permissions. $customer_props = array( 'customer_user', 'billing_email' ); - if ( ! empty( array_intersect( $customer_props, $updated_props ) ) ) { + if ( array_intersect( $customer_props, $updated_props ) ) { $data_store = WC_Data_Store::load( 'customer-download' ); $data_store->update_user_by_order_id( $order->get_id(), $order->get_customer_id(), $order->get_billing_email() ); }