diff --git a/plugins/woocommerce/changelog/fix-35074-hpos-cpt-admin-redirects b/plugins/woocommerce/changelog/fix-35074-hpos-cpt-admin-redirects new file mode 100644 index 00000000000..9a4fa61de72 --- /dev/null +++ b/plugins/woocommerce/changelog/fix-35074-hpos-cpt-admin-redirects @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +When custom order tables are not authoritative, admin UI requests will be redirected to the matching legacy order screen as appropriate. diff --git a/plugins/woocommerce/includes/admin/class-wc-admin-menus.php b/plugins/woocommerce/includes/admin/class-wc-admin-menus.php index 191d65ca586..8b1491042b7 100644 --- a/plugins/woocommerce/includes/admin/class-wc-admin-menus.php +++ b/plugins/woocommerce/includes/admin/class-wc-admin-menus.php @@ -6,6 +6,7 @@ * @version 2.5.0 */ +use Automattic\WooCommerce\Internal\Admin\Orders\COTRedirectionController; use Automattic\WooCommerce\Internal\Admin\Orders\PageController as Custom_Orders_PageController; use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController; use Automattic\WooCommerce\Admin\Features\Features; @@ -316,6 +317,8 @@ class WC_Admin_Menus { if ( wc_get_container()->get( CustomOrdersTableController::class )->custom_orders_table_usage_is_enabled() ) { $this->orders_page_controller = new Custom_Orders_PageController(); $this->orders_page_controller->setup(); + } else { + wc_get_container()->get( COTRedirectionController::class )->setup(); } } diff --git a/plugins/woocommerce/src/Internal/Admin/Orders/COTRedirectionController.php b/plugins/woocommerce/src/Internal/Admin/Orders/COTRedirectionController.php new file mode 100644 index 00000000000..f307bad74d7 --- /dev/null +++ b/plugins/woocommerce/src/Internal/Admin/Orders/COTRedirectionController.php @@ -0,0 +1,75 @@ +get( COTRedirectionController::class ), 'handle_hpos_admin_requests' ) + * ); + */ +class COTRedirectionController { + use AccessiblePrivateMethods; + + /** + * Add hooks needed to perform our magic. + */ + public function setup(): void { + // Only take action in cases where access to the admin screen would otherwise be denied. + self::add_action( 'admin_page_access_denied', array( $this, 'handle_hpos_admin_requests' ) ); + } + + /** + * Listen for denied admin requests and, if they appear to relate to HPOS admin screens, potentially + * redirect the user to the equivalent CPT-driven screens. + * + * @param array|null $query_params The query parameters to use when determining the redirect. If not provided, the $_GET superglobal will be used. + */ + private function handle_hpos_admin_requests( $query_params = null ) { + $query_params = is_array( $query_params ) ? $query_params : $_GET; + + if ( ! isset( $query_params['page'] ) || 'wc-orders' !== $query_params['page'] ) { + return; + } + + $params = wp_unslash( $query_params ); + $action = $params['action'] ?? ''; + unset( $params['page'] ); + + if ( 'edit' === $action && isset( $params['id'] ) ) { + $params['post'] = $params['id']; + unset( $params['id'] ); + $new_url = add_query_arg( $params, get_admin_url( null, 'post.php' ) ); + } elseif ( 'new' === $action ) { + unset( $params['action'] ); + $params['post_type'] = 'shop_order'; + $new_url = add_query_arg( $params, get_admin_url( null, 'post-new.php' ) ); + } else { + // If nonce parameters are present and valid, rebuild them for the CPT admin list table. + if ( isset( $params['_wpnonce'] ) && check_admin_referer( 'bulk-orders' ) ) { + $params['_wp_http_referer'] = get_admin_url( null, 'edit.php?post_type=shop_order' ); + $params['_wpnonce'] = wp_create_nonce( 'bulk-posts' ); + } + + // If an `order` array parameter is present, rename as `post`. + if ( isset( $params['order'] ) && is_array( $params['order'] ) ) { + $params['post'] = $params['order']; + unset( $params['order'] ); + } + + $params['post_type'] = 'shop_order'; + $new_url = add_query_arg( $params, get_admin_url( null, 'edit.php' ) ); + } + + if ( ! empty( $new_url ) && wp_safe_redirect( $new_url, 301 ) ) { + exit; + } + } +} diff --git a/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/OrderAdminServiceProvider.php b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/OrderAdminServiceProvider.php index ee473150be7..bbdd2a83d43 100644 --- a/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/OrderAdminServiceProvider.php +++ b/plugins/woocommerce/src/Internal/DependencyManagement/ServiceProviders/OrderAdminServiceProvider.php @@ -5,6 +5,7 @@ namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders; +use Automattic\WooCommerce\Internal\Admin\Orders\COTRedirectionController; use Automattic\WooCommerce\Internal\Admin\Orders\Edit; use Automattic\WooCommerce\Internal\Admin\Orders\ListTable; use Automattic\WooCommerce\Internal\Admin\Orders\PageController; @@ -21,6 +22,7 @@ class OrderAdminServiceProvider extends AbstractServiceProvider { * @var string[] */ protected $provides = array( + COTRedirectionController::class, PageController::class, Edit::class, ListTable::class, @@ -32,6 +34,7 @@ class OrderAdminServiceProvider extends AbstractServiceProvider { * @return void */ public function register() { + $this->share( COTRedirectionController::class ); $this->share( PageController::class ); $this->share( Edit::class )->addArgument( PageController::class ); $this->share( ListTable::class )->addArgument( PageController::class ); diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/Orders/COTRedirectionControllerTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/Orders/COTRedirectionControllerTest.php new file mode 100644 index 00000000000..f15a085ff47 --- /dev/null +++ b/plugins/woocommerce/tests/php/src/Internal/Admin/Orders/COTRedirectionControllerTest.php @@ -0,0 +1,181 @@ +sut = new COTRedirectionController(); + $this->sut->setup(); + $this->redirected_to = ''; + + add_filter( 'wp_redirect', array( $this, 'watch_and_anull_redirects' ) ); + } + + /** + * Remove our redirect listener. + * + * @return void + */ + public function tearDown(): void { + parent::tearDown(); + remove_filter( 'wp_redirect', array( $this, 'watch_and_anull_redirects' ) ); + } + + /** + * Captures the attempted redirect location, and stops the redirect from taking place. + * + * @param string $url Redirect location. + * + * @return null + */ + public function watch_and_anull_redirects( string $url ) { + $this->redirected_to = $url; + return null; + } + + /** + * Supplies the URL of the last attempted redirect, then resets ready for the next test. + * + * @return string + */ + private function get_redirect_attempt(): string { + $return = $this->redirected_to; + $this->redirected_to = ''; + return $return; + } + + /** + * Test that redirects only occur in relation to HPOS admin screen requests. + * + * @return void + */ + public function test_redirects_only_impact_hpos_admin_requests() { + $this->sut->handle_hpos_admin_requests( array( 'page' => 'wc-orders' ) ); + $this->assertNotEmpty( $this->get_redirect_attempt(), 'A redirect was attempted in relation to an HPOS admin request.' ); + + $this->sut->handle_hpos_admin_requests( array( 'page' => 'foo' ) ); + $this->assertEmpty( $this->get_redirect_attempt(), 'A redirect was not attempted in relation to a non-HPOS admin request.' ); + } + + /** + * Test order editor redirects work (in relation to creating new orders). + * + * @return void + */ + public function test_redirects_to_the_new_order_screen(): void { + $this->sut->handle_hpos_admin_requests( + array( + 'action' => 'new', + 'page' => 'wc-orders', + ) + ); + + $this->assertStringContainsString( + '/wp-admin/post-new.php?post_type=shop_order', + $this->get_redirect_attempt(), + 'Attempts to access the new order page (HPOS) are successfully redirected to the new order page (CPT).' + ); + } + + /** + * Test order editor redirects work (in relation to existing orders). + * + * @return void + */ + public function test_redirects_to_the_order_editor_screen(): void { + $this->sut->handle_hpos_admin_requests( + array( + 'action' => 'edit', + 'id' => 12345, + 'page' => 'wc-orders', + ) + ); + + $redirect_url = $this->get_redirect_attempt(); + $redirect_base = wp_parse_url( $redirect_url, PHP_URL_PATH ); + parse_str( wp_parse_url( $redirect_url, PHP_URL_QUERY ), $redirect_query ); + + $this->assertStringContainsString( + '/post.php', + $redirect_base, + 'Confirm order editor redirects go to the expected WordPress admin controller.' + ); + + $this->assertEquals( + '12345', + $redirect_query['post'], + 'Confirm order editor redirects maintain the correct order ID.' + ); + } + + /** + * Tests order list table redirects work. + * + * @return void + */ + public function test_redirects_to_the_order_admin_list_screen(): void { + $this->sut->handle_hpos_admin_requests( + array( + 'arbitrary' => '3pd-integration', + 'order' => array( + 123, + 456, + ), + 'page' => 'wc-orders', + ) + ); + + $redirect_url = $this->get_redirect_attempt(); + $redirect_base = wp_parse_url( $redirect_url, PHP_URL_PATH ); + parse_str( wp_parse_url( $redirect_url, PHP_URL_QUERY ), $redirect_query ); + + $this->assertStringContainsString( + '/edit.php', + $redirect_base, + 'Confirm order list table redirects go to the expected WordPress admin controller.' + ); + + $this->assertEquals( + array( + '123', + '456', + ), + $redirect_query['post'], + 'Confirm order list table redirects maintain a list of order IDs for bulk action requests (if one was passed).' + ); + + $this->assertEquals( + 'shop_order', + $redirect_query['post_type'], + 'Confirm order list table redirects reference the correct custom post type.' + ); + + $this->assertEquals( + '3pd-integration', + $redirect_query['arbitrary'], + 'Confirm that arbitrary query parameters are also passed across via order list table redirects.' + ); + } +}