diff --git a/src/DataView/DataViewConfig.php b/src/DataView/DataViewConfig.php index 678d2be..a4102d4 100644 --- a/src/DataView/DataViewConfig.php +++ b/src/DataView/DataViewConfig.php @@ -282,6 +282,31 @@ 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; + } + + /** + * 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/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 90adc5b..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->url_builder->get_nonce_action( 'create' ) ); + $this->nonce_field( 'create' ); echo $this->renderer->render_editor( $layout, $data ); echo '
'; @@ -316,8 +316,10 @@ protected function render_edit_form( int $id, array $errors = [] ): void { } echo '
'; - wp_nonce_field( $this->url_builder->get_nonce_action( 'edit', $id ) ); + $this->nonce_field( 'edit', $id ); + $this->nonce_field( 'delete', $id ); echo ''; + echo $this->renderer->render_editor( $layout, $data ); echo '
'; @@ -388,7 +390,7 @@ protected function render_settings_form( array $errors = [] ): void { } echo '
'; - wp_nonce_field( $this->url_builder->get_nonce_action( 'update' ) ); + $this->nonce_field( 'update' ); echo $this->renderer->render_editor( $layout, $data ); echo '
'; @@ -480,7 +482,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' ] ); } ); } @@ -519,7 +523,7 @@ protected function render_list_table( array $entities ): string { $html .= ''; $html .= 'Edit'; $html .= ' | '; - $html .= 'Delete'; + $html .= 'Delete'; $html .= ''; $html .= ''; @@ -529,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. * @@ -537,8 +554,8 @@ 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 = $this->request->get_nonce(); + $nonce_action = $this->config->get_nonce_action( $action, $id ); + $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 aef632e..77ae2b1 100644 --- a/src/DataView/UrlBuilder.php +++ b/src/DataView/UrlBuilder.php @@ -47,30 +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 ); - } - - /** - * 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; + return wp_nonce_url( $url, $nonce_action, '_wpnonce_' . $action ); } } diff --git a/src/Renderer/TangibleFieldsRenderer.php b/src/Renderer/TangibleFieldsRenderer.php index 36556b8..f34f287 100644 --- a/src/Renderer/TangibleFieldsRenderer.php +++ b/src/Renderer/TangibleFieldsRenderer.php @@ -279,11 +279,19 @@ 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', '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 ff79edf..6eb9ef0 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; @@ -700,14 +703,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 +1259,92 @@ 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 ) ); + } + + /** + * ========================================================================== + * 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 @@ -1506,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.