diff --git a/includes/class-wc-post-types.php b/includes/class-wc-post-types.php index 213cfc4992c..616b3292bb3 100644 --- a/includes/class-wc-post-types.php +++ b/includes/class-wc-post-types.php @@ -484,14 +484,6 @@ class WC_Post_Types { $order_statuses = apply_filters( 'woocommerce_register_shop_order_post_statuses', array( - 'wc-auto-draft' => array( - 'public' => false, - 'exclude_from_search' => false, - 'show_in_admin_all_list' => true, - 'show_in_admin_status_list' => true, - /* translators: %s: number of orders */ - 'label_count' => _n_noop( 'Draft (%s)', 'Draft (%s)', 'woocommerce' ), - ), 'wc-pending' => array( 'label' => _x( 'Pending payment', 'Order status', 'woocommerce' ), 'public' => false, diff --git a/includes/class-wc-webhook.php b/includes/class-wc-webhook.php index 9170415044c..ddb3fa1bc93 100644 --- a/includes/class-wc-webhook.php +++ b/includes/class-wc-webhook.php @@ -123,62 +123,158 @@ class WC_Webhook extends WC_Legacy_Webhook { * @return bool True if webhook should be delivered, false otherwise. */ private function should_deliver( $arg ) { - $should_deliver = true; - $current_action = current_action(); + $should_deliver = $this->is_active() && $this->is_valid_topic() && $this->is_valid_action( $arg ) && $this->is_valid_resource( $arg ); - // Only active webhooks can be delivered. - if ( 'active' !== $this->get_status() ) { - $should_deliver = false; - } elseif ( in_array( $current_action, array( 'delete_post', 'wp_trash_post', 'untrashed_post' ), true ) ) { - // Only deliver deleted/restored event for coupons, orders, and products. - if ( isset( $GLOBALS['post_type'] ) && ! in_array( $GLOBALS['post_type'], array( 'shop_coupon', 'shop_order', 'product' ), true ) ) { - $should_deliver = false; - } - - // Check if is delivering for the correct resource. - if ( isset( $GLOBALS['post_type'] ) && str_replace( 'shop_', '', $GLOBALS['post_type'] ) !== $this->get_resource() ) { - $should_deliver = false; - } - } elseif ( 'delete_user' === $current_action ) { - $user = get_userdata( absint( $arg ) ); - - // Only deliver deleted customer event for users with customer role. - if ( ! $user || ! in_array( 'customer', (array) $user->roles, true ) ) { - $should_deliver = false; - } - } elseif ( 'order' === $this->get_resource() && ! in_array( get_post_type( absint( $arg ) ), wc_get_order_types( 'order-webhooks' ), true ) ) { - // Only if the custom order type has chosen to exclude order webhooks from triggering along with its own webhooks. - $should_deliver = false; - - } elseif ( 0 === strpos( $current_action, 'woocommerce_process_shop' ) || 0 === strpos( $current_action, 'woocommerce_process_product' ) ) { - // The `woocommerce_process_shop_*` and `woocommerce_process_product_*` hooks - // fire for create and update of products and orders, so check the post - // creation date to determine the actual event. - $resource = get_post( absint( $arg ) ); - - // Drafts don't have post_date_gmt so calculate it here. - $gmt_date = get_gmt_from_date( $resource->post_date ); - - // A resource is considered created when the hook is executed within 10 seconds of the post creation date. - $resource_created = ( ( time() - 10 ) <= strtotime( $gmt_date ) ); - - if ( 'created' === $this->get_event() && ! $resource_created ) { - $should_deliver = false; - } elseif ( 'updated' === $this->get_event() && $resource_created ) { - $should_deliver = false; - } - } - - if ( ! wc_is_webhook_valid_topic( $this->get_topic() ) ) { - $should_deliver = false; - } - - /* + /** * Let other plugins intercept deliver for some messages queue like rabbit/zeromq. + * + * @param bool $should_deliver True if the webhook should be sent, or false to not send it. + * @param WC_Webhook $this The current webhook class. + * @param mixed $arg First hook argument. */ return apply_filters( 'woocommerce_webhook_should_deliver', $should_deliver, $this, $arg ); } + /** + * Returns if webhook is active. + * + * @since 3.6.0 + * @return bool True if validation passes. + */ + private function is_active() { + return 'active' === $this->get_status(); + } + + /** + * Returns if topic is valid. + * + * @since 3.6.0 + * @return bool True if validation passes. + */ + private function is_valid_topic() { + return wc_is_webhook_valid_topic( $this->get_topic() ); + } + + /** + * Validates the criteria for certain actions. + * + * @since 3.6.0 + * @param mixed $arg First hook argument. + * @return bool True if validation passes. + */ + private function is_valid_action( $arg ) { + $current_action = current_action(); + $return = true; + + switch ( $current_action ) { + case 'delete_post': + case 'wp_trash_post': + case 'untrashed_post': + $return = $this->is_valid_post_action( $arg ); + break; + case 'delete_user': + $return = $this->is_valid_user_action( $arg ); + break; + } + + if ( 0 === strpos( $current_action, 'woocommerce_process_shop' ) || 0 === strpos( $current_action, 'woocommerce_process_product' ) ) { + $return = $this->is_valid_processing_action( $arg ); + } + + return $return; + } + + /** + * Validates post actions. + * + * @since 3.6.0 + * @param mixed $arg First hook argument. + * @return bool True if validation passes. + */ + private function is_valid_post_action( $arg ) { + // Only deliver deleted/restored event for coupons, orders, and products. + if ( isset( $GLOBALS['post_type'] ) && ! in_array( $GLOBALS['post_type'], array( 'shop_coupon', 'shop_order', 'product' ), true ) ) { + return false; + } + + // Check if is delivering for the correct resource. + if ( isset( $GLOBALS['post_type'] ) && str_replace( 'shop_', '', $GLOBALS['post_type'] ) !== $this->get_resource() ) { + return false; + } + return true; + } + + /** + * Validates user actions. + * + * @since 3.6.0 + * @param mixed $arg First hook argument. + * @return bool True if validation passes. + */ + private function is_valid_user_action( $arg ) { + $user = get_userdata( absint( $arg ) ); + + // Only deliver deleted customer event for users with customer role. + if ( ! $user || ! in_array( 'customer', (array) $user->roles, true ) ) { + return false; + } + + return true; + } + + /** + * Validates WC processing actions. + * + * @since 3.6.0 + * @param mixed $arg First hook argument. + * @return bool True if validation passes. + */ + private function is_valid_processing_action( $arg ) { + // The `woocommerce_process_shop_*` and `woocommerce_process_product_*` hooks + // fire for create and update of products and orders, so check the post + // creation date to determine the actual event. + $resource = get_post( absint( $arg ) ); + + // Drafts don't have post_date_gmt so calculate it here. + $gmt_date = get_gmt_from_date( $resource->post_date ); + + // A resource is considered created when the hook is executed within 10 seconds of the post creation date. + $resource_created = ( ( time() - 10 ) <= strtotime( $gmt_date ) ); + + if ( 'created' === $this->get_event() && ! $resource_created ) { + return false; + } elseif ( 'updated' === $this->get_event() && $resource_created ) { + return false; + } + return true; + } + + /** + * Checks the resource for this webhook is valid e.g. valid post status. + * + * @since 3.6.0 + * @param mixed $arg First hook argument. + * @return bool True if validation passes. + */ + private function is_valid_resource( $arg ) { + $resource = $this->get_resource(); + + if ( in_array( $resource, array( 'order', 'product', 'coupon' ), true ) ) { + $status = get_post_status( absint( $arg ) ); + + // Ignore auto drafts for all resources. + if ( in_array( $status, array( 'auto-draft', 'new' ), true ) ) { + return false; + } + + // Ignore standard drafts for orders. + if ( 'order' === $resource && 'draft' === $status ) { + return false; + } + } + return true; + } + /** * Deliver the webhook payload using wp_safe_remote_request(). * @@ -868,11 +964,9 @@ class WC_Webhook extends WC_Legacy_Webhook { 'delete_user', ), 'order.created' => array( - 'woocommerce_process_shop_order_meta', 'woocommerce_new_order', ), 'order.updated' => array( - 'woocommerce_process_shop_order_meta', 'woocommerce_update_order', 'woocommerce_order_refunded', ), diff --git a/includes/data-stores/abstract-wc-order-data-store-cpt.php b/includes/data-stores/abstract-wc-order-data-store-cpt.php index adcb7406d9c..d28a23b6d06 100644 --- a/includes/data-stores/abstract-wc-order-data-store-cpt.php +++ b/includes/data-stores/abstract-wc-order-data-store-cpt.php @@ -58,6 +58,9 @@ abstract class Abstract_WC_Order_Data_Store_CPT extends WC_Data_Store_WP impleme $order->set_date_created( current_time( 'timestamp', true ) ); $order->set_currency( $order->get_currency() ? $order->get_currency() : get_woocommerce_currency() ); + $raw_status = $order->get_status( 'edit' ) ? $order->get_status( 'edit' ) : apply_filters( 'woocommerce_default_order_status', 'pending' ); + $status = wc_is_order_status( 'wc-' . $raw_status ) ? 'wc-' . $raw_status : $raw_status; + $id = wp_insert_post( apply_filters( 'woocommerce_new_order_data', @@ -65,7 +68,7 @@ abstract class Abstract_WC_Order_Data_Store_CPT extends WC_Data_Store_WP impleme 'post_date' => gmdate( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getOffsetTimestamp() ), 'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ), 'post_type' => $order->get_type( 'edit' ), - 'post_status' => 'wc-' . ( $order->get_status( 'edit' ) ? $order->get_status( 'edit' ) : apply_filters( 'woocommerce_default_order_status', 'pending' ) ), + 'post_status' => $status, 'ping_status' => 'closed', 'post_author' => 1, 'post_title' => $this->get_post_title(), @@ -73,7 +76,8 @@ abstract class Abstract_WC_Order_Data_Store_CPT extends WC_Data_Store_WP impleme 'post_parent' => $order->get_parent_id( 'edit' ), 'post_excerpt' => $this->get_post_excerpt( $order ), ) - ), true + ), + true ); if ( $id && ! is_wp_error( $id ) ) { @@ -119,7 +123,7 @@ abstract class Abstract_WC_Order_Data_Store_CPT extends WC_Data_Store_WP impleme * stored. @todo When meta is flattened, handle this during migration. */ if ( version_compare( $order->get_version( 'edit' ), '2.3.7', '<' ) && $order->get_prices_include_tax( 'edit' ) ) { - $order->set_discount_total( (double) get_post_meta( $order->get_id(), '_cart_discount', true ) - (double) get_post_meta( $order->get_id(), '_cart_discount_tax', true ) ); + $order->set_discount_total( (float) get_post_meta( $order->get_id(), '_cart_discount', true ) - (float) get_post_meta( $order->get_id(), '_cart_discount_tax', true ) ); } } @@ -140,10 +144,12 @@ abstract class Abstract_WC_Order_Data_Store_CPT extends WC_Data_Store_WP impleme // Only update the post when the post data changes. if ( array_intersect( array( 'date_created', 'date_modified', 'status', 'parent_id', 'post_excerpt' ), array_keys( $changes ) ) ) { + $raw_status = $order->get_status( 'edit' ) ? $order->get_status( 'edit' ) : apply_filters( 'woocommerce_default_order_status', 'pending' ); + $status = wc_is_order_status( 'wc-' . $raw_status ) ? 'wc-' . $raw_status : $raw_status; $post_data = array( 'post_date' => gmdate( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getOffsetTimestamp() ), 'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ), - 'post_status' => 'wc-' . ( $order->get_status( 'edit' ) ? $order->get_status( 'edit' ) : apply_filters( 'woocommerce_default_order_status', 'pending' ) ), + 'post_status' => $status, 'post_parent' => $order->get_parent_id(), 'post_excerpt' => $this->get_post_excerpt( $order ), 'post_modified' => isset( $changes['date_modified'] ) ? gmdate( 'Y-m-d H:i:s', $order->get_date_modified( 'edit' )->getOffsetTimestamp() ) : current_time( 'mysql' ), diff --git a/includes/data-stores/class-wc-order-data-store-cpt.php b/includes/data-stores/class-wc-order-data-store-cpt.php index 1f4dee00ad0..a73d10f27c9 100644 --- a/includes/data-stores/class-wc-order-data-store-cpt.php +++ b/includes/data-stores/class-wc-order-data-store-cpt.php @@ -156,10 +156,20 @@ class WC_Order_Data_Store_CPT extends Abstract_WC_Order_Data_Store_CPT implement $order->set_date_paid( $order->get_date_created( 'edit' ) ); } + // Also grab the current status so we can compare. + $previous_status = get_post_status( $order->get_id() ); + // Update the order. parent::update( $order ); - do_action( 'woocommerce_update_order', $order->get_id() ); + // Fire a hook depending on the status - this should be considered a creation if it was previously draft status. + $new_status = $order->get_status( 'edit' ); + + if ( $new_status !== $previous_status && in_array( $previous_status, array( 'new', 'auto-draft', 'draft' ), true ) ) { + do_action( 'woocommerce_new_order', $order->get_id() ); + } else { + do_action( 'woocommerce_update_order', $order->get_id() ); + } } /** @@ -482,8 +492,10 @@ class WC_Order_Data_Store_CPT extends Abstract_WC_Order_Data_Store_CPT implement * @var array */ $search_fields = array_map( - 'wc_clean', apply_filters( - 'woocommerce_shop_order_search_fields', array( + 'wc_clean', + apply_filters( + 'woocommerce_shop_order_search_fields', + array( '_billing_address_index', '_shipping_address_index', '_billing_last_name', diff --git a/tests/unit-tests/webhooks/crud.php b/tests/unit-tests/webhooks/crud.php index 46d5b6fc867..4c8675c560e 100644 --- a/tests/unit-tests/webhooks/crud.php +++ b/tests/unit-tests/webhooks/crud.php @@ -1,15 +1,18 @@ save(); $this->assertEquals( $id, $object->get_id() ); @@ -19,7 +22,7 @@ class WC_Tests_CRUD_Webhooks extends WC_Unit_Test_Case { /** * Test: get_data */ - function test_get_data() { + public function test_get_data() { $object = new WC_Webhook(); $this->assertInternalType( 'array', $object->get_data() ); } @@ -27,7 +30,7 @@ class WC_Tests_CRUD_Webhooks extends WC_Unit_Test_Case { /** * Test: get_name */ - function test_get_name() { + public function test_get_name() { $object = new WC_Webhook(); $expected = 'test'; $object->set_name( $expected ); @@ -37,7 +40,7 @@ class WC_Tests_CRUD_Webhooks extends WC_Unit_Test_Case { /** * Test: get_date_created */ - function test_get_date_created() { + public function test_get_date_created() { $object = new WC_Webhook(); $object->set_date_created( '2016-12-12' ); $this->assertEquals( '1481500800', $object->get_date_created()->getOffsetTimestamp() ); @@ -49,7 +52,7 @@ class WC_Tests_CRUD_Webhooks extends WC_Unit_Test_Case { /** * Test: get_date_modified */ - function test_get_date_modified() { + public function test_get_date_modified() { $object = new WC_Webhook(); $object->set_date_modified( '2016-12-12' ); $this->assertEquals( '1481500800', $object->get_date_modified()->getOffsetTimestamp() ); @@ -61,8 +64,8 @@ class WC_Tests_CRUD_Webhooks extends WC_Unit_Test_Case { /** * Test: get_status */ - function test_get_status() { - $object = new WC_Webhook(); + public function test_get_status() { + $object = new WC_Webhook(); $this->assertEquals( 'disabled', $object->get_status() ); $expected = 'active'; @@ -73,7 +76,7 @@ class WC_Tests_CRUD_Webhooks extends WC_Unit_Test_Case { /** * Test: get_secret */ - function test_get_secret() { + public function test_get_secret() { $object = new WC_Webhook(); $expected = 'secret'; $object->set_secret( $expected ); @@ -83,7 +86,7 @@ class WC_Tests_CRUD_Webhooks extends WC_Unit_Test_Case { /** * Test: get_topic */ - function test_get_topic() { + public function test_get_topic() { $object = new WC_Webhook(); $expected = 'order.created'; $object->set_topic( $expected ); @@ -93,7 +96,7 @@ class WC_Tests_CRUD_Webhooks extends WC_Unit_Test_Case { /** * Test: get_delivery_url */ - function test_get_delivery_url() { + public function test_get_delivery_url() { $object = new WC_Webhook(); $expected = 'https://woocommerce.com'; $object->set_delivery_url( $expected ); @@ -103,7 +106,7 @@ class WC_Tests_CRUD_Webhooks extends WC_Unit_Test_Case { /** * Test: get_user_id */ - function test_get_user_id() { + public function test_get_user_id() { $object = new WC_Webhook(); $expected = 1; $object->set_user_id( $expected ); @@ -113,7 +116,7 @@ class WC_Tests_CRUD_Webhooks extends WC_Unit_Test_Case { /** * Test: get_api_version */ - function test_get_api_version() { + public function test_get_api_version() { $object = new WC_Webhook(); $expected = 'wp_api_v2'; $object->set_api_version( $expected ); @@ -123,7 +126,7 @@ class WC_Tests_CRUD_Webhooks extends WC_Unit_Test_Case { /** * Test: get_failure_count */ - function test_get_failure_count() { + public function test_get_failure_count() { $object = new WC_Webhook(); $expected = 1; $object->set_failure_count( $expected ); @@ -133,7 +136,7 @@ class WC_Tests_CRUD_Webhooks extends WC_Unit_Test_Case { /** * Test: get_pending_delivery */ - function test_get_pending_delivery() { + public function test_get_pending_delivery() { $object = new WC_Webhook(); $expected = true; $object->set_pending_delivery( $expected ); @@ -143,11 +146,10 @@ class WC_Tests_CRUD_Webhooks extends WC_Unit_Test_Case { /** * Test: get_hooks */ - function test_get_hooks() { + public function test_get_hooks() { $object = new WC_Webhook(); $object->set_topic( 'order.created' ); $expected = array( - 'woocommerce_process_shop_order_meta', 'woocommerce_new_order', ); $this->assertEquals( $expected, $object->get_hooks() ); @@ -156,7 +158,7 @@ class WC_Tests_CRUD_Webhooks extends WC_Unit_Test_Case { /** * Test: get_resource */ - function test_get_resource() { + public function test_get_resource() { $object = new WC_Webhook(); $object->set_topic( 'order.created' ); $this->assertEquals( 'order', $object->get_resource() ); @@ -165,7 +167,7 @@ class WC_Tests_CRUD_Webhooks extends WC_Unit_Test_Case { /** * Test: get_event */ - function test_get_event() { + public function test_get_event() { $object = new WC_Webhook(); $object->set_topic( 'order.created' ); $this->assertEquals( 'created', $object->get_event() ); @@ -174,7 +176,7 @@ class WC_Tests_CRUD_Webhooks extends WC_Unit_Test_Case { /** * Test: get_i18n_status */ - function test_get_i18n_status() { + public function test_get_i18n_status() { $object = new WC_Webhook(); $object->set_status( 'active' ); $this->assertEquals( 'Active', $object->get_i18n_status() ); @@ -183,7 +185,7 @@ class WC_Tests_CRUD_Webhooks extends WC_Unit_Test_Case { /** * Test: generate_signature */ - function test_generate_signature() { + public function test_generate_signature() { $object = new WC_Webhook(); $this->assertEquals( 'GBDo00G55h6IiV+6CxqivQPLbI//KzaOZm747971tPs=', $object->generate_signature( 'secret' ) ); }