diff --git a/src/DataView/Request.php b/src/DataView/Request.php index e67537b..aa6b16e 100644 --- a/src/DataView/Request.php +++ b/src/DataView/Request.php @@ -47,6 +47,18 @@ public function get_current_action(): string { return sanitize_key( (string) $this->rest_request->get_param( 'action' ) ); } + /** + * Get the current admin page slug from the request. + * + * Corresponds to the `page` query argument WordPress sets when an admin + * menu page is being viewed or submitted to. + * + * @return string Current page slug (empty string if not present). + */ + public function get_current_page(): string { + return sanitize_key( (string) $this->rest_request->get_param( 'page' ) ); + } + /** * Get the entity ID from the current request. * diff --git a/src/DataView/RequestRouter.php b/src/DataView/RequestRouter.php index 123767b..c84f446 100644 --- a/src/DataView/RequestRouter.php +++ b/src/DataView/RequestRouter.php @@ -144,10 +144,18 @@ public function route(): void { * @param int|null $id Entity ID. */ public function maybe_redirect(): void { - $this->check_capability(); + // This runs on the global admin_init hook, so it must ignore requests + // that are not targeting this DataView's own admin page. Without this + // guard it would intercept every admin POST (e.g. core or other-plugin + // settings) and fail their nonce check with "Security check failed.". + if ( $this->request->get_current_page() !== $this->config->get_menu_page() ) { + return; + } if ( ! $this->request->is_post() ) return; + $this->check_capability(); + if ( $this->config->is_singular() ) { $this->handle_settings_submit(); return; diff --git a/tests/phpunit/data-view.php b/tests/phpunit/data-view.php index d34fbcc..a56e46a 100644 --- a/tests/phpunit/data-view.php +++ b/tests/phpunit/data-view.php @@ -293,6 +293,93 @@ public function test_array_field_round_trips_through_dataview_and_option_storage $this->assertSame( false, $stored['post_types']['pages'] ); } + public function test_request_exposes_current_page(): void { + $_GET['page'] = 'My-Settings_Page'; + $request = new Request(); + $this->assertSame( 'my-settings_page', $request->get_current_page() ); + unset( $_GET['page'] ); + + $this->assertSame( '', ( new Request() )->get_current_page() ); + } + + /** + * Regression: maybe_redirect() runs on the global admin_init hook, so a POST + * to an UNRELATED admin page (core settings, another plugin) must be ignored + * rather than failing its nonce check with "Security check failed.". + */ + public function test_maybe_redirect_ignores_posts_to_other_pages(): void { + $config = [ + 'slug' => 'dv_test_scope_settings', + 'label' => 'Settings', + 'mode' => 'singular', + 'storage' => 'option', + 'fields' => [ 'enabled' => 'boolean' ], + ]; + + $saved_method = $_SERVER['REQUEST_METHOD'] ?? null; + $_SERVER['REQUEST_METHOD'] = 'POST'; + $_GET['page'] = 'some_other_plugin_page'; + $_POST['some_field'] = 'value'; + + // Request snapshots the superglobals at construction. + $router = $this->get_router( new DataView( $config ) ); + + // Must return without calling wp_die() (no WPDieException thrown). + $router->maybe_redirect(); + $this->assertTrue( true, 'maybe_redirect() returned without intercepting an unrelated page' ); + + unset( $_GET['page'], $_POST['some_field'] ); + if ( $saved_method === null ) { + unset( $_SERVER['REQUEST_METHOD'] ); + } else { + $_SERVER['REQUEST_METHOD'] = $saved_method; + } + } + + /** + * The guard still lets a POST to this DataView's own page through to the + * nonce check — an invalid nonce on our own page should fail closed. + */ + public function test_maybe_redirect_enforces_nonce_on_own_page(): void { + wp_set_current_user( $this->factory->user->create( [ 'role' => 'administrator' ] ) ); + + $config = [ + 'slug' => 'dv_test_scope_own', + 'label' => 'Settings', + 'mode' => 'singular', + 'storage' => 'option', + 'fields' => [ 'enabled' => 'boolean' ], + ]; + + $saved_method = $_SERVER['REQUEST_METHOD'] ?? null; + $_SERVER['REQUEST_METHOD'] = 'POST'; + $_GET['page'] = 'dv_test_scope_own'; + $_POST['enabled'] = '1'; // no valid _wpnonce_update field + + $router = $this->get_router( new DataView( $config ) ); + + $this->expectException( \WPDieException::class ); + try { + $router->maybe_redirect(); + } finally { + unset( $_GET['page'], $_POST['enabled'] ); + if ( $saved_method === null ) { + unset( $_SERVER['REQUEST_METHOD'] ); + } else { + $_SERVER['REQUEST_METHOD'] = $saved_method; + } + } + } + + /** + * Reach the protected RequestRouter on a DataView for direct invocation. + */ + private function get_router( DataView $view ): \Tangible\DataView\RequestRouter { + $property = new \ReflectionProperty( DataView::class, 'router' ); + $property->setAccessible( true ); + return $property->getValue( $view ); + } + public function test_field_type_registry_has_repeater_type(): void { $registry = new FieldTypeRegistry();