From e6f8be7faf51876d28bf55d988547cddd2b5c84a Mon Sep 17 00:00:00 2001 From: Nicolas Jaussaud Date: Wed, 27 May 2026 18:15:40 +0200 Subject: [PATCH 1/5] Data View Config: Move nonce action generation from URL builder to DataViewConfig --- src/DataView/DataViewConfig.php | 15 +++++++++++++++ src/DataView/RequestRouter.php | 10 +++++----- src/DataView/UrlBuilder.php | 24 ------------------------ tests/phpunit/data-view.php | 20 ++++++++++++-------- 4 files changed, 32 insertions(+), 37 deletions(-) diff --git a/src/DataView/DataViewConfig.php b/src/DataView/DataViewConfig.php index 678d2be..b9c13d0 100644 --- a/src/DataView/DataViewConfig.php +++ b/src/DataView/DataViewConfig.php @@ -282,6 +282,21 @@ public function get_menu_page(): string { return $this->ui['menu_page']; } + /** + * Generate the nonce action name for a given action and optional ID. + * + * @param string $action Action name. + * @param int|null $id Entity ID. + * @return string Nonce action name. + */ + public function get_nonce_action( string $action, ?int $id = null ): string { + $nonce = $this->get_menu_page() . '_' . $action; + if ( $id !== null ) { + $nonce .= '_' . $id; + } + return $nonce; + } + /** * Get the admin menu label. * diff --git a/src/DataView/RequestRouter.php b/src/DataView/RequestRouter.php index 90adc5b..823a8d1 100644 --- a/src/DataView/RequestRouter.php +++ b/src/DataView/RequestRouter.php @@ -257,7 +257,7 @@ protected function render_create_form( array $errors = [], array $data = [] ): v } echo '
'; - wp_nonce_field( $this->url_builder->get_nonce_action( 'create' ) ); + wp_nonce_field( $this->config->get_nonce_action( 'create' ) ); echo $this->renderer->render_editor( $layout, $data ); echo '
'; @@ -316,7 +316,7 @@ protected function render_edit_form( int $id, array $errors = [] ): void { } echo '
'; - wp_nonce_field( $this->url_builder->get_nonce_action( 'edit', $id ) ); + wp_nonce_field( $this->config->get_nonce_action( 'edit', $id ) ); echo ''; echo $this->renderer->render_editor( $layout, $data ); echo '
'; @@ -388,7 +388,7 @@ protected function render_settings_form( array $errors = [] ): void { } echo '
'; - wp_nonce_field( $this->url_builder->get_nonce_action( 'update' ) ); + wp_nonce_field( $this->config->get_nonce_action( 'update' ) ); echo $this->renderer->render_editor( $layout, $data ); echo '
'; @@ -519,7 +519,7 @@ protected function render_list_table( array $entities ): string { $html .= ''; $html .= 'Edit'; $html .= ' | '; - $html .= 'Delete'; + $html .= 'Delete'; $html .= ''; $html .= ''; @@ -537,7 +537,7 @@ protected function render_list_table( array $entities ): string { * @return bool True if nonce is valid. */ protected function verify_nonce( string $action, ?int $id = null ): bool { - $nonce_action = $this->url_builder->get_nonce_action( $action, $id ); + $nonce_action = $this->config->get_nonce_action( $action, $id ); $nonce = $this->request->get_nonce(); return wp_verify_nonce( $nonce, $nonce_action ) !== false; } diff --git a/src/DataView/UrlBuilder.php b/src/DataView/UrlBuilder.php index aef632e..d7d56cf 100644 --- a/src/DataView/UrlBuilder.php +++ b/src/DataView/UrlBuilder.php @@ -49,28 +49,4 @@ public function url_with_nonce( string $action, ?int $id, string $nonce_action ) $url = $this->url( $action, $id ); return wp_nonce_url( $url, $nonce_action ); } - - /** - * Get the menu page slug. - * - * @return string Menu page slug. - */ - public function get_menu_page(): string { - return $this->menu_page; - } - - /** - * Generate the nonce action name for a given action and optional ID. - * - * @param string $action Action name. - * @param int|null $id Entity ID. - * @return string Nonce action name. - */ - public function get_nonce_action( string $action, ?int $id = null ): string { - $nonce = $this->menu_page . '_' . $action; - if ( $id !== null ) { - $nonce .= '_' . $id; - } - return $nonce; - } } diff --git a/tests/phpunit/data-view.php b/tests/phpunit/data-view.php index ff79edf..1759451 100644 --- a/tests/phpunit/data-view.php +++ b/tests/phpunit/data-view.php @@ -700,14 +700,6 @@ public function test_url_builder_generates_edit_url_with_id(): void { $this->assertStringContainsString( 'id=42', $url ); } - public function test_url_builder_generates_nonce_action(): void { - $builder = new UrlBuilder( 'my_page' ); - - $this->assertEquals( 'my_page_create', $builder->get_nonce_action( 'create' ) ); - $this->assertEquals( 'my_page_edit_42', $builder->get_nonce_action( 'edit', 42 ) ); - $this->assertEquals( 'my_page_delete_5', $builder->get_nonce_action( 'delete', 5 ) ); - } - /** * ========================================================================== * DataView with CPT Storage Tests @@ -1264,6 +1256,18 @@ public function test_dataview_database_storage_options_override_defaults(): void $this->assertEquals( 'Test Item', $result->get_entity()->get( 'name' ) ); } + public function test_data_view_config_generates_nonce_action(): void { + $config = new DataViewConfig( [ + 'slug' => 'test_view', + 'label' => 'Test', + 'fields' => [ 'name' => 'string' ], + ] ); + + $this->assertEquals( 'test_view_create', $config->get_nonce_action( 'create' ) ); + $this->assertEquals( 'test_view_edit_42', $config->get_nonce_action( 'edit', 42 ) ); + $this->assertEquals( 'test_view_delete_5', $config->get_nonce_action( 'delete', 5 ) ); + } + /** * ========================================================================== * TangibleFieldsRenderer Tests From 00ee5c8f30d2fed08893134cfb329866d8b1503d Mon Sep 17 00:00:00 2001 From: Nicolas Jaussaud Date: Wed, 27 May 2026 19:05:26 +0200 Subject: [PATCH 2/5] Layout: Render default actions according to current action (create or edit) + tests for default layout --- src/DataView/RequestRouter.php | 4 +- src/Renderer/TangibleFieldsRenderer.php | 8 +++ tests/phpunit/data-view.php | 77 +++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 1 deletion(-) diff --git a/src/DataView/RequestRouter.php b/src/DataView/RequestRouter.php index 823a8d1..aa1e637 100644 --- a/src/DataView/RequestRouter.php +++ b/src/DataView/RequestRouter.php @@ -480,7 +480,9 @@ protected function build_default_layout( Layout $layout ): void { } ); $layout->sidebar( function ( Sidebar $sidebar ) { - $sidebar->actions( [ 'save', 'delete' ] ); + $this->request->get_current_action() === 'create' + ? $sidebar->actions( [ 'create' ] ) + : $sidebar->actions( [ 'save', 'delete' ] ); } ); } diff --git a/src/Renderer/TangibleFieldsRenderer.php b/src/Renderer/TangibleFieldsRenderer.php index 36556b8..c77e5ef 100644 --- a/src/Renderer/TangibleFieldsRenderer.php +++ b/src/Renderer/TangibleFieldsRenderer.php @@ -279,6 +279,14 @@ protected function render_sidebar( array $sidebar ): string { */ protected function render_action( string $action ): string { $config = match ( $action ) { + 'create' => [ + 'label' => __( 'Create', 'tangible-object' ), + 'type' => 'submit', + 'name' => 'action', + 'value' => 'create', + 'class' => 'button button-primary', + 'onclick' => '', + ], 'save' => [ 'label' => __( 'Save', 'tangible-object' ), 'type' => 'submit', diff --git a/tests/phpunit/data-view.php b/tests/phpunit/data-view.php index 1759451..a04e702 100644 --- a/tests/phpunit/data-view.php +++ b/tests/phpunit/data-view.php @@ -10,6 +10,9 @@ use Tangible\DataView\DataView; use Tangible\DataView\DataViewConfig; use Tangible\DataView\FieldTypeRegistry; +use Tangible\DataView\Request; +use Tangible\DataView\RequestRouter; +use Tangible\EditorLayout\Layout; use Tangible\DataView\LabelGenerator; use Tangible\DataView\SchemaGenerator; use Tangible\DataView\UrlBuilder; @@ -1268,6 +1271,80 @@ public function test_data_view_config_generates_nonce_action(): void { $this->assertEquals( 'test_view_delete_5', $config->get_nonce_action( 'delete', 5 ) ); } + /** + * ========================================================================== + * RequestRouter Default Layout Tests + * ========================================================================== + */ + + /** + * Get default layout structure according to action (create or edit) + */ + private function build_default_layout_for_action( string $action ): array { + $request = new class( $action ) extends Request { + public function __construct( public string $current_action ) {} + public function get_current_action(): string { + return $this->current_action; + } + }; + + $config = new DataViewConfig( [ + 'slug' => 'dv_layout_test', + 'label' => 'Book', + 'fields' => [ + 'title' => 'string', + 'author' => 'string' + ], + ] ); + + $dataset = new DataSet(); + $dataset->add_string( 'title' ); + $dataset->add_string( 'author' ); + + $router = new RequestRouter( + $config, + $dataset, + new PluralHandler( new PluralObject( $config->slug ) ), + new FieldTypeRegistry(), + new UrlBuilder( $config->get_menu_page() ), + request: $request + ); + + $method = new \ReflectionMethod( $router, 'build_default_layout' ); + $method->setAccessible( true ); + $layout = new Layout( $dataset ); + $method->invoke( $router, $layout ); + + return $layout->get_structure(); + } + + public function test_build_default_layout_uses_create_action_for_create(): void { + $this->assertEquals( + [ 'create' ], + $this->build_default_layout_for_action( 'create' )['sidebar']['actions'] + ); + } + + public function test_build_default_layout_uses_save_and_delete_for_edit(): void { + $this->assertEquals( + [ 'save', 'delete' ], + $this->build_default_layout_for_action( 'edit' )['sidebar']['actions'] + ); + } + + public function test_build_default_layout_section_contains_config_fields(): void { + $structure = $this->build_default_layout_for_action( 'edit' ); + + $section = $structure['items'][0]; + $this->assertEquals( 1, count( $structure['items'] ) ); + $this->assertEquals( 2, count( $section['fields'] ) ); + + $field_slugs = array_column( $section['fields'], 'slug' ); + + $this->assertEquals( 'section', $section['type'] ); + $this->assertEquals( [ 'title', 'author' ], $field_slugs ); + } + /** * ========================================================================== * TangibleFieldsRenderer Tests From b20cfe4188c369ced2284fae13d6242b34f78e3e Mon Sep 17 00:00:00 2001 From: Nicolas Jaussaud Date: Wed, 27 May 2026 19:29:22 +0200 Subject: [PATCH 3/5] RequestRouter: Add nonces in form according to current action (create or edit) --- src/DataView/Request.php | 4 ++-- src/DataView/RequestRouter.php | 23 ++++++++++++++++++----- src/Renderer/TangibleFieldsRenderer.php | 2 +- tests/phpunit/data-view.php | 2 +- 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/DataView/Request.php b/src/DataView/Request.php index 15a56ed..e67537b 100644 --- a/src/DataView/Request.php +++ b/src/DataView/Request.php @@ -60,8 +60,8 @@ public function get_current_id(): ?int { /** * Get the WordPress nonce from the current request. */ - public function get_nonce(): string { - return (string) $this->rest_request->get_param( '_wpnonce' ); + public function get_nonce( ?string $name = '_wpnonce' ): string { + return (string) $this->rest_request->get_param( $name ); } /** diff --git a/src/DataView/RequestRouter.php b/src/DataView/RequestRouter.php index aa1e637..f0aea7e 100644 --- a/src/DataView/RequestRouter.php +++ b/src/DataView/RequestRouter.php @@ -316,8 +316,15 @@ protected function render_edit_form( int $id, array $errors = [] ): void { } echo '
'; - wp_nonce_field( $this->config->get_nonce_action( 'edit', $id ) ); - echo ''; + if ( $this->request->get_current_action() === 'create' ) { + wp_nonce_field( $this->config->get_nonce_action( 'create' ) ); + } + else { + wp_nonce_field( $this->config->get_nonce_action( 'edit', $id ) ); + wp_nonce_field( $this->config->get_nonce_action( 'delete', $id ), '_wpnonce_delete' ); + echo ''; + } + echo $this->renderer->render_editor( $layout, $data ); echo '
'; @@ -356,7 +363,7 @@ protected function handle_edit_submit( int $id ): void { * @param int $id Entity ID. */ protected function handle_delete( int $id ): void { - if ( ! $this->verify_nonce( 'delete', $id ) ) { + if ( ! $this->verify_nonce( 'delete', $id, '_wpnonce_delete' ) ) { wp_die( 'Security check failed.' ); } @@ -538,9 +545,15 @@ protected function render_list_table( array $entities ): string { * @param int|null $id Entity ID. * @return bool True if nonce is valid. */ - protected function verify_nonce( string $action, ?int $id = null ): bool { + protected function verify_nonce( + string $action, + ?int $id = null, + ?string $name = '_wpnonce' + ): bool { $nonce_action = $this->config->get_nonce_action( $action, $id ); - $nonce = $this->request->get_nonce(); + $nonce = $this->request->get_nonce( $name ); + var_dump( $nonce ); + var_dump( $nonce_action ); return wp_verify_nonce( $nonce, $nonce_action ) !== false; } diff --git a/src/Renderer/TangibleFieldsRenderer.php b/src/Renderer/TangibleFieldsRenderer.php index c77e5ef..f34f287 100644 --- a/src/Renderer/TangibleFieldsRenderer.php +++ b/src/Renderer/TangibleFieldsRenderer.php @@ -291,7 +291,7 @@ protected function render_action( string $action ): string { 'label' => __( 'Save', 'tangible-object' ), 'type' => 'submit', 'name' => 'action', - 'value' => 'save', + 'value' => 'edit', 'class' => 'button button-primary', 'onclick' => '', ], diff --git a/tests/phpunit/data-view.php b/tests/phpunit/data-view.php index a04e702..6eb9ef0 100644 --- a/tests/phpunit/data-view.php +++ b/tests/phpunit/data-view.php @@ -1587,7 +1587,7 @@ public function test_renderer_action_buttons_output_html(): void { $this->assertStringContainsString( 'assertStringContainsString( 'type="submit"', $save_html ); $this->assertStringContainsString( 'name="action"', $save_html ); - $this->assertStringContainsString( 'value="save"', $save_html ); + $this->assertStringContainsString( 'value="edit"', $save_html ); $this->assertStringContainsString( '>Save', $save_html ); // Test delete button. From e4e8b07147008bb75a4140cbded4f4d7085f5f58 Mon Sep 17 00:00:00 2001 From: Nicolas Jaussaud Date: Wed, 27 May 2026 19:47:45 +0200 Subject: [PATCH 4/5] Remove uneeded changes + clean --- src/DataView/RequestRouter.php | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/DataView/RequestRouter.php b/src/DataView/RequestRouter.php index f0aea7e..0d4571f 100644 --- a/src/DataView/RequestRouter.php +++ b/src/DataView/RequestRouter.php @@ -316,14 +316,9 @@ protected function render_edit_form( int $id, array $errors = [] ): void { } echo '
'; - if ( $this->request->get_current_action() === 'create' ) { - wp_nonce_field( $this->config->get_nonce_action( 'create' ) ); - } - else { - wp_nonce_field( $this->config->get_nonce_action( 'edit', $id ) ); - wp_nonce_field( $this->config->get_nonce_action( 'delete', $id ), '_wpnonce_delete' ); - echo ''; - } + wp_nonce_field( $this->config->get_nonce_action( 'edit', $id ) ); + wp_nonce_field( $this->config->get_nonce_action( 'delete', $id ), '_wpnonce_delete' ); + echo ''; echo $this->renderer->render_editor( $layout, $data ); echo '
'; @@ -552,8 +547,6 @@ protected function verify_nonce( ): bool { $nonce_action = $this->config->get_nonce_action( $action, $id ); $nonce = $this->request->get_nonce( $name ); - var_dump( $nonce ); - var_dump( $nonce_action ); return wp_verify_nonce( $nonce, $nonce_action ) !== false; } From 1d2f9e0a9697a294315b7aeab28964313dd57727 Mon Sep 17 00:00:00 2001 From: Nicolas Jaussaud Date: Wed, 27 May 2026 20:04:21 +0200 Subject: [PATCH 5/5] Nonces: Switch from default _wpnonce to _wpnonce_{action} everywhere to avoid special cases according to method/action --- src/DataView/DataViewConfig.php | 10 ++++++++++ src/DataView/RequestRouter.php | 31 ++++++++++++++++++++----------- src/DataView/UrlBuilder.php | 2 +- 3 files changed, 31 insertions(+), 12 deletions(-) diff --git a/src/DataView/DataViewConfig.php b/src/DataView/DataViewConfig.php index b9c13d0..a4102d4 100644 --- a/src/DataView/DataViewConfig.php +++ b/src/DataView/DataViewConfig.php @@ -297,6 +297,16 @@ public function get_nonce_action( string $action, ?int $id = null ): string { return $nonce; } + /** + * Generate the nonce request field name for a given action. + * + * @param string $action Action name. + * @return string Nonce field name. + */ + public function get_nonce_name( string $action ): string { + return '_wpnonce_' . $action; + } + /** * Get the admin menu label. * diff --git a/src/DataView/RequestRouter.php b/src/DataView/RequestRouter.php index 0d4571f..123767b 100644 --- a/src/DataView/RequestRouter.php +++ b/src/DataView/RequestRouter.php @@ -257,7 +257,7 @@ protected function render_create_form( array $errors = [], array $data = [] ): v } echo '
'; - wp_nonce_field( $this->config->get_nonce_action( 'create' ) ); + $this->nonce_field( 'create' ); echo $this->renderer->render_editor( $layout, $data ); echo '
'; @@ -316,8 +316,8 @@ protected function render_edit_form( int $id, array $errors = [] ): void { } echo '
'; - wp_nonce_field( $this->config->get_nonce_action( 'edit', $id ) ); - wp_nonce_field( $this->config->get_nonce_action( 'delete', $id ), '_wpnonce_delete' ); + $this->nonce_field( 'edit', $id ); + $this->nonce_field( 'delete', $id ); echo ''; echo $this->renderer->render_editor( $layout, $data ); @@ -358,7 +358,7 @@ protected function handle_edit_submit( int $id ): void { * @param int $id Entity ID. */ protected function handle_delete( int $id ): void { - if ( ! $this->verify_nonce( 'delete', $id, '_wpnonce_delete' ) ) { + if ( ! $this->verify_nonce( 'delete', $id ) ) { wp_die( 'Security check failed.' ); } @@ -390,7 +390,7 @@ protected function render_settings_form( array $errors = [] ): void { } echo ''; - wp_nonce_field( $this->config->get_nonce_action( 'update' ) ); + $this->nonce_field( 'update' ); echo $this->renderer->render_editor( $layout, $data ); echo '
'; @@ -533,6 +533,19 @@ protected function render_list_table( array $entities ): string { return $html; } + /** + * Output a nonce field for an action. + * + * @param string $action Action name. + * @param int|null $id Entity ID. + */ + protected function nonce_field( string $action, ?int $id = null ): void { + wp_nonce_field( + $this->config->get_nonce_action( $action, $id ), + $this->config->get_nonce_name( $action ) + ); + } + /** * Verify nonce for an action. * @@ -540,13 +553,9 @@ protected function render_list_table( array $entities ): string { * @param int|null $id Entity ID. * @return bool True if nonce is valid. */ - protected function verify_nonce( - string $action, - ?int $id = null, - ?string $name = '_wpnonce' - ): bool { + protected function verify_nonce( string $action, ?int $id = null ): bool { $nonce_action = $this->config->get_nonce_action( $action, $id ); - $nonce = $this->request->get_nonce( $name ); + $nonce = $this->request->get_nonce( $this->config->get_nonce_name( $action ) ); return wp_verify_nonce( $nonce, $nonce_action ) !== false; } diff --git a/src/DataView/UrlBuilder.php b/src/DataView/UrlBuilder.php index d7d56cf..77ae2b1 100644 --- a/src/DataView/UrlBuilder.php +++ b/src/DataView/UrlBuilder.php @@ -47,6 +47,6 @@ public function url( string $action = 'list', ?int $id = null, array $extra = [] */ public function url_with_nonce( string $action, ?int $id, string $nonce_action ): string { $url = $this->url( $action, $id ); - return wp_nonce_url( $url, $nonce_action ); + return wp_nonce_url( $url, $nonce_action, '_wpnonce_' . $action ); } }