From e4f3273fb54b8580af08b3d88711b5dbd1f55b88 Mon Sep 17 00:00:00 2001 From: Vedanshu Jain Date: Wed, 14 Jun 2023 19:17:22 +0530 Subject: [PATCH] Add meta boxes for custom taxonomies in order edit screens (#38676) --- plugins/woocommerce/changelog/fix-38560 | 4 + .../src/Internal/Admin/Orders/Edit.php | 27 ++++ .../Orders/MetaBoxes/TaxonomiesMetaBox.php | 147 ++++++++++++++++++ .../Orders/OrdersTableDataStore.php | 78 ++++++++++ .../OrderAdminServiceProvider.php | 4 + .../class-wc-abstract-order-test.php | 31 +++- 6 files changed, 288 insertions(+), 3 deletions(-) create mode 100644 plugins/woocommerce/changelog/fix-38560 create mode 100644 plugins/woocommerce/src/Internal/Admin/Orders/MetaBoxes/TaxonomiesMetaBox.php diff --git a/plugins/woocommerce/changelog/fix-38560 b/plugins/woocommerce/changelog/fix-38560 new file mode 100644 index 00000000000..57c57921ada --- /dev/null +++ b/plugins/woocommerce/changelog/fix-38560 @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Add support for taxonomy meta boxes in HPOS order edit screen. diff --git a/plugins/woocommerce/src/Internal/Admin/Orders/Edit.php b/plugins/woocommerce/src/Internal/Admin/Orders/Edit.php index 9ae7e049508..f08f8acec36 100644 --- a/plugins/woocommerce/src/Internal/Admin/Orders/Edit.php +++ b/plugins/woocommerce/src/Internal/Admin/Orders/Edit.php @@ -6,6 +6,7 @@ namespace Automattic\WooCommerce\Internal\Admin\Orders; use Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes\CustomMetaBox; +use Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes\TaxonomiesMetaBox; /** * Class Edit. @@ -26,6 +27,13 @@ class Edit { */ private $custom_meta_box; + /** + * Instance of the TaxonomiesMetaBox class. Used to render meta box for taxonomies. + * + * @var TaxonomiesMetaBox + */ + private $taxonomies_meta_box; + /** * Instance of WC_Order to be used in metaboxes. * @@ -110,10 +118,16 @@ class Edit { if ( ! isset( $this->custom_meta_box ) ) { $this->custom_meta_box = wc_get_container()->get( CustomMetaBox::class ); } + + if ( ! isset( $this->taxonomies_meta_box ) ) { + $this->taxonomies_meta_box = wc_get_container()->get( TaxonomiesMetaBox::class ); + } + $this->add_save_meta_boxes(); $this->handle_order_update(); $this->add_order_meta_boxes( $this->screen_id, __( 'Order', 'woocommerce' ) ); $this->add_order_specific_meta_box(); + $this->add_order_taxonomies_meta_box(); /** * From wp-admin/includes/meta-boxes.php. @@ -159,6 +173,15 @@ class Edit { ); } + /** + * Render custom meta box. + * + * @return void + */ + private function add_order_taxonomies_meta_box() { + $this->taxonomies_meta_box->add_taxonomies_meta_boxes( $this->screen_id, $this->order->get_type() ); + } + /** * Takes care of updating order data. Fires action that metaboxes can hook to for order data updating. * @@ -176,6 +199,10 @@ class Edit { check_admin_referer( $this->get_order_edit_nonce_action() ); + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized later on by taxonomies_meta_box object. + $taxonomy_input = isset( $_POST['tax_input'] ) ? wp_unslash( $_POST['tax_input'] ) : null; + $this->taxonomies_meta_box->save_taxonomies( $this->order, $taxonomy_input ); + /** * Save meta for shop order. * diff --git a/plugins/woocommerce/src/Internal/Admin/Orders/MetaBoxes/TaxonomiesMetaBox.php b/plugins/woocommerce/src/Internal/Admin/Orders/MetaBoxes/TaxonomiesMetaBox.php new file mode 100644 index 00000000000..f932371aad2 --- /dev/null +++ b/plugins/woocommerce/src/Internal/Admin/Orders/MetaBoxes/TaxonomiesMetaBox.php @@ -0,0 +1,147 @@ +orders_table_data_store = $orders_table_data_store; + } + + /** + * Registers meta boxes to be rendered in order edit screen for taxonomies. + * + * Note: This is re-implementation of part of WP core's `register_and_do_post_meta_boxes` function. Since the code block that add meta box for taxonomies is not filterable, we have to re-implement it. + * + * @param string $screen_id Screen ID. + * @param string $order_type Order type to register meta boxes for. + * + * @return void + */ + public function add_taxonomies_meta_boxes( string $screen_id, string $order_type ) { + include_once ABSPATH . 'wp-admin/includes/meta-boxes.php'; + $taxonomies = get_object_taxonomies( $order_type ); + // All taxonomies. + foreach ( $taxonomies as $tax_name ) { + $taxonomy = get_taxonomy( $tax_name ); + if ( ! $taxonomy->show_ui || false === $taxonomy->meta_box_cb ) { + continue; + } + + if ( 'post_categories_meta_box' === $taxonomy->meta_box_cb ) { + $taxonomy->meta_box_cb = array( $this, 'order_categories_meta_box' ); + } + + if ( 'post_tags_meta_box' === $taxonomy->meta_box_cb ) { + $taxonomy->meta_box_cb = array( $this, 'order_tags_meta_box' ); + } + + $label = $taxonomy->labels->name; + + if ( ! is_taxonomy_hierarchical( $tax_name ) ) { + $tax_meta_box_id = 'tagsdiv-' . $tax_name; + } else { + $tax_meta_box_id = $tax_name . 'div'; + } + + add_meta_box( + $tax_meta_box_id, + $label, + $taxonomy->meta_box_cb, + $screen_id, + 'side', + 'core', + array( + 'taxonomy' => $tax_name, + '__back_compat_meta_box' => true, + ) + ); + } + } + + /** + * Save handler for taxonomy data. + * + * @param \WC_Abstract_Order $order Order object. + * @param array|null $taxonomy_input Taxonomy input passed from input. + */ + public function save_taxonomies( \WC_Abstract_Order $order, $taxonomy_input ) { + if ( ! isset( $taxonomy_input ) ) { + return; + } + + $sanitized_tax_input = $this->sanitize_tax_input( $taxonomy_input ); + + $sanitized_tax_input = $this->orders_table_data_store->init_default_taxonomies( $order, $sanitized_tax_input ); + $this->orders_table_data_store->set_custom_taxonomies( $order, $sanitized_tax_input ); + } + + /** + * Sanitize taxonomy input by calling sanitize callbacks for each registered taxonomy. + * + * @param array|null $taxonomy_data Nonce verified taxonomy input. + * + * @return array Sanitized taxonomy input. + */ + private function sanitize_tax_input( $taxonomy_data ) : array { + $sanitized_tax_input = array(); + if ( ! is_array( $taxonomy_data ) ) { + return $sanitized_tax_input; + } + + // Convert taxonomy input to term IDs, to avoid ambiguity. + foreach ( $taxonomy_data as $taxonomy => $terms ) { + $tax_object = get_taxonomy( $taxonomy ); + if ( $tax_object && isset( $tax_object->meta_box_sanitize_cb ) ) { + $sanitized_tax_input[ $taxonomy ] = call_user_func_array( $tax_object->meta_box_sanitize_cb, array( $taxonomy, $terms ) ); + } + } + + return $sanitized_tax_input; + } + + /** + * Add the categories meta box to the order screen. This is just a wrapper around the post_categories_meta_box. + * + * @param \WC_Abstract_Order $order Order object. + * @param array $box Meta box args. + * + * @return void + */ + public function order_categories_meta_box( $order, $box ) { + $post = get_post( $order->get_id() ); + post_categories_meta_box( $post, $box ); + } + + /** + * Add the tags meta box to the order screen. This is just a wrapper around the post_tags_meta_box. + * + * @param \WC_Abstract_Order $order Order object. + * @param array $box Meta box args. + * + * @return void + */ + public function order_tags_meta_box( $order, $box ) { + $post = get_post( $order->get_id() ); + post_tags_meta_box( $post, $box ); + } +} diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php index 2d25de0d9b3..6e7adfdfc66 100644 --- a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php +++ b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php @@ -1641,6 +1641,84 @@ FROM $order_meta_table $changes = $order->get_changes(); $this->update_address_index_meta( $order, $changes ); + $default_taxonomies = $this->init_default_taxonomies( $order, array() ); + $this->set_custom_taxonomies( $order, $default_taxonomies ); + } + + /** + * Set default taxonomies for the order. + * + * Note: This is re-implementation of part of WP core's `wp_insert_post` function. Since the code block that set default taxonomies is not filterable, we have to re-implement it. + * + * @param \WC_Abstract_Order $order Order object. + * @param array $sanitized_tax_input Sanitized taxonomy input. + * + * @return array Sanitized tax input with default taxonomies. + */ + public function init_default_taxonomies( \WC_Abstract_Order $order, array $sanitized_tax_input ) { + if ( 'auto-draft' === $order->get_status() ) { + return $sanitized_tax_input; + } + + foreach ( get_object_taxonomies( $order->get_type(), 'object' ) as $taxonomy => $tax_object ) { + if ( empty( $tax_object->default_term ) ) { + return $sanitized_tax_input; + } + + // Filter out empty terms. + if ( isset( $sanitized_tax_input[ $taxonomy ] ) && is_array( $sanitized_tax_input[ $taxonomy ] ) ) { + $sanitized_tax_input[ $taxonomy ] = array_filter( $sanitized_tax_input[ $taxonomy ] ); + } + + // Passed custom taxonomy list overwrites the existing list if not empty. + $terms = wp_get_object_terms( $order->get_id(), $taxonomy, array( 'fields' => 'ids' ) ); + if ( ! empty( $terms ) && empty( $sanitized_tax_input[ $taxonomy ] ) ) { + $sanitized_tax_input[ $taxonomy ] = $terms; + } + + if ( empty( $sanitized_tax_input[ $taxonomy ] ) ) { + $default_term_id = get_option( 'default_term_' . $taxonomy ); + if ( ! empty( $default_term_id ) ) { + $sanitized_tax_input[ $taxonomy ] = array( (int) $default_term_id ); + } + } + } + return $sanitized_tax_input; + } + + /** + * Set custom taxonomies for the order. + * + * Note: This is re-implementation of part of WP core's `wp_insert_post` function. Since the code block that set custom taxonomies is not filterable, we have to re-implement it. + * + * @param \WC_Abstract_Order $order Order object. + * @param array $sanitized_tax_input Sanitized taxonomy input. + * + * @return void + */ + public function set_custom_taxonomies( \WC_Abstract_Order $order, array $sanitized_tax_input ) { + if ( empty( $sanitized_tax_input ) ) { + return; + } + + foreach ( $sanitized_tax_input as $taxonomy => $tags ) { + $taxonomy_obj = get_taxonomy( $taxonomy ); + + if ( ! $taxonomy_obj ) { + /* translators: %s: Taxonomy name. */ + _doing_it_wrong( __FUNCTION__, esc_html( sprintf( __( 'Invalid taxonomy: %s.', 'woocommerce' ), $taxonomy ) ), '7.9.0' ); + continue; + } + + // array = hierarchical, string = non-hierarchical. + if ( is_array( $tags ) ) { + $tags = array_filter( $tags ); + } + + if ( current_user_can( $taxonomy_obj->cap->assign_terms ) ) { + wp_set_post_terms( $order->get_id(), $tags, $taxonomy ); + } + } } /** diff --git a/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/OrderAdminServiceProvider.php b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/OrderAdminServiceProvider.php index b78acdaf918..e916f54561d 100644 --- a/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/OrderAdminServiceProvider.php +++ b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/OrderAdminServiceProvider.php @@ -9,7 +9,9 @@ use Automattic\WooCommerce\Internal\Admin\Orders\COTRedirectionController; use Automattic\WooCommerce\Internal\Admin\Orders\Edit; use Automattic\WooCommerce\Internal\Admin\Orders\EditLock; use Automattic\WooCommerce\Internal\Admin\Orders\ListTable; +use Automattic\WooCommerce\Internal\Admin\Orders\MetaBoxes\TaxonomiesMetaBox; use Automattic\WooCommerce\Internal\Admin\Orders\PageController; +use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore; use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider; /** @@ -28,6 +30,7 @@ class OrderAdminServiceProvider extends AbstractServiceProvider { Edit::class, ListTable::class, EditLock::class, + TaxonomiesMetaBox::class, ); /** @@ -41,5 +44,6 @@ class OrderAdminServiceProvider extends AbstractServiceProvider { $this->share( Edit::class )->addArgument( PageController::class ); $this->share( ListTable::class )->addArgument( PageController::class ); $this->share( EditLock::class ); + $this->share( TaxonomiesMetaBox::class )->addArgument( OrdersTableDataStore::class ); } } diff --git a/plugins/woocommerce/tests/php/includes/abstracts/class-wc-abstract-order-test.php b/plugins/woocommerce/tests/php/includes/abstracts/class-wc-abstract-order-test.php index b164ec95a5f..f966aa2e4ca 100644 --- a/plugins/woocommerce/tests/php/includes/abstracts/class-wc-abstract-order-test.php +++ b/plugins/woocommerce/tests/php/includes/abstracts/class-wc-abstract-order-test.php @@ -190,7 +190,7 @@ class WC_Abstract_Order_Test extends WC_Unit_Test_Case { */ public function test_apply_coupon_across_status() { $coupon_code = 'coupon_test_count_across_status'; - $coupon = WC_Helper_Coupon::create_coupon( $coupon_code ); + $coupon = WC_Helper_Coupon::create_coupon( $coupon_code ); $this->assertEquals( 0, $coupon->get_usage_count() ); $order = WC_Helper_Order::create_order(); @@ -253,8 +253,8 @@ class WC_Abstract_Order_Test extends WC_Unit_Test_Case { */ public function test_apply_coupon_stores_meta_data() { $coupon_code = 'coupon_test_meta_data'; - $coupon = WC_Helper_Coupon::create_coupon( $coupon_code ); - $order = WC_Helper_Order::create_order(); + $coupon = WC_Helper_Coupon::create_coupon( $coupon_code ); + $order = WC_Helper_Order::create_order(); $order->set_status( 'processing' ); $order->save(); $order->apply_coupon( $coupon_code ); @@ -324,4 +324,29 @@ class WC_Abstract_Order_Test extends WC_Unit_Test_Case { $order = wc_get_order( $order->get_id() ); $this->assertInstanceOf( Automattic\WooCommerce\Admin\Overrides\Order::class, $order ); } + + /** + * @testDox When a taxonomy with a default term is set on the order, it's inserted when a new order is created. + */ + public function test_default_term_for_custom_taxonomy() { + $custom_taxonomy = register_taxonomy( + 'custom_taxonomy', + 'shop_order', + array( + 'default_term' => 'new_term', + ), + ); + + // Set user who has access to create term. + $current_user_id = get_current_user_id(); + $user = new WP_User( wp_create_user( 'test', '' ) ); + $user->set_role( 'administrator' ); + wp_set_current_user( $user->ID ); + + $order = wc_create_order(); + + wp_set_current_user( $current_user_id ); + $order_terms = wp_list_pluck( wp_get_object_terms( $order->get_id(), $custom_taxonomy->name ), 'name' ); + $this->assertContains( 'new_term', $order_terms ); + } }