Skip to content

Commit

Permalink
Use domain context for validating REST requests
Browse files Browse the repository at this point in the history
  • Loading branch information
leonidasmi committed Nov 19, 2024
1 parent 0a88fc4 commit c497fd3
Show file tree
Hide file tree
Showing 5 changed files with 102 additions and 52 deletions.
12 changes: 7 additions & 5 deletions src/dashboard/application/seo-scores/seo-scores-repository.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\Dashboard\Application\SEO_Scores;

use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Type;
use Yoast\WP\SEO\Dashboard\Domain\SEO_Scores\SEO_Scores_Interface;
use Yoast\WP\SEO\Dashboard\Domain\Taxonomies\Taxonomy;
use Yoast\WP\SEO\Dashboard\Infrastructure\SEO_Scores\SEO_Scores_Collector;

/**
Expand Down Expand Up @@ -41,19 +43,19 @@ public function __construct(
/**
* Returns the SEO Scores of a content type.
*
* @param string $content_type The content type.
* @param string $taxonomy The taxonomy of the term we're filtering for.
* @param int $term_id The ID of the term we're filtering for.
* @param Content_Type $content_type The content type.
* @param Taxonomy|null $taxonomy The taxonomy of the term we're filtering for.
* @param int|null $term_id The ID of the term we're filtering for.
*
* @return array<array<string, string|array<string, string>>> The SEO scores.
*/
public function get_seo_scores( string $content_type, string $taxonomy, int $term_id ): array {
public function get_seo_scores( Content_Type $content_type, ?Taxonomy $taxonomy, ?int $term_id ): array {
$seo_scores = [];

$current_scores = $this->seo_scores_collector->get_seo_scores( $this->seo_scores, $content_type, $taxonomy, $term_id );
foreach ( $this->seo_scores as $seo_score ) {
$seo_score->set_amount( (int) $current_scores[ $seo_score->get_name() ] );
$seo_score->set_view_link( $this->seo_scores_collector->get_view_link( $seo_score->get_filter_name(), $content_type, $taxonomy, $term_id ) );
$seo_score->set_view_link( $this->seo_scores_collector->get_view_link( $seo_score, $content_type, $taxonomy, $term_id ) );

$seo_scores[] = $seo_score->to_array();
}
Expand Down
9 changes: 9 additions & 0 deletions src/dashboard/domain/taxonomies/taxonomy.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,15 @@ public function __construct(
$this->rest_url = $rest_url;
}

/**
* Returns the name of the taxonomy.
*
* @return string The name of the taxonomy.
*/
public function get_name(): string {
return $this->name;
}

/**
* Parses the taxonomy to the expected key value representation.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public function __construct(
/**
* Returns the content types array.
*
* @return array<array<string,array<string, array<string, array<string, string|null>>>>> The content types array.
* @return Content_Type[] The content types array.
*/
public function get_content_types(): array {
$content_types = [];
Expand Down
34 changes: 18 additions & 16 deletions src/dashboard/infrastructure/seo-scores/seo-scores-collector.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

use wpdb;
use Yoast\WP\Lib\Model;
use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Type;
use Yoast\WP\SEO\Dashboard\Domain\SEO_Scores\SEO_Scores_Interface;
use Yoast\WP\SEO\Dashboard\Domain\Taxonomies\Taxonomy;

/**
* Getting SEO scores from the indexable database table.
Expand Down Expand Up @@ -33,24 +35,24 @@ public function __construct(
* Retrieves the current SEO scores for a content type.
*
* @param SEO_Scores_Interface[] $seo_scores All SEO scores.
* @param string $content_type The content type.
* @param string $taxonomy The taxonomy of the term we're filtering for.
* @param int $term_id The ID of the term we're filtering for.
* @param Content_Type $content_type The content type.
* @param Taxonomy|null $taxonomy The taxonomy of the term we're filtering for.
* @param int|null $term_id The ID of the term we're filtering for.
*
* @return array<string, string> The SEO scores for a content type.
*/
public function get_seo_scores( array $seo_scores, string $content_type, string $taxonomy, int $term_id ) {
public function get_seo_scores( array $seo_scores, Content_Type $content_type, ?Taxonomy $taxonomy, ?int $term_id ) {
$select = $this->build_select( $seo_scores );

$replacements = \array_merge(
\array_values( $select['replacements'] ),
[
Model::get_table_name( 'Indexable' ),
$content_type,
$content_type->get_name(),
]
);

if ( $term_id === 0 || $taxonomy === '' ) {
if ( $term_id === null || $taxonomy === null ) {
$query = $this->wpdb->prepare(

Check warning on line 56 in src/dashboard/infrastructure/seo-scores/seo-scores-collector.php

View workflow job for this annotation

GitHub Actions / Check code style

Incorrect number of replacements passed to $wpdb->prepare(). Found 1 replacement parameters, expected 2.

Check warning on line 56 in src/dashboard/infrastructure/seo-scores/seo-scores-collector.php

View workflow job for this annotation

GitHub Actions / Check code style

Incorrect number of replacements passed to $wpdb->prepare(). Found 1 replacement parameters, expected 2.
"
SELECT {$select['fields']}

Check failure on line 58 in src/dashboard/infrastructure/seo-scores/seo-scores-collector.php

View workflow job for this annotation

GitHub Actions / Check code style

Use placeholders and $wpdb->prepare(); found interpolated variable {$select['fields']} at SELECT {$select['fields']}

Check failure on line 58 in src/dashboard/infrastructure/seo-scores/seo-scores-collector.php

View workflow job for this annotation

GitHub Actions / Check code style

Use placeholders and $wpdb->prepare(); found interpolated variable {$select['fields']} at SELECT {$select['fields']}
Expand All @@ -69,7 +71,7 @@ public function get_seo_scores( array $seo_scores, string $content_type, string
$replacements[] = $this->wpdb->term_relationships;
$replacements[] = $this->wpdb->term_taxonomy;
$replacements[] = $term_id;
$replacements[] = $taxonomy;
$replacements[] = $taxonomy->get_name();

$query = $this->wpdb->prepare(

Check warning on line 76 in src/dashboard/infrastructure/seo-scores/seo-scores-collector.php

View workflow job for this annotation

GitHub Actions / Check code style

Incorrect number of replacements passed to $wpdb->prepare(). Found 1 replacement parameters, expected 6.

Check warning on line 76 in src/dashboard/infrastructure/seo-scores/seo-scores-collector.php

View workflow job for this annotation

GitHub Actions / Check code style

Incorrect number of replacements passed to $wpdb->prepare(). Found 1 replacement parameters, expected 6.
"
Expand Down Expand Up @@ -136,27 +138,27 @@ private function build_select( array $seo_scores ): array {
/**
* Builds the view link of the SEO score.
*
* @param string $seo_score_name The name of the SEO score.
* @param string $content_type The content type.
* @param string $taxonomy The taxonomy of the term we might be filtering.
* @param int $term_id The ID of the term we might be filtering.
* @param SEO_Scores_Interface $seo_score_name The name of the SEO score.
* @param Content_Type $content_type The content type.
* @param Taxonomy|null $taxonomy The taxonomy of the term we might be filtering.
* @param int|null $term_id The ID of the term we might be filtering.
*
* @return string The view link of the SEO score.
*/
public function get_view_link( string $seo_score_name, string $content_type, string $taxonomy, int $term_id ): ?string {
public function get_view_link( SEO_Scores_Interface $seo_score_name, Content_Type $content_type, ?Taxonomy $taxonomy, ?int $term_id ): ?string {
// @TODO: Refactor by Single Source of Truthing this with the `WPSEO_Meta_Columns` class. Until then, we build this manually.
$posts_page = \admin_url( 'edit.php' );
$args = [
'post_status' => 'publish',
'post_type' => $content_type,
'seo_filter' => $seo_score_name,
'post_type' => $content_type->get_name(),
'seo_filter' => $seo_score_name->get_filter_name(),
];

if ( $taxonomy === '' || $term_id === 0 ) {
if ( $taxonomy === null || $term_id === null ) {
return \add_query_arg( $args, $posts_page );
}

$taxonomy_object = \get_taxonomy( $taxonomy );
$taxonomy_object = \get_taxonomy( $taxonomy->get_name() );
$query_var = $taxonomy_object->query_var;

if ( $query_var === false ) {
Expand Down
97 changes: 67 additions & 30 deletions src/dashboard/user-interface/seo-scores-route.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@
use WPSEO_Capability_Utils;
use Yoast\WP\SEO\Conditionals\No_Conditionals;
use Yoast\WP\SEO\Dashboard\Application\SEO_Scores\SEO_Scores_Repository;
use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Type;
use Yoast\WP\SEO\Dashboard\Domain\Taxonomies\Taxonomy;
use Yoast\WP\SEO\Dashboard\Infrastructure\Content_Types\Content_Types_Collector;
use Yoast\WP\SEO\Dashboard\Infrastructure\Taxonomies\Taxonomies_Collector;
use Yoast\WP\SEO\Main;
use Yoast\WP\SEO\Repositories\Indexable_Repository;
use Yoast\WP\SEO\Routes\Route_Interface;
Expand All @@ -34,6 +37,13 @@ class SEO_Scores_Route implements Route_Interface {
*/
private $content_types_collector;

/**
* The taxonomies collector.
*
* @var Taxonomies_Collector
*/
private $taxonomies_collector;

/**
* The SEO Scores repository.
*
Expand All @@ -59,17 +69,20 @@ class SEO_Scores_Route implements Route_Interface {
* Constructs the class.
*
* @param Content_Types_Collector $content_types_collector The content type collector.
* @param Taxonomies_Collector $taxonomies_collector The taxonomies collector.
* @param SEO_Scores_Repository $seo_scores_repository The SEO Scores repository.
* @param Indexable_Repository $indexable_repository The indexable repository.
* @param wpdb $wpdb The WordPress database object.
*/
public function __construct(
Content_Types_Collector $content_types_collector,
Taxonomies_Collector $taxonomies_collector,
SEO_Scores_Repository $seo_scores_repository,
Indexable_Repository $indexable_repository,
wpdb $wpdb
) {
$this->content_types_collector = $content_types_collector;
$this->taxonomies_collector = $taxonomies_collector;
$this->seo_scores_repository = $seo_scores_repository;
$this->indexable_repository = $indexable_repository;
$this->wpdb = $wpdb;
Expand All @@ -94,23 +107,20 @@ public function register_routes() {
'required' => true,
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => [ $this, 'validate_content_type' ],
],
'term' => [
'required' => false,
'type' => 'integer',
'default' => 0,
'default' => null,
'sanitize_callback' => static function ( $param ) {
return \intval( $param );
},
'validate_callback' => [ $this, 'validate_term' ],
],
'taxonomy' => [
'required' => false,
'type' => 'string',
'default' => '',
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => [ $this, 'validate_taxonomy' ],
],
],
],
Expand All @@ -126,7 +136,36 @@ public function register_routes() {
* @return WP_REST_Response|WP_Error The success or failure response.
*/
public function get_seo_scores( WP_REST_Request $request ) {
$result = $this->seo_scores_repository->get_seo_scores( $request['contentType'], $request['taxonomy'], $request['term'] );
$content_type = $this->get_content_type( $request['contentType'] );
if ( $content_type === null ) {
return new WP_REST_Response(
[
'error' => 'Invalid content type.',
],
400
);
}

$taxonomy = $this->get_taxonomy( $request['taxonomy'], $content_type );
if ( $request['taxonomy'] !== '' && $taxonomy === null ) {
return new WP_REST_Response(
[
'error' => 'Invalid taxonomy.',
],
400
);
}

if ( ! $this->validate_term( $request['term'], $taxonomy ) ) {
return new WP_REST_Response(
[
'error' => 'Invalid term.',
],
400
);
}

$result = $this->seo_scores_repository->get_seo_scores( $content_type, $taxonomy, $request['term'] );

return new WP_REST_Response(
[
Expand All @@ -137,59 +176,57 @@ public function get_seo_scores( WP_REST_Request $request ) {
}

/**
* Validates the content type against the content types collector.
* Gets the content type object.
*
* @param string $content_type The content type.
*
* @return bool Whether the content type passed validation.
* @return Content_Type|null The content type object.
*/
public function validate_content_type( $content_type ) {
// @TODO: Is it necessary to go through all the indexable content types again and validate against those? If so, it can look like this.
protected function get_content_type( string $content_type ): ?Content_Type {
$content_types = $this->content_types_collector->get_content_types();

if ( isset( $content_types[ $content_type ] ) ) {
return true;
if ( isset( $content_types[ $content_type ] ) && \is_a( $content_types[ $content_type ], Content_Type::class ) ) {
return $content_types[ $content_type ];
}

return false;
return null;
}

/**
* Validates the taxonomy against the given content type.
* Gets the taxonomy object.
*
* @param string $taxonomy The taxonomy.
* @param WP_REST_Request $request The request object.
* @param string $taxonomy The taxonomy.
* @param Content_Type $content_type The content type that the taxonomy is filtering.
*
* @return bool Whether the taxonomy passed validation.
* @return Taxonomy|null The taxonomy object.
*/
public function validate_taxonomy( $taxonomy, $request ) {
// @TODO: Is it necessary to validate against content types? If so, it can take inspiration from validate_content_type().
return true;
protected function get_taxonomy( string $taxonomy, Content_Type $content_type ): ?Taxonomy {
if ( $taxonomy === '' ) {
return null;
}
return $this->taxonomies_collector->get_taxonomy( $taxonomy, $content_type->get_name() );
}

/**
* Validates the term against the given content type.
* Validates the term against the given taxonomy.
*
* @param int $term_id The term ID.
* @param WP_REST_Request $request The request object.
* @param int $term_id The ID of the term.
* @param Taxonomy|null $taxonomy The taxonomy.
*
* @return bool Whether the term passed validation.
*/
public function validate_term( $term_id, $request ) {
// @TODO: Is it necessary to validate against content types? If so, it can look like this.
if ( $request['term'] === 0 ) {
return true;
protected function validate_term( ?int $term_id, ?Taxonomy $taxonomy ): bool {
if ( $term_id === null ) {
return ( $taxonomy === null );
}

$term = \get_term( $term_id );
if ( ! $term || \is_wp_error( $term ) ) {
return false;
}

$post_type = $request['contentType'];

// Check if the term's taxonomy is associated with the post type.
return \in_array( $term->taxonomy, \get_object_taxonomies( $post_type ), true );
$taxonomy_name = ( $taxonomy === null ) ? '' : $taxonomy->get_name();
return $term->taxonomy === $taxonomy_name;
}

/**
Expand Down

0 comments on commit c497fd3

Please sign in to comment.