Commit e889a5b3bf8 for woocommerce
commit e889a5b3bf819f03f6683c653482dae39863048e
Author: Hannah Tinkler <hannah.tinkler@gmail.com>
Date: Thu Sep 24 19:00:12 2026 +0100
Return 404 for push token writes when push notifications are disabled (#69058)
* Return 404 for push token writes when push notifications are disabled
Registering or removing a push token on a disabled store returned 403 after
the token list moved ahead of the enablement check, and the apps expect a 404
to recognise the store as unavailable. The write routes are now only
registered while the module is enabled.
* Note when the push token write routes can stop depending on enablement
* Drop the unneeded on_init call from the disabled token route tests
diff --git a/plugins/woocommerce/src/Internal/PushNotifications/Controllers/PushTokenRestController.php b/plugins/woocommerce/src/Internal/PushNotifications/Controllers/PushTokenRestController.php
index 34752d3fb14..b5d36d00a53 100644
--- a/plugins/woocommerce/src/Internal/PushNotifications/Controllers/PushTokenRestController.php
+++ b/plugins/woocommerce/src/Internal/PushNotifications/Controllers/PushTokenRestController.php
@@ -64,6 +64,14 @@ class PushTokenRestController extends RestApiControllerBase {
/**
* Register the REST API endpoints handled by this controller.
*
+ * The token list is registered whatever the module's state. The write
+ * endpoints are only registered while it is enabled, because the apps read
+ * the 404 for a missing route as push notifications being unavailable.
+ * Once the apps read that from {@see PushNotificationStatusRestController}
+ * instead, the write endpoints can be registered unconditionally again and
+ * left to their permission callbacks. Registering a second handler on an
+ * existing route adds to it.
+ *
* @since 10.6.0
*
* @return void
@@ -111,12 +119,6 @@ class PushTokenRestController extends RestApiControllerBase {
),
),
),
- array(
- 'methods' => WP_REST_Server::CREATABLE,
- 'callback' => fn ( WP_REST_Request $request ) => $this->run( $request, 'create' ),
- 'args' => $this->get_args( 'create' ),
- 'permission_callback' => array( $this, 'authorize_as_authenticated' ),
- ),
'schema' => fn () => array_merge(
$this->get_base_schema(),
array(
@@ -135,6 +137,23 @@ class PushTokenRestController extends RestApiControllerBase {
)
);
+ if ( ! wc_get_container()->get( PushNotifications::class )->should_be_enabled() ) {
+ return;
+ }
+
+ register_rest_route(
+ $this->route_namespace,
+ $this->rest_base,
+ array(
+ array(
+ 'methods' => WP_REST_Server::CREATABLE,
+ 'callback' => fn ( WP_REST_Request $request ) => $this->run( $request, 'create' ),
+ 'args' => $this->get_args( 'create' ),
+ 'permission_callback' => array( $this, 'authorize_as_authenticated' ),
+ ),
+ )
+ );
+
register_rest_route(
$this->route_namespace,
$this->rest_base . '/(?P<id>[\d]+)',
diff --git a/plugins/woocommerce/src/Internal/PushNotifications/PushNotifications.php b/plugins/woocommerce/src/Internal/PushNotifications/PushNotifications.php
index 9e7a8cf3970..048c9bd55c5 100644
--- a/plugins/woocommerce/src/Internal/PushNotifications/PushNotifications.php
+++ b/plugins/woocommerce/src/Internal/PushNotifications/PushNotifications.php
@@ -82,8 +82,8 @@ class PushNotifications {
wc_get_container()->get( UserDataCleanupService::class )->register();
// Registered ahead of the enablement check so the token list can still be
- // read on a store that has been switched off. The write routes stay gated
- // in their permission callbacks, as does everything below.
+ // read on a store that has been switched off. The controller registers the
+ // write routes only while the module is enabled, like everything below.
( new PushTokenRestController() )->register();
if ( ! $this->should_be_enabled() ) {
diff --git a/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Controllers/PushTokenRestControllerTest.php b/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Controllers/PushTokenRestControllerTest.php
index b8521bf2c64..a80465b20c7 100644
--- a/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Controllers/PushTokenRestControllerTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Controllers/PushTokenRestControllerTest.php
@@ -120,7 +120,11 @@ class PushTokenRestControllerTest extends WC_Unit_Test_Case {
public function setUp(): void {
parent::setUp();
- $this->reset_push_notifications_cache();
+ /**
+ * The write routes are only registered on an enabled store. Tests that
+ * need it disabled re-mock the connection, which also resets the cache.
+ */
+ $this->mock_jetpack_connection_manager_is_connected();
$this->controller = new PushTokenRestController();
$this->server = $this->create_rest_server_with_routes(
diff --git a/plugins/woocommerce/tests/php/src/Internal/PushNotifications/PushNotificationsTest.php b/plugins/woocommerce/tests/php/src/Internal/PushNotifications/PushNotificationsTest.php
index 6bc751d6bb8..242ac845694 100644
--- a/plugins/woocommerce/tests/php/src/Internal/PushNotifications/PushNotificationsTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/PushNotifications/PushNotificationsTest.php
@@ -19,6 +19,7 @@ use WC_Logger;
use WC_Unit_Test_Case;
use WP_Http;
use WP_REST_Request;
+use WP_REST_Response;
/**
* PushNotifications test.
@@ -262,8 +263,7 @@ class PushNotificationsTest extends WC_Unit_Test_Case {
->method( 'is_connected' )
->willReturn( false );
- $push_notifications = new PushNotifications();
- $push_notifications->on_init();
+ wc_get_container()->get( PushNotifications::class )->on_init();
$registered = $this->get_registered_rest_controllers();
@@ -329,34 +329,57 @@ class PushNotificationsTest extends WC_Unit_Test_Case {
}
/**
- * @testdox Tests that registering a token is refused while the module is disabled.
+ * @testdox Tests that registering a token returns a missing route while the module is disabled.
+ */
+ public function test_token_registration_returns_no_route_when_disabled_via_filter() {
+ $request = new WP_REST_Request( 'POST', '/wc-push-notifications/push-tokens' );
+ $request->set_param( 'token', str_repeat( 'a', 64 ) );
+ $request->set_param( 'platform', PushToken::PLATFORM_APPLE );
+ $request->set_param( 'device_uuid', 'refused-device-uuid' );
+ $request->set_param( 'origin', PushToken::ORIGIN_WOOCOMMERCE_IOS );
+ $request->set_param( 'device_locale', 'en_US' );
+
+ $response = $this->dispatch_as_shop_manager_while_disabled( $request );
+
+ $this->assertSame( WP_Http::NOT_FOUND, $response->get_status() );
+ $this->assertSame( 'rest_no_route', $response->get_data()['code'] );
+ }
+
+ /**
+ * @testdox Tests that deleting a token returns a missing route while the module is disabled.
+ */
+ public function test_token_deletion_returns_no_route_when_disabled_via_filter() {
+ $response = $this->dispatch_as_shop_manager_while_disabled(
+ new WP_REST_Request( 'DELETE', '/wc-push-notifications/push-tokens/1' )
+ );
+
+ $this->assertSame( WP_Http::NOT_FOUND, $response->get_status() );
+ $this->assertSame( 'rest_no_route', $response->get_data()['code'] );
+ }
+
+ /**
+ * Dispatches a request as a shop manager against the token routes, with the
+ * module disabled through the filter.
+ *
+ * @param WP_REST_Request $request The request to dispatch.
+ * @phpstan-param WP_REST_Request<array<string, mixed>> $request
+ * @return WP_REST_Response
*/
- public function test_token_registration_is_refused_when_disabled_via_filter() {
+ private function dispatch_as_shop_manager_while_disabled( WP_REST_Request $request ): WP_REST_Response {
add_filter( 'woocommerce_enhanced_push_notifications_disabled', '__return_true' );
try {
wp_set_current_user( self::factory()->user->create( array( 'role' => 'shop_manager' ) ) );
- ( new PushNotifications() )->on_init();
-
$server = $this->create_rest_server_with_routes(
array( array( new PushTokenRestController(), 'register_routes' ) ),
true
);
- $request = new WP_REST_Request( 'POST', '/wc-push-notifications/push-tokens' );
- $request->set_param( 'token', str_repeat( 'a', 64 ) );
- $request->set_param( 'platform', PushToken::PLATFORM_APPLE );
- $request->set_param( 'device_uuid', 'refused-device-uuid' );
- $request->set_param( 'origin', PushToken::ORIGIN_WOOCOMMERCE_IOS );
- $request->set_param( 'device_locale', 'en_US' );
-
- $response = $server->dispatch( $request );
+ return $server->dispatch( $request );
} finally {
remove_filter( 'woocommerce_enhanced_push_notifications_disabled', '__return_true' );
}
-
- $this->assertSame( WP_Http::FORBIDDEN, $response->get_status() );
}
/**