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
25 changes: 25 additions & 0 deletions src/DataView/DataViewConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
4 changes: 2 additions & 2 deletions src/DataView/Request.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
}

/**
Expand Down
31 changes: 24 additions & 7 deletions src/DataView/RequestRouter.php
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ protected function render_create_form( array $errors = [], array $data = [] ): v
}

echo '<form method="post" action="' . esc_url( $this->url_builder->url( 'create' ) ) . '">';
wp_nonce_field( $this->url_builder->get_nonce_action( 'create' ) );
$this->nonce_field( 'create' );
echo $this->renderer->render_editor( $layout, $data );
echo '</form>';

Expand Down Expand Up @@ -316,8 +316,10 @@ protected function render_edit_form( int $id, array $errors = [] ): void {
}

echo '<form method="post" action="' . esc_url( $this->url_builder->url( 'edit', $id ) ) . '">';
wp_nonce_field( $this->url_builder->get_nonce_action( 'edit', $id ) );
$this->nonce_field( 'edit', $id );
$this->nonce_field( 'delete', $id );
echo '<input type="hidden" name="id" value="' . esc_attr( (string) $id ) . '">';

echo $this->renderer->render_editor( $layout, $data );
echo '</form>';

Expand Down Expand Up @@ -388,7 +390,7 @@ protected function render_settings_form( array $errors = [] ): void {
}

echo '<form method="post">';
wp_nonce_field( $this->url_builder->get_nonce_action( 'update' ) );
$this->nonce_field( 'update' );
echo $this->renderer->render_editor( $layout, $data );
echo '</form>';

Expand Down Expand Up @@ -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' ] );
} );
}

Expand Down Expand Up @@ -519,7 +523,7 @@ protected function render_list_table( array $entities ): string {
$html .= '<td>';
$html .= '<a href="' . esc_url( $this->url_builder->url( 'edit', $id ) ) . '">Edit</a>';
$html .= ' | ';
$html .= '<a href="' . esc_url( $this->url_builder->url_with_nonce( 'delete', $id, $this->url_builder->get_nonce_action( 'delete', $id ) ) ) . '" onclick="return confirm(\'Are you sure?\');">Delete</a>';
$html .= '<a href="' . esc_url( $this->url_builder->url_with_nonce( 'delete', $id, $this->config->get_nonce_action( 'delete', $id ) ) ) . '" onclick="return confirm(\'Are you sure?\');">Delete</a>';
$html .= '</td>';

$html .= '</tr>';
Expand All @@ -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.
*
Expand All @@ -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;
}

Expand Down
26 changes: 1 addition & 25 deletions src/DataView/UrlBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
}
}
10 changes: 9 additions & 1 deletion src/Renderer/TangibleFieldsRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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' => '',
],
Expand Down
99 changes: 90 additions & 9 deletions tests/phpunit/data-view.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1506,7 +1587,7 @@ public function test_renderer_action_buttons_output_html(): void {
$this->assertStringContainsString( '<button', $save_html );
$this->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</button>', $save_html );

// Test delete button.
Expand Down