Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/DataView/Request.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
10 changes: 9 additions & 1 deletion src/DataView/RequestRouter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
87 changes: 87 additions & 0 deletions tests/phpunit/data-view.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down