Admin redirects for HPOS URLs (#35463)

* Redirect COT/HPOS admin requests to the corresponding CPT screen, if COT is not authoritative.

* Tidy handling of query parameters.

* Linting fixes.
This commit is contained in:
Barry Hughes 2022-11-03 03:29:15 -07:00 committed by GitHub
parent c561d7941d
commit 925432aebe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 266 additions and 0 deletions

View File

@ -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.

View File

@ -6,6 +6,7 @@
* @version 2.5.0 * @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\Admin\Orders\PageController as Custom_Orders_PageController;
use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController; use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;
use Automattic\WooCommerce\Admin\Features\Features; 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() ) { 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 = new Custom_Orders_PageController();
$this->orders_page_controller->setup(); $this->orders_page_controller->setup();
} else {
wc_get_container()->get( COTRedirectionController::class )->setup();
} }
} }

View File

@ -0,0 +1,75 @@
<?php
namespace Automattic\WooCommerce\Internal\Admin\Orders;
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
/**
* When Custom Order Tables are not the default order store (ie, posts are authoritative), we should take care of
* redirecting requests for the order editor and order admin list table to the equivalent posts-table screens.
*
* If the redirect logic is problematic, it can be unhooked using code like the following example:
*
* remove_action(
* 'admin_page_access_denied',
* array( wc_get_container()->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;
}
}
}

View File

@ -5,6 +5,7 @@
namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders; 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\Edit;
use Automattic\WooCommerce\Internal\Admin\Orders\ListTable; use Automattic\WooCommerce\Internal\Admin\Orders\ListTable;
use Automattic\WooCommerce\Internal\Admin\Orders\PageController; use Automattic\WooCommerce\Internal\Admin\Orders\PageController;
@ -21,6 +22,7 @@ class OrderAdminServiceProvider extends AbstractServiceProvider {
* @var string[] * @var string[]
*/ */
protected $provides = array( protected $provides = array(
COTRedirectionController::class,
PageController::class, PageController::class,
Edit::class, Edit::class,
ListTable::class, ListTable::class,
@ -32,6 +34,7 @@ class OrderAdminServiceProvider extends AbstractServiceProvider {
* @return void * @return void
*/ */
public function register() { public function register() {
$this->share( COTRedirectionController::class );
$this->share( PageController::class ); $this->share( PageController::class );
$this->share( Edit::class )->addArgument( PageController::class ); $this->share( Edit::class )->addArgument( PageController::class );
$this->share( ListTable::class )->addArgument( PageController::class ); $this->share( ListTable::class )->addArgument( PageController::class );

View File

@ -0,0 +1,181 @@
<?php
use Automattic\WooCommerce\Internal\Admin\Orders\COTRedirectionController;
/**
* Describes our redirection logic covering HPOS admin screens when Custom Order Tables are not authoritative.
*/
class COTRedirectionControllerTest extends WC_Unit_Test_Case {
/**
* @var COTRedirectionController
*/
private $sut;
/**
* Holds the URL of the last attempted redirect.
*
* @var string
*/
private $redirected_to = '';
/**
* Setup our SUT and start listening for redirects.
*
* @return void
*/
public function setUp(): void {
parent::setUp();
$this->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.'
);
}
}