diff --git a/src/dashboard/application/content-types/content-types-repository.php b/src/dashboard/application/content-types/content-types-repository.php index dd77cafd1dd..5c9d443a5cb 100644 --- a/src/dashboard/application/content-types/content-types-repository.php +++ b/src/dashboard/application/content-types/content-types-repository.php @@ -4,9 +4,7 @@ namespace Yoast\WP\SEO\Dashboard\Application\Content_Types; use Yoast\WP\SEO\Dashboard\Application\Taxonomies\Taxonomies_Repository; -use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Type; -use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Types_List; -use Yoast\WP\SEO\Helpers\Post_Type_Helper; +use Yoast\WP\SEO\Dashboard\Infrastructure\Content_Types\Content_Types_Collector; /** * The repository to get content types. @@ -16,16 +14,9 @@ class Content_Types_Repository { /** * The post type helper. * - * @var Post_Type_Helper + * @var Content_Types_Collector */ - protected $post_type_helper; - - /** - * The content types list. - * - * @var Content_Types_List - */ - protected $content_types_list; + protected $content_types_collector; /** * The taxonomies repository. @@ -37,18 +28,15 @@ class Content_Types_Repository { /** * The constructor. * - * @param Post_Type_Helper $post_type_helper The post type helper. - * @param Content_Types_List $content_types_list The content types list. - * @param Taxonomies_Repository $taxonomies_repository The taxonomies repository. + * @param Content_Types_Collector $content_types_collector The post type helper. + * @param Taxonomies_Repository $taxonomies_repository The taxonomies repository. */ public function __construct( - Post_Type_Helper $post_type_helper, - Content_Types_List $content_types_list, + Content_Types_Collector $content_types_collector, Taxonomies_Repository $taxonomies_repository ) { - $this->post_type_helper = $post_type_helper; - $this->content_types_list = $content_types_list; - $this->taxonomies_repository = $taxonomies_repository; + $this->content_types_collector = $content_types_collector; + $this->taxonomies_repository = $taxonomies_repository; } /** @@ -57,16 +45,13 @@ public function __construct( * @return array>>>> The content types array. */ public function get_content_types(): array { - $post_types = $this->post_type_helper->get_indexable_post_types(); - - foreach ( $post_types as $post_type ) { - $post_type_object = \get_post_type_object( $post_type ); // @TODO: Refactor `Post_Type_Helper::get_indexable_post_types()` to be able to return objects. That way, we can remove this line. - $content_type_taxonomy = $this->taxonomies_repository->get_content_type_taxonomy( $post_type_object->name ); + $content_types_list = $this->content_types_collector->get_content_types(); - $content_type = new Content_Type( $post_type_object->name, $post_type_object->label, $content_type_taxonomy ); - $this->content_types_list->add( $content_type ); + foreach ( $content_types_list->get() as $content_type ) { + $content_type_taxonomy = $this->taxonomies_repository->get_content_type_taxonomy( $content_type->get_name() ); + $content_type->set_taxonomy( $content_type_taxonomy ); } - return $this->content_types_list->to_array(); + return $content_types_list->to_array(); } } diff --git a/src/dashboard/application/scores/abstract-scores-repository.php b/src/dashboard/application/scores/abstract-scores-repository.php new file mode 100644 index 00000000000..fb46e9d80d6 --- /dev/null +++ b/src/dashboard/application/scores/abstract-scores-repository.php @@ -0,0 +1,77 @@ +score_link_collector = $score_link_collector; + } + + /** + * Returns the scores of a content type. + * + * @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>> The scores. + */ + public function get_scores( Content_Type $content_type, ?Taxonomy $taxonomy, ?int $term_id ): array { + $scores_list = new Scores_List(); + $current_scores = $this->scores_collector->get_current_scores( $this->scores, $content_type, $term_id ); + + foreach ( $this->scores as $score ) { + $score_name = $score->get_name(); + $score->set_amount( (int) $current_scores->$score_name ); + $score->set_view_link( $this->score_link_collector->get_view_link( $score, $content_type, $taxonomy, $term_id ) ); + + $scores_list->add( $score ); + } + + return $scores_list->to_array(); + } +} diff --git a/src/dashboard/application/scores/readability-scores/readability-scores-repository.php b/src/dashboard/application/scores/readability-scores/readability-scores-repository.php new file mode 100644 index 00000000000..cbd51810975 --- /dev/null +++ b/src/dashboard/application/scores/readability-scores/readability-scores-repository.php @@ -0,0 +1,28 @@ +scores_collector = $readability_scores_collector; + $this->scores = $readability_scores; + } +} diff --git a/src/dashboard/application/scores/scores-repository-interface.php b/src/dashboard/application/scores/scores-repository-interface.php new file mode 100644 index 00000000000..3dd84acab7c --- /dev/null +++ b/src/dashboard/application/scores/scores-repository-interface.php @@ -0,0 +1,24 @@ +>> The scores. + */ + public function get_scores( Content_Type $content_type, ?Taxonomy $taxonomy, ?int $term_id ): array; +} diff --git a/src/dashboard/application/scores/seo-scores/seo-scores-repository.php b/src/dashboard/application/scores/seo-scores/seo-scores-repository.php new file mode 100644 index 00000000000..7fee6024d7c --- /dev/null +++ b/src/dashboard/application/scores/seo-scores/seo-scores-repository.php @@ -0,0 +1,28 @@ +scores_collector = $seo_scores_collector; + $this->scores = $seo_scores; + } +} diff --git a/src/dashboard/domain/content-types/content-type.php b/src/dashboard/domain/content-types/content-type.php index c1797868df1..275e40ef5c4 100644 --- a/src/dashboard/domain/content-types/content-type.php +++ b/src/dashboard/domain/content-types/content-type.php @@ -37,7 +37,7 @@ class Content_Type { * @param string $label The label of the content type. * @param Taxonomy|null $taxonomy The taxonomy that filters the content type. */ - public function __construct( string $name, string $label, ?Taxonomy $taxonomy ) { + public function __construct( string $name, string $label, ?Taxonomy $taxonomy = null ) { $this->name = $name; $this->label = $label; $this->taxonomy = $taxonomy; @@ -69,4 +69,15 @@ public function get_label(): string { public function get_taxonomy(): ?Taxonomy { return $this->taxonomy; } + + /** + * Sets the taxonomy that filters the content type. + * + * @param Taxonomy|null $taxonomy The taxonomy that filters the content type. + * + * @return void + */ + public function set_taxonomy( ?Taxonomy $taxonomy ): void { + $this->taxonomy = $taxonomy; + } } diff --git a/src/dashboard/domain/content-types/content-types-list.php b/src/dashboard/domain/content-types/content-types-list.php index 520161e2a54..ad77c889ab8 100644 --- a/src/dashboard/domain/content-types/content-types-list.php +++ b/src/dashboard/domain/content-types/content-types-list.php @@ -22,7 +22,16 @@ class Content_Types_List { * @return void */ public function add( Content_Type $content_type ): void { - $this->content_types[] = $content_type; + $this->content_types[ $content_type->get_name() ] = $content_type; + } + + /** + * Returns the content types in the list. + * + * @return array The content types in the list. + */ + public function get(): array { + return $this->content_types; } /** diff --git a/src/dashboard/domain/scores/abstract-score.php b/src/dashboard/domain/scores/abstract-score.php new file mode 100644 index 00000000000..f30f13ac885 --- /dev/null +++ b/src/dashboard/domain/scores/abstract-score.php @@ -0,0 +1,105 @@ +amount; + } + + /** + * Sets the amount of the score. + * + * @param int $amount The amount of the score. + * + * @return void + */ + public function set_amount( int $amount ): void { + $this->amount = $amount; + } + + /** + * Gets the view link of the score. + * + * @return string|null The view link of the score. + */ + public function get_view_link(): ?string { + return $this->view_link; + } + + /** + * Sets the view link of the score. + * + * @param string $view_link The view link of the score. + * + * @return void + */ + public function set_view_link( ?string $view_link ): void { + $this->view_link = $view_link; + } +} diff --git a/src/dashboard/domain/scores/readability-scores/abstract-readability-score.php b/src/dashboard/domain/scores/readability-scores/abstract-readability-score.php new file mode 100644 index 00000000000..133623f9173 --- /dev/null +++ b/src/dashboard/domain/scores/readability-scores/abstract-readability-score.php @@ -0,0 +1,21 @@ +scores[] = $score; + } + + /** + * Parses the score list to the expected key value representation. + * + * @return array>> The score list presented as the expected key value representation. + */ + public function to_array(): array { + $array = []; + foreach ( $this->scores as $score ) { + $array[ $score->get_position() ] = [ + 'name' => $score->get_name(), + 'amount' => $score->get_amount(), + 'links' => ( $score->get_view_link() === null ) ? [] : [ 'view' => $score->get_view_link() ], + ]; + } + + \ksort( $array ); + + return $array; + } +} diff --git a/src/dashboard/domain/scores/seo-scores/abstract-seo-score.php b/src/dashboard/domain/scores/seo-scores/abstract-seo-score.php new file mode 100644 index 00000000000..52f5b3f0d0f --- /dev/null +++ b/src/dashboard/domain/scores/seo-scores/abstract-seo-score.php @@ -0,0 +1,21 @@ +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. * diff --git a/src/dashboard/infrastructure/content-types/content-types-collector.php b/src/dashboard/infrastructure/content-types/content-types-collector.php new file mode 100644 index 00000000000..02b5af03139 --- /dev/null +++ b/src/dashboard/infrastructure/content-types/content-types-collector.php @@ -0,0 +1,50 @@ +post_type_helper = $post_type_helper; + } + + /** + * Returns the content types in a list. + * + * @return Content_Types_List The content types in a list. + */ + public function get_content_types(): Content_Types_List { + $content_types_list = new Content_Types_List(); + $post_types = $this->post_type_helper->get_indexable_post_types(); + + foreach ( $post_types as $post_type ) { + $post_type_object = \get_post_type_object( $post_type ); // @TODO: Refactor `Post_Type_Helper::get_indexable_post_types()` to be able to return objects. That way, we can remove this line. + + $content_type = new Content_Type( $post_type_object->name, $post_type_object->label ); + $content_types_list->add( $content_type ); + } + + return $content_types_list; + } +} diff --git a/src/dashboard/infrastructure/scores/readability-scores/readability-scores-collector.php b/src/dashboard/infrastructure/scores/readability-scores/readability-scores-collector.php new file mode 100644 index 00000000000..8edb76b1ee0 --- /dev/null +++ b/src/dashboard/infrastructure/scores/readability-scores/readability-scores-collector.php @@ -0,0 +1,120 @@ + The current readability scores for a content type. + */ + public function get_current_scores( array $readability_scores, Content_Type $content_type, ?int $term_id ) { + global $wpdb; + $select = $this->build_select( $readability_scores ); + + $replacements = \array_merge( + \array_values( $select['replacements'] ), + [ + Model::get_table_name( 'Indexable' ), + $content_type->get_name(), + ] + ); + + if ( $term_id === null ) { + //phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $select['fields'] is an array of simple strings with placeholders. + //phpcs:disable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- $replacements is an array with the correct replacements. + //phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. + //phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. + $current_scores = $wpdb->get_row( + $wpdb->prepare( + " + SELECT {$select['fields']} + FROM %i AS I + WHERE ( I.post_status = 'publish' OR I.post_status IS NULL ) + AND I.object_type = 'post' + AND I.object_sub_type = %s", + $replacements + ) + ); + //phpcs:enable + return $current_scores; + + } + + $replacements[] = $wpdb->term_relationships; + $replacements[] = $term_id; + + //phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $select['fields'] is an array of simple strings with placeholders. + //phpcs:disable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- $replacements is an array with the correct replacements. + //phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. + //phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. + $current_scores = $wpdb->get_row( + $wpdb->prepare( + " + SELECT {$select['fields']} + FROM %i AS I + WHERE ( I.post_status = 'publish' OR I.post_status IS NULL ) + AND I.object_type = 'post' + AND I.object_sub_type = %s + AND I.object_id IN ( + SELECT object_id + FROM %i + WHERE term_taxonomy_id = %d + )", + $replacements + ) + ); + //phpcs:enable + return $current_scores; + } + + /** + * Builds the select statement for the readability scores query. + * + * @param Readability_Scores_Interface[] $readability_scores All readability scores. + * + * @return array The select statement for the readability scores query. + */ + private function build_select( array $readability_scores ): array { + $select_fields = []; + $select_replacements = []; + + foreach ( $readability_scores as $readability_score ) { + $min = $readability_score->get_min_score(); + $max = $readability_score->get_max_score(); + $name = $readability_score->get_name(); + + if ( $min === null || $max === null ) { + $select_fields[] = 'COUNT(CASE WHEN I.readability_score = 0 AND I.estimated_reading_time_minutes IS NULL THEN 1 END) AS %i'; + $select_replacements[] = $name; + } + else { + $select_fields[] = 'COUNT(CASE WHEN I.readability_score >= %d AND I.readability_score <= %d AND I.estimated_reading_time_minutes IS NOT NULL THEN 1 END) AS %i'; + $select_replacements[] = $min; + $select_replacements[] = $max; + $select_replacements[] = $name; + } + } + + $select_fields = \implode( ', ', $select_fields ); + + return [ + 'fields' => $select_fields, + 'replacements' => $select_replacements, + ]; + } +} diff --git a/src/dashboard/infrastructure/scores/score-link-collector.php b/src/dashboard/infrastructure/scores/score-link-collector.php new file mode 100644 index 00000000000..0576811346e --- /dev/null +++ b/src/dashboard/infrastructure/scores/score-link-collector.php @@ -0,0 +1,49 @@ + 'publish', + 'post_type' => $content_type->get_name(), + $score_name->get_filter_key() => $score_name->get_filter_value(), + ]; + + if ( $taxonomy === null || $term_id === null ) { + return \add_query_arg( $args, $posts_page ); + } + + $taxonomy_object = \get_taxonomy( $taxonomy->get_name() ); + $query_var = $taxonomy_object->query_var; + + if ( $query_var === false ) { + return null; + } + + $term = \get_term( $term_id ); + $args[ $query_var ] = $term->slug; + + return \add_query_arg( $args, $posts_page ); + } +} diff --git a/src/dashboard/infrastructure/scores/scores-collector-interface.php b/src/dashboard/infrastructure/scores/scores-collector-interface.php new file mode 100644 index 00000000000..06d398e84a7 --- /dev/null +++ b/src/dashboard/infrastructure/scores/scores-collector-interface.php @@ -0,0 +1,24 @@ + The current scores for a content type. + */ + public function get_current_scores( array $scores, Content_Type $content_type, ?int $term_id ); +} diff --git a/src/dashboard/infrastructure/scores/seo-scores/seo-scores-collector.php b/src/dashboard/infrastructure/scores/seo-scores/seo-scores-collector.php new file mode 100644 index 00000000000..0a8f4503465 --- /dev/null +++ b/src/dashboard/infrastructure/scores/seo-scores/seo-scores-collector.php @@ -0,0 +1,122 @@ + The current SEO scores for a content type. + */ + public function get_current_scores( array $seo_scores, Content_Type $content_type, ?int $term_id ) { + global $wpdb; + $select = $this->build_select( $seo_scores ); + + $replacements = \array_merge( + \array_values( $select['replacements'] ), + [ + Model::get_table_name( 'Indexable' ), + $content_type->get_name(), + ] + ); + + if ( $term_id === null ) { + //phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $select['fields'] is an array of simple strings with placeholders. + //phpcs:disable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- $replacements is an array with the correct replacements. + //phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. + //phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. + $current_scores = $wpdb->get_row( + $wpdb->prepare( + " + SELECT {$select['fields']} + FROM %i AS I + WHERE ( I.post_status = 'publish' OR I.post_status IS NULL ) + AND I.object_type = 'post' + AND I.object_sub_type = %s + AND ( I.is_robots_noindex IS NULL OR I.is_robots_noindex <> 1 )", + $replacements + ) + ); + //phpcs:enable + return $current_scores; + + } + + $replacements[] = $wpdb->term_relationships; + $replacements[] = $term_id; + + //phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $select['fields'] is an array of simple strings with placeholders. + //phpcs:disable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- $replacements is an array with the correct replacements. + //phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. + //phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. + $current_scores = $wpdb->get_row( + $wpdb->prepare( + " + SELECT {$select['fields']} + FROM %i AS I + WHERE ( I.post_status = 'publish' OR I.post_status IS NULL ) + AND I.object_type IN ('post') + AND I.object_sub_type = %s + AND ( I.is_robots_noindex IS NULL OR I.is_robots_noindex <> 1 ) + AND I.object_id IN ( + SELECT object_id + FROM %i + WHERE term_taxonomy_id = %d + )", + $replacements + ) + ); + //phpcs:enable + return $current_scores; + } + + /** + * Builds the select statement for the SEO scores query. + * + * @param SEO_Scores_Interface[] $seo_scores All SEO scores. + * + * @return array The select statement for the SEO scores query. + */ + private function build_select( array $seo_scores ): array { + $select_fields = []; + $select_replacements = []; + + foreach ( $seo_scores as $seo_score ) { + $min = $seo_score->get_min_score(); + $max = $seo_score->get_max_score(); + $name = $seo_score->get_name(); + + if ( $min === null || $max === null ) { + $select_fields[] = 'COUNT(CASE WHEN I.primary_focus_keyword_score IS NULL THEN 1 END) AS %i'; + $select_replacements[] = $name; + } + else { + $select_fields[] = 'COUNT(CASE WHEN I.primary_focus_keyword_score >= %d AND I.primary_focus_keyword_score <= %d THEN 1 END) AS %i'; + $select_replacements[] = $min; + $select_replacements[] = $max; + $select_replacements[] = $name; + } + } + + $select_fields = \implode( ', ', $select_fields ); + + return [ + 'fields' => $select_fields, + 'replacements' => $select_replacements, + ]; + } +} diff --git a/src/dashboard/user-interface/scores/abstract-scores-route.php b/src/dashboard/user-interface/scores/abstract-scores-route.php new file mode 100644 index 00000000000..72f69930f21 --- /dev/null +++ b/src/dashboard/user-interface/scores/abstract-scores-route.php @@ -0,0 +1,263 @@ +content_types_collector = $content_types_collector; + } + + /** + * Sets the repositories. + * + * @required + * + * @param Taxonomies_Repository $taxonomies_repository The taxonomies repository. + * @param Indexable_Repository $indexable_repository The indexable repository. + * + * @return void + */ + public function set_repositories( + Taxonomies_Repository $taxonomies_repository, + Indexable_Repository $indexable_repository + ) { + $this->taxonomies_repository = $taxonomies_repository; + $this->indexable_repository = $indexable_repository; + } + + /** + * Returns the route prefix. + * + * @return string The route prefix. + * + * @throws Exception If the ROUTE_PREFIX constant is not set in the child class. + */ + public function get_route_prefix() { + $class = static::class; + $prefix = $class::ROUTE_PREFIX; + + if ( $prefix === null ) { + throw new Exception( 'Score route without explicit prefix' ); + } + + return $prefix; + } + + /** + * Registers routes for scores. + * + * @return void + */ + public function register_routes() { + \register_rest_route( + Main::API_V1_NAMESPACE, + $this->get_route_prefix(), + [ + [ + 'methods' => 'GET', + 'callback' => [ $this, 'get_scores' ], + 'permission_callback' => [ $this, 'permission_manage_options' ], + 'args' => [ + 'contentType' => [ + 'required' => true, + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + ], + 'taxonomy' => [ + 'required' => false, + 'type' => 'string', + 'default' => '', + 'sanitize_callback' => 'sanitize_text_field', + ], + 'term' => [ + 'required' => false, + 'type' => 'integer', + 'default' => null, + 'sanitize_callback' => static function ( $param ) { + return \intval( $param ); + }, + ], + ], + ], + ] + ); + } + + /** + * Gets the scores of a specific content type. + * + * @param WP_REST_Request $request The request object. + * + * @return WP_REST_Response The success or failure response. + */ + public function get_scores( WP_REST_Request $request ) { + try { + $content_type = $this->get_content_type( $request['contentType'] ); + $taxonomy = $this->get_taxonomy( $request['taxonomy'], $content_type ); + $term_id = $this->get_validated_term_id( $request['term'], $taxonomy ); + } catch ( Exception $exception ) { + return new WP_REST_Response( + [ + 'error' => $exception->getMessage(), + ], + $exception->getCode() + ); + } + + return new WP_REST_Response( + $this->scores_repository->get_scores( $content_type, $taxonomy, $term_id ), + 200 + ); + } + + /** + * Gets the content type object. + * + * @param string $content_type The content type. + * + * @return Content_Type|null The content type object. + * + * @throws Exception When the content type is invalid. + */ + protected function get_content_type( string $content_type ): ?Content_Type { + $content_types = $this->content_types_collector->get_content_types()->get(); + + if ( isset( $content_types[ $content_type ] ) && \is_a( $content_types[ $content_type ], Content_Type::class ) ) { + return $content_types[ $content_type ]; + } + + throw new Exception( 'Invalid content type.', 400 ); + } + + /** + * Gets the taxonomy object. + * + * @param string $taxonomy The taxonomy. + * @param Content_Type $content_type The content type that the taxonomy is filtering. + * + * @return Taxonomy|null The taxonomy object. + * + * @throws Exception When the taxonomy is invalid. + */ + protected function get_taxonomy( string $taxonomy, Content_Type $content_type ): ?Taxonomy { + if ( $taxonomy === '' ) { + return null; + } + + $valid_taxonomy = $this->taxonomies_repository->get_content_type_taxonomy( $content_type->get_name() ); + + if ( $valid_taxonomy && $valid_taxonomy->get_name() === $taxonomy ) { + return $valid_taxonomy; + } + + throw new Exception( 'Invalid taxonomy.', 400 ); + } + + /** + * Gets the term ID validated against the given taxonomy. + * + * @param int|null $term_id The term ID to be validated. + * @param Taxonomy|null $taxonomy The taxonomy. + * + * @return bool The validated term ID. + * + * @throws Exception When the term id is invalidated. + */ + protected function get_validated_term_id( ?int $term_id, ?Taxonomy $taxonomy ): ?int { + if ( $term_id !== null && $taxonomy === null ) { + throw new Exception( 'Term needs a provided taxonomy.', 400 ); + } + + if ( $term_id === null && $taxonomy !== null ) { + throw new Exception( 'Taxonomy needs a provided term.', 400 ); + } + + if ( $term_id !== null ) { + $term = \get_term( $term_id ); + if ( ! $term || \is_wp_error( $term ) ) { + throw new Exception( 'Invalid term.', 400 ); + } + + if ( $taxonomy !== null && $term->taxonomy !== $taxonomy->get_name() ) { + throw new Exception( 'Invalid term.', 400 ); + } + } + + return $term_id; + } + + /** + * Permission callback. + * + * @return bool True when user has the 'wpseo_manage_options' capability. + */ + public function permission_manage_options() { + return WPSEO_Capability_Utils::current_user_can( 'wpseo_manage_options' ); + } +} diff --git a/src/dashboard/user-interface/scores/readability-scores-route.php b/src/dashboard/user-interface/scores/readability-scores-route.php new file mode 100644 index 00000000000..680d03b265e --- /dev/null +++ b/src/dashboard/user-interface/scores/readability-scores-route.php @@ -0,0 +1,29 @@ +scores_repository = $readability_scores_repository; + } +} diff --git a/src/dashboard/user-interface/scores/seo-scores-route.php b/src/dashboard/user-interface/scores/seo-scores-route.php new file mode 100644 index 00000000000..8fd7c79647d --- /dev/null +++ b/src/dashboard/user-interface/scores/seo-scores-route.php @@ -0,0 +1,29 @@ +scores_repository = $seo_scores_repository; + } +}