Phase 5: Content and SEO - Yoast SEO, Schema.org markup, Open Graph, favicon support, XML sitemap

This commit is contained in:
Hanson.xyz Dev
2025-11-28 17:10:24 -06:00
parent c4f29a3152
commit 91de533da4
1552 changed files with 171432 additions and 7 deletions
@@ -0,0 +1,157 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\Dashboard\Application\Configuration;
use Yoast\WP\SEO\Dashboard\Application\Content_Types\Content_Types_Repository;
use Yoast\WP\SEO\Dashboard\Application\Endpoints\Endpoints_Repository;
use Yoast\WP\SEO\Dashboard\Application\Tracking\Setup_Steps_Tracking;
use Yoast\WP\SEO\Dashboard\Infrastructure\Browser_Cache\Browser_Cache_Configuration;
use Yoast\WP\SEO\Dashboard\Infrastructure\Integrations\Site_Kit;
use Yoast\WP\SEO\Dashboard\Infrastructure\Nonces\Nonce_Repository;
use Yoast\WP\SEO\Editors\Application\Analysis_Features\Enabled_Analysis_Features_Repository;
use Yoast\WP\SEO\Editors\Framework\Keyphrase_Analysis;
use Yoast\WP\SEO\Editors\Framework\Readability_Analysis;
use Yoast\WP\SEO\Helpers\Indexable_Helper;
use Yoast\WP\SEO\Helpers\User_Helper;
/**
* Responsible for the dashboard configuration.
*/
class Dashboard_Configuration {
/**
* The content types repository.
*
* @var Content_Types_Repository
*/
private $content_types_repository;
/**
* The indexable helper.
*
* @var Indexable_Helper
*/
private $indexable_helper;
/**
* The user helper.
*
* @var User_Helper
*/
private $user_helper;
/**
* The repository.
*
* @var Enabled_Analysis_Features_Repository
*/
private $enabled_analysis_features_repository;
/**
* The endpoints repository.
*
* @var Endpoints_Repository
*/
private $endpoints_repository;
/**
* The nonce repository.
*
* @var Nonce_Repository
*/
private $nonce_repository;
/**
* The Site Kit integration data.
*
* @var Site_Kit
*/
private $site_kit_integration_data;
/**
* The setup steps tracking data.
*
* @var Setup_Steps_Tracking
*/
private $setup_steps_tracking;
/**
* The browser cache configuration.
*
* @var Browser_Cache_Configuration
*/
private $browser_cache_configuration;
/**
* The constructor.
*
* @param Content_Types_Repository $content_types_repository The content types repository.
* @param Indexable_Helper $indexable_helper The indexable helper
* repository.
* @param User_Helper $user_helper The user helper.
* @param Enabled_Analysis_Features_Repository $enabled_analysis_features_repository The analysis feature.
* repository.
* @param Endpoints_Repository $endpoints_repository The endpoints repository.
* @param Nonce_Repository $nonce_repository The nonce repository.
* @param Site_Kit $site_kit_integration_data The Site Kit integration data.
* @param Setup_Steps_Tracking $setup_steps_tracking The setup steps tracking data.
* @param Browser_Cache_Configuration $browser_cache_configuration The browser cache configuration.
*/
public function __construct(
Content_Types_Repository $content_types_repository,
Indexable_Helper $indexable_helper,
User_Helper $user_helper,
Enabled_Analysis_Features_Repository $enabled_analysis_features_repository,
Endpoints_Repository $endpoints_repository,
Nonce_Repository $nonce_repository,
Site_Kit $site_kit_integration_data,
Setup_Steps_Tracking $setup_steps_tracking,
Browser_Cache_Configuration $browser_cache_configuration
) {
$this->content_types_repository = $content_types_repository;
$this->indexable_helper = $indexable_helper;
$this->user_helper = $user_helper;
$this->enabled_analysis_features_repository = $enabled_analysis_features_repository;
$this->endpoints_repository = $endpoints_repository;
$this->nonce_repository = $nonce_repository;
$this->site_kit_integration_data = $site_kit_integration_data;
$this->setup_steps_tracking = $setup_steps_tracking;
$this->browser_cache_configuration = $browser_cache_configuration;
}
/**
* Returns a configuration
*
* @return array<string, array<string>|array<string, string|array<string, array<string, int>>>>
*/
public function get_configuration(): array {
$configuration = [
'contentTypes' => $this->content_types_repository->get_content_types(),
'indexablesEnabled' => $this->indexable_helper->should_index_indexables(),
'displayName' => $this->user_helper->get_current_user_display_name(),
'enabledAnalysisFeatures' => $this->enabled_analysis_features_repository->get_features_by_keys(
[
Readability_Analysis::NAME,
Keyphrase_Analysis::NAME,
]
)->to_array(),
'endpoints' => $this->endpoints_repository->get_all_endpoints()->to_array(),
'nonce' => $this->nonce_repository->get_rest_nonce(),
'setupStepsTracking' => $this->setup_steps_tracking->to_array(),
];
$site_kit_integration_data = $this->site_kit_integration_data->to_array();
if ( ! empty( $site_kit_integration_data ) ) {
$configuration ['siteKitConfiguration'] = $site_kit_integration_data;
}
$browser_cache_configuration = $this->browser_cache_configuration->get_configuration();
if ( ! empty( $browser_cache_configuration ) ) {
$configuration ['browserCache'] = $browser_cache_configuration;
}
return $configuration;
}
}
@@ -0,0 +1,57 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\Dashboard\Application\Content_Types;
use Yoast\WP\SEO\Dashboard\Application\Taxonomies\Taxonomies_Repository;
use Yoast\WP\SEO\Dashboard\Infrastructure\Content_Types\Content_Types_Collector;
/**
* The repository to get content types.
*/
class Content_Types_Repository {
/**
* The post type helper.
*
* @var Content_Types_Collector
*/
protected $content_types_collector;
/**
* The taxonomies repository.
*
* @var Taxonomies_Repository
*/
private $taxonomies_repository;
/**
* The constructor.
*
* @param Content_Types_Collector $content_types_collector The post type helper.
* @param Taxonomies_Repository $taxonomies_repository The taxonomies repository.
*/
public function __construct(
Content_Types_Collector $content_types_collector,
Taxonomies_Repository $taxonomies_repository
) {
$this->content_types_collector = $content_types_collector;
$this->taxonomies_repository = $taxonomies_repository;
}
/**
* Returns the content types array.
*
* @return array<array<string, array<string, array<string, array<string, string|null>>>>> The content types array.
*/
public function get_content_types(): array {
$content_types_list = $this->content_types_collector->get_content_types();
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 $content_types_list->to_array();
}
}
@@ -0,0 +1,42 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Application\Endpoints;
use Yoast\WP\SEO\Dashboard\Domain\Endpoint\Endpoint_Interface;
use Yoast\WP\SEO\Dashboard\Domain\Endpoint\Endpoint_List;
/**
* Repository for endpoints.
*/
class Endpoints_Repository {
/**
* Holds the endpoints.
*
* @var array<Endpoint_Interface>
*/
private $endpoints;
/**
* Constructs the repository.
*
* @param Endpoint_Interface ...$endpoints The endpoints to add to the repository.
*/
public function __construct( Endpoint_Interface ...$endpoints ) {
$this->endpoints = $endpoints;
}
/**
* Creates a list with all endpoints.
*
* @return Endpoint_List The list with all endpoints.
*/
public function get_all_endpoints(): Endpoint_List {
$list = new Endpoint_List();
foreach ( $this->endpoints as $endpoint ) {
$list->add_endpoint( $endpoint );
}
return $list;
}
}
@@ -0,0 +1,59 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\Dashboard\Application\Filter_Pairs;
use Yoast\WP\SEO\Dashboard\Domain\Filter_Pairs\Filter_Pairs_Interface;
use Yoast\WP\SEO\Dashboard\Domain\Taxonomies\Taxonomy;
use Yoast\WP\SEO\Dashboard\Infrastructure\Taxonomies\Taxonomies_Collector;
/**
* The repository to get hardcoded filter pairs.
*/
class Filter_Pairs_Repository {
/**
* The taxonomies collector.
*
* @var Taxonomies_Collector
*/
private $taxonomies_collector;
/**
* All filter pairs.
*
* @var Filter_Pairs_Interface[]
*/
private $filter_pairs;
/**
* The constructor.
*
* @param Taxonomies_Collector $taxonomies_collector The taxonomies collector.
* @param Filter_Pairs_Interface ...$filter_pairs All filter pairs.
*/
public function __construct(
Taxonomies_Collector $taxonomies_collector,
Filter_Pairs_Interface ...$filter_pairs
) {
$this->taxonomies_collector = $taxonomies_collector;
$this->filter_pairs = $filter_pairs;
}
/**
* Returns a taxonomy based on a content type, by looking into hardcoded filter pairs.
*
* @param string $content_type The content type.
*
* @return Taxonomy|null The taxonomy filter.
*/
public function get_taxonomy( string $content_type ): ?Taxonomy {
foreach ( $this->filter_pairs as $filter_pair ) {
if ( $filter_pair->get_filtered_content_type() === $content_type ) {
return $this->taxonomies_collector->get_taxonomy( $filter_pair->get_filtering_taxonomy(), $content_type );
}
}
return null;
}
}
@@ -0,0 +1,55 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\Dashboard\Application\Score_Groups\SEO_Score_Groups;
use Yoast\WP\SEO\Dashboard\Domain\Score_Groups\SEO_Score_Groups\No_SEO_Score_Group;
use Yoast\WP\SEO\Dashboard\Domain\Score_Groups\SEO_Score_Groups\SEO_Score_Groups_Interface;
/**
* The repository to get SEO score groups.
*/
class SEO_Score_Groups_Repository {
/**
* All SEO score groups.
*
* @var SEO_Score_Groups_Interface[]
*/
private $seo_score_groups;
/**
* The constructor.
*
* @param SEO_Score_Groups_Interface ...$seo_score_groups All SEO score groups.
*/
public function __construct( SEO_Score_Groups_Interface ...$seo_score_groups ) {
$this->seo_score_groups = $seo_score_groups;
}
/**
* Returns the SEO score group that a SEO score belongs to.
*
* @param int $seo_score The SEO score to be assigned into a group.
*
* @return SEO_Score_Groups_Interface The SEO score group that the SEO score belongs to.
*/
public function get_seo_score_group( ?int $seo_score ): SEO_Score_Groups_Interface {
if ( $seo_score === null || $seo_score === 0 ) {
return new No_SEO_Score_Group();
}
foreach ( $this->seo_score_groups as $seo_score_group ) {
if ( $seo_score_group->get_max_score() === null ) {
continue;
}
if ( $seo_score >= $seo_score_group->get_min_score() && $seo_score <= $seo_score_group->get_max_score() ) {
return $seo_score_group;
}
}
return new No_SEO_Score_Group();
}
}
@@ -0,0 +1,80 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
namespace Yoast\WP\SEO\Dashboard\Application\Score_Results;
use Exception;
use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Type;
use Yoast\WP\SEO\Dashboard\Domain\Score_Groups\Score_Groups_Interface;
use Yoast\WP\SEO\Dashboard\Domain\Score_Results\Score_Result;
use Yoast\WP\SEO\Dashboard\Domain\Taxonomies\Taxonomy;
use Yoast\WP\SEO\Dashboard\Infrastructure\Score_Results\Score_Results_Collector_Interface;
/**
* The abstract score results repository.
*/
abstract class Abstract_Score_Results_Repository {
/**
* The score results collector.
*
* @var Score_Results_Collector_Interface
*/
protected $score_results_collector;
/**
* The current scores repository.
*
* @var Current_Scores_Repository
*/
protected $current_scores_repository;
/**
* All score groups.
*
* @var Score_Groups_Interface[]
*/
protected $score_groups;
/**
* Sets the repositories.
*
* @required
*
* @param Current_Scores_Repository $current_scores_repository The current scores repository.
*
* @return void
*/
public function set_repositories( Current_Scores_Repository $current_scores_repository ) {
$this->current_scores_repository = $current_scores_repository;
}
/**
* Returns the score results for 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.
* @param bool|null $is_troubleshooting Whether we're in troubleshooting mode.
*
* @return array<array<string, string|int|array<string, string>>> The scores.
*
* @throws Exception When getting score results from the infrastructure fails.
*/
public function get_score_results( Content_Type $content_type, ?Taxonomy $taxonomy, ?int $term_id, ?bool $is_troubleshooting ): array {
$score_results = $this->score_results_collector->get_score_results( $this->score_groups, $content_type, $term_id, $is_troubleshooting );
if ( $is_troubleshooting === true ) {
$score_results['score_ids'] = clone $score_results['scores'];
foreach ( $score_results['scores'] as &$score ) {
$score = ( $score !== null ) ? \count( \explode( ',', $score ) ) : 0;
}
}
$current_scores_list = $this->current_scores_repository->get_current_scores( $this->score_groups, $score_results, $content_type, $taxonomy, $term_id );
$score_result_object = new Score_Result( $current_scores_list, $score_results['query_time'], $score_results['cache_used'] );
return $score_result_object->to_array();
}
}
@@ -0,0 +1,76 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
namespace Yoast\WP\SEO\Dashboard\Application\Score_Results;
use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Type;
use Yoast\WP\SEO\Dashboard\Domain\Score_Groups\Score_Groups_Interface;
use Yoast\WP\SEO\Dashboard\Domain\Score_Results\Current_Score;
use Yoast\WP\SEO\Dashboard\Domain\Score_Results\Current_Scores_List;
use Yoast\WP\SEO\Dashboard\Domain\Taxonomies\Taxonomy;
use Yoast\WP\SEO\Dashboard\Infrastructure\Score_Groups\Score_Group_Link_Collector;
/**
* The current scores repository.
*/
class Current_Scores_Repository {
/**
* The score group link collector.
*
* @var Score_Group_Link_Collector
*/
protected $score_group_link_collector;
/**
* The constructor.
*
* @param Score_Group_Link_Collector $score_group_link_collector The score group link collector.
*/
public function __construct( Score_Group_Link_Collector $score_group_link_collector ) {
$this->score_group_link_collector = $score_group_link_collector;
}
/**
* Returns the current results.
*
* @param Score_Groups_Interface[] $score_groups The score groups.
* @param array<string, string> $score_results The score results.
* @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|int|array<string, string>>> The current results.
*/
public function get_current_scores( array $score_groups, array $score_results, Content_Type $content_type, ?Taxonomy $taxonomy, ?int $term_id ): Current_Scores_List {
$current_scores_list = new Current_Scores_List();
foreach ( $score_groups as $score_group ) {
$score_name = $score_group->get_name();
$current_score_links = $this->get_current_score_links( $score_group, $content_type, $taxonomy, $term_id );
$score_amount = (int) $score_results['scores']->$score_name;
$score_ids = ( isset( $score_results['score_ids'] ) ) ? $score_results['score_ids']->$score_name : null;
$current_score = new Current_Score( $score_name, $score_amount, $score_ids, $current_score_links );
$current_scores_list->add( $current_score, $score_group->get_position() );
}
return $current_scores_list;
}
/**
* Returns the links for the current scores of a score group.
*
* @param Score_Groups_Interface $score_group The scoure group.
* @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 current score links.
*/
protected function get_current_score_links( Score_Groups_Interface $score_group, Content_Type $content_type, ?Taxonomy $taxonomy, ?int $term_id ): array {
return [
'view' => $this->score_group_link_collector->get_view_link( $score_group, $content_type, $taxonomy, $term_id ),
];
}
}
@@ -0,0 +1,28 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
namespace Yoast\WP\SEO\Dashboard\Application\Score_Results\Readability_Score_Results;
use Yoast\WP\SEO\Dashboard\Application\Score_Results\Abstract_Score_Results_Repository;
use Yoast\WP\SEO\Dashboard\Domain\Score_Groups\Readability_Score_Groups\Readability_Score_Groups_Interface;
use Yoast\WP\SEO\Dashboard\Infrastructure\Score_Results\Readability_Score_Results\Cached_Readability_Score_Results_Collector;
/**
* The repository to get readability score results.
*/
class Readability_Score_Results_Repository extends Abstract_Score_Results_Repository {
/**
* The constructor.
*
* @param Cached_Readability_Score_Results_Collector $readability_score_results_collector The cached readability score results collector.
* @param Readability_Score_Groups_Interface ...$readability_score_groups All readability score groups.
*/
public function __construct(
Cached_Readability_Score_Results_Collector $readability_score_results_collector,
Readability_Score_Groups_Interface ...$readability_score_groups
) {
$this->score_results_collector = $readability_score_results_collector;
$this->score_groups = $readability_score_groups;
}
}
@@ -0,0 +1,28 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
namespace Yoast\WP\SEO\Dashboard\Application\Score_Results\SEO_Score_Results;
use Yoast\WP\SEO\Dashboard\Application\Score_Results\Abstract_Score_Results_Repository;
use Yoast\WP\SEO\Dashboard\Domain\Score_Groups\SEO_Score_Groups\SEO_Score_Groups_Interface;
use Yoast\WP\SEO\Dashboard\Infrastructure\Score_Results\SEO_Score_Results\Cached_SEO_Score_Results_Collector;
/**
* The repository to get SEO score results.
*/
class SEO_Score_Results_Repository extends Abstract_Score_Results_Repository {
/**
* The constructor.
*
* @param Cached_SEO_Score_Results_Collector $seo_score_results_collector The cached SEO score results collector.
* @param SEO_Score_Groups_Interface ...$seo_score_groups All SEO score groups.
*/
public function __construct(
Cached_SEO_Score_Results_Collector $seo_score_results_collector,
SEO_Score_Groups_Interface ...$seo_score_groups
) {
$this->score_results_collector = $seo_score_results_collector;
$this->score_groups = $seo_score_groups;
}
}
@@ -0,0 +1,61 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Application\Search_Rankings;
use Yoast\WP\SEO\Dashboard\Domain\Data_Provider\Dashboard_Repository_Interface;
use Yoast\WP\SEO\Dashboard\Domain\Data_Provider\Data_Container;
use Yoast\WP\SEO\Dashboard\Domain\Data_Provider\Parameters;
use Yoast\WP\SEO\Dashboard\Domain\Time_Based_Seo_Metrics\Data_Source_Not_Available_Exception;
use Yoast\WP\SEO\Dashboard\Infrastructure\Integrations\Site_Kit;
use Yoast\WP\SEO\Dashboard\Infrastructure\Search_Console\Site_Kit_Search_Console_Adapter;
/**
* The data provider for comparing search ranking related data.
*/
class Search_Ranking_Compare_Repository implements Dashboard_Repository_Interface {
/**
* The adapter.
*
* @var Site_Kit_Search_Console_Adapter
*/
private $site_kit_search_console_adapter;
/**
* The site kit configuration object.
*
* @var Site_Kit
*/
private $site_kit_configuration;
/**
* The constructor.
*
* @param Site_Kit_Search_Console_Adapter $site_kit_search_console_adapter The adapter.
* @param Site_Kit $site_kit_configuration The site kit configuration object.
*/
public function __construct(
Site_Kit_Search_Console_Adapter $site_kit_search_console_adapter,
Site_Kit $site_kit_configuration
) {
$this->site_kit_search_console_adapter = $site_kit_search_console_adapter;
$this->site_kit_configuration = $site_kit_configuration;
}
/**
* Gets the comparing search ranking data.
*
* @param Parameters $parameters The parameter to use for getting the comparing search ranking data.
*
* @return Data_Container
*
* @throws Data_Source_Not_Available_Exception When getting the comparing search ranking data fails.
*/
public function get_data( Parameters $parameters ): Data_Container {
if ( ! $this->site_kit_configuration->is_onboarded() ) {
throw new Data_Source_Not_Available_Exception( 'Comparison search ranking repository' );
}
return $this->site_kit_search_console_adapter->get_comparison_data( $parameters );
}
}
@@ -0,0 +1,74 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Application\Search_Rankings;
use Yoast\WP\SEO\Dashboard\Domain\Data_Provider\Dashboard_Repository_Interface;
use Yoast\WP\SEO\Dashboard\Domain\Data_Provider\Data_Container;
use Yoast\WP\SEO\Dashboard\Domain\Data_Provider\Parameters;
use Yoast\WP\SEO\Dashboard\Domain\Time_Based_Seo_Metrics\Data_Source_Not_Available_Exception;
use Yoast\WP\SEO\Dashboard\Infrastructure\Indexables\Top_Page_Indexable_Collector;
use Yoast\WP\SEO\Dashboard\Infrastructure\Integrations\Site_Kit;
use Yoast\WP\SEO\Dashboard\Infrastructure\Search_Console\Site_Kit_Search_Console_Adapter;
/**
* The data provider for top page data.
*/
class Top_Page_Repository implements Dashboard_Repository_Interface {
/**
* The adapter.
*
* @var Site_Kit_Search_Console_Adapter
*/
private $site_kit_search_console_adapter;
/**
* The top page indexable collector.
*
* @var Top_Page_Indexable_Collector
*/
private $top_page_indexable_collector;
/**
* The site kit configuration object.
*
* @var Site_Kit
*/
private $site_kit_configuration;
/**
* The constructor.
*
* @param Site_Kit_Search_Console_Adapter $site_kit_search_console_adapter The adapter.
* @param Top_Page_Indexable_Collector $top_page_indexable_collector The top page indexable collector.
* @param Site_Kit $site_kit_configuration The site kit configuration object.
*/
public function __construct(
Site_Kit_Search_Console_Adapter $site_kit_search_console_adapter,
Top_Page_Indexable_Collector $top_page_indexable_collector,
Site_Kit $site_kit_configuration
) {
$this->site_kit_search_console_adapter = $site_kit_search_console_adapter;
$this->top_page_indexable_collector = $top_page_indexable_collector;
$this->site_kit_configuration = $site_kit_configuration;
}
/**
* Gets the top pages' data.
*
* @param Parameters $parameters The parameter to use for getting the top pages.
*
* @return Data_Container
*
* @throws Data_Source_Not_Available_Exception When this repository is used without the needed prerequisites ready.
*/
public function get_data( Parameters $parameters ): Data_Container {
if ( ! $this->site_kit_configuration->is_onboarded() ) {
throw new Data_Source_Not_Available_Exception( 'Top page repository' );
}
$top_pages_search_ranking_data = $this->site_kit_search_console_adapter->get_data( $parameters );
$top_pages_full_data = $this->top_page_indexable_collector->get_data( $top_pages_search_ranking_data );
return $top_pages_full_data;
}
}
@@ -0,0 +1,61 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Application\Search_Rankings;
use Yoast\WP\SEO\Dashboard\Domain\Data_Provider\Dashboard_Repository_Interface;
use Yoast\WP\SEO\Dashboard\Domain\Data_Provider\Data_Container;
use Yoast\WP\SEO\Dashboard\Domain\Data_Provider\Parameters;
use Yoast\WP\SEO\Dashboard\Domain\Time_Based_Seo_Metrics\Data_Source_Not_Available_Exception;
use Yoast\WP\SEO\Dashboard\Infrastructure\Integrations\Site_Kit;
use Yoast\WP\SEO\Dashboard\Infrastructure\Search_Console\Site_Kit_Search_Console_Adapter;
/**
* The data provider for top query data.
*/
class Top_Query_Repository implements Dashboard_Repository_Interface {
/**
* The adapter.
*
* @var Site_Kit_Search_Console_Adapter
*/
private $site_kit_search_console_adapter;
/**
* The site kit configuration object.
*
* @var Site_Kit
*/
private $site_kit_configuration;
/**
* The constructor.
*
* @param Site_Kit_Search_Console_Adapter $site_kit_search_console_adapter The adapter.
* @param Site_Kit $site_kit_configuration The site kit configuration object.
*/
public function __construct(
Site_Kit_Search_Console_Adapter $site_kit_search_console_adapter,
Site_Kit $site_kit_configuration
) {
$this->site_kit_search_console_adapter = $site_kit_search_console_adapter;
$this->site_kit_configuration = $site_kit_configuration;
}
/**
* Gets the top queries' data.
*
* @param Parameters $parameters The parameter to use for getting the top queries.
*
* @return Data_Container
*
* @throws Data_Source_Not_Available_Exception When this repository is used without the needed prerequisites ready.
*/
public function get_data( Parameters $parameters ): Data_Container {
if ( ! $this->site_kit_configuration->is_onboarded() ) {
throw new Data_Source_Not_Available_Exception( 'Top queries repository' );
}
return $this->site_kit_search_console_adapter->get_data( $parameters );
}
}
@@ -0,0 +1,66 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\Dashboard\Application\Taxonomies;
use Yoast\WP\SEO\Dashboard\Application\Filter_Pairs\Filter_Pairs_Repository;
use Yoast\WP\SEO\Dashboard\Domain\Taxonomies\Taxonomy;
use Yoast\WP\SEO\Dashboard\Infrastructure\Taxonomies\Taxonomies_Collector;
/**
* The repository to get taxonomies.
*/
class Taxonomies_Repository {
/**
* The taxonomies collector.
*
* @var Taxonomies_Collector
*/
private $taxonomies_collector;
/**
* The filter pairs repository.
*
* @var Filter_Pairs_Repository
*/
private $filter_pairs_repository;
/**
* The constructor.
*
* @param Taxonomies_Collector $taxonomies_collector The taxonomies collector.
* @param Filter_Pairs_Repository $filter_pairs_repository The filter pairs repository.
*/
public function __construct(
Taxonomies_Collector $taxonomies_collector,
Filter_Pairs_Repository $filter_pairs_repository
) {
$this->taxonomies_collector = $taxonomies_collector;
$this->filter_pairs_repository = $filter_pairs_repository;
}
/**
* Returns the object of the filtering taxonomy of a content type.
*
* @param string $content_type The content type that the taxonomy filters.
*
* @return Taxonomy|null The filtering taxonomy of the content type.
*/
public function get_content_type_taxonomy( string $content_type ) {
// First we check if there's a filter that overrides the filtering taxonomy for this content type.
$taxonomy = $this->taxonomies_collector->get_custom_filtering_taxonomy( $content_type );
if ( $taxonomy ) {
return $taxonomy;
}
// Then we check if there is a filter explicitly made for this content type.
$taxonomy = $this->filter_pairs_repository->get_taxonomy( $content_type );
if ( $taxonomy ) {
return $taxonomy;
}
// If everything else returned empty, we can always try the fallback taxonomy.
return $this->taxonomies_collector->get_fallback_taxonomy( $content_type );
}
}
@@ -0,0 +1,87 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Application\Tracking;
use Yoast\WP\SEO\Dashboard\Infrastructure\Tracking\Setup_Steps_Tracking_Repository_Interface;
/**
* Tracks the setup steps.
*/
class Setup_Steps_Tracking {
/**
* The setup steps tracking repository.
*
* @var Setup_Steps_Tracking_Repository_Interface
*/
private $setup_steps_tracking_repository;
/**
* Constructs the class.
*
* @param Setup_Steps_Tracking_Repository_Interface $setup_steps_tracking_repository The setup steps tracking repository.
*/
public function __construct( Setup_Steps_Tracking_Repository_Interface $setup_steps_tracking_repository ) {
$this->setup_steps_tracking_repository = $setup_steps_tracking_repository;
}
/**
* If the Site Kit setup widget has been loaded.
*
* @return string "yes" on "no".
*/
public function get_setup_widget_loaded(): string {
return $this->setup_steps_tracking_repository->get_setup_steps_tracking_element( 'setup_widget_loaded' );
}
/**
* Gets the stage of the first interaction.
*
* @return string The stage name.
*/
public function get_first_interaction_stage(): string {
return $this->setup_steps_tracking_repository->get_setup_steps_tracking_element( 'first_interaction_stage' );
}
/**
* Gets the stage of the last interaction.
*
* @return string The stage name.
*/
public function get_last_interaction_stage(): string {
return $this->setup_steps_tracking_repository->get_setup_steps_tracking_element( 'last_interaction_stage' );
}
/**
* If the setup widget has been temporarily dismissed.
*
* @return string "yes" on "no".
*/
public function get_setup_widget_temporarily_dismissed(): string {
return $this->setup_steps_tracking_repository->get_setup_steps_tracking_element( 'setup_widget_temporarily_dismissed' );
}
/**
* If the setup widget has been permanently dismissed.
*
* @return string "yes" on "no".
*/
public function get_setup_widget_permanently_dismissed(): string {
return $this->setup_steps_tracking_repository->get_setup_steps_tracking_element( 'setup_widget_permanently_dismissed' );
}
/**
* Return this object represented by a key value array.
*
* @return array<string> The tracking data
*/
public function to_array(): array {
return [
'setupWidgetLoaded' => $this->get_setup_widget_loaded(),
'firstInteractionStage' => $this->get_first_interaction_stage(),
'lastInteractionStage' => $this->get_last_interaction_stage(),
'setupWidgetTemporarilyDismissed' => $this->get_setup_widget_temporarily_dismissed(),
'setupWidgetPermanentlyDismissed' => $this->get_setup_widget_permanently_dismissed(),
];
}
}
@@ -0,0 +1,61 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Application\Traffic;
use Yoast\WP\SEO\Dashboard\Domain\Data_Provider\Dashboard_Repository_Interface;
use Yoast\WP\SEO\Dashboard\Domain\Data_Provider\Data_Container;
use Yoast\WP\SEO\Dashboard\Domain\Data_Provider\Parameters;
use Yoast\WP\SEO\Dashboard\Domain\Time_Based_Seo_Metrics\Data_Source_Not_Available_Exception;
use Yoast\WP\SEO\Dashboard\Infrastructure\Analytics_4\Site_Kit_Analytics_4_Adapter;
use Yoast\WP\SEO\Dashboard\Infrastructure\Integrations\Site_Kit;
/**
* The data provider for comparison organic sessions data.
*/
class Organic_Sessions_Compare_Repository implements Dashboard_Repository_Interface {
/**
* The adapter.
*
* @var Site_Kit_Analytics_4_Adapter
*/
private $site_kit_analytics_4_adapter;
/**
* The site kit configuration object.
*
* @var Site_Kit
*/
private $site_kit_configuration;
/**
* The constructor.
*
* @param Site_Kit_Analytics_4_Adapter $site_kit_analytics_4_adapter The adapter.
* @param Site_Kit $site_kit_configuration The site kit configuration object.
*/
public function __construct(
Site_Kit_Analytics_4_Adapter $site_kit_analytics_4_adapter,
Site_Kit $site_kit_configuration
) {
$this->site_kit_analytics_4_adapter = $site_kit_analytics_4_adapter;
$this->site_kit_configuration = $site_kit_configuration;
}
/**
* Gets comparison organic sessions' data.
*
* @param Parameters $parameters The parameter to use for getting the comparison organic sessions' data.
*
* @return Data_Container
*
* @throws Data_Source_Not_Available_Exception When getting the comparison organic sessions' data fails.
*/
public function get_data( Parameters $parameters ): Data_Container {
if ( ! $this->site_kit_configuration->is_onboarded() || ! $this->site_kit_configuration->is_ga_connected() ) {
throw new Data_Source_Not_Available_Exception( 'Comparison organic sessions repository' );
}
return $this->site_kit_analytics_4_adapter->get_comparison_data( $parameters );
}
}
@@ -0,0 +1,61 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Application\Traffic;
use Yoast\WP\SEO\Dashboard\Domain\Data_Provider\Dashboard_Repository_Interface;
use Yoast\WP\SEO\Dashboard\Domain\Data_Provider\Data_Container;
use Yoast\WP\SEO\Dashboard\Domain\Data_Provider\Parameters;
use Yoast\WP\SEO\Dashboard\Domain\Time_Based_Seo_Metrics\Data_Source_Not_Available_Exception;
use Yoast\WP\SEO\Dashboard\Infrastructure\Analytics_4\Site_Kit_Analytics_4_Adapter;
use Yoast\WP\SEO\Dashboard\Infrastructure\Integrations\Site_Kit;
/**
* The data provider for daily organic sessions data.
*/
class Organic_Sessions_Daily_Repository implements Dashboard_Repository_Interface {
/**
* The adapter.
*
* @var Site_Kit_Analytics_4_Adapter
*/
private $site_kit_analytics_4_adapter;
/**
* The site kit configuration object.
*
* @var Site_Kit
*/
private $site_kit_configuration;
/**
* The constructor.
*
* @param Site_Kit_Analytics_4_Adapter $site_kit_analytics_4_adapter The adapter.
* @param Site_Kit $site_kit_configuration The site kit configuration object.
*/
public function __construct(
Site_Kit_Analytics_4_Adapter $site_kit_analytics_4_adapter,
Site_Kit $site_kit_configuration
) {
$this->site_kit_analytics_4_adapter = $site_kit_analytics_4_adapter;
$this->site_kit_configuration = $site_kit_configuration;
}
/**
* Gets daily organic sessions' data.
*
* @param Parameters $parameters The parameter to use for getting the daily organic sessions' data.
*
* @return Data_Container
*
* @throws Data_Source_Not_Available_Exception When this repository is used without the needed prerequisites ready.
*/
public function get_data( Parameters $parameters ): Data_Container {
if ( ! $this->site_kit_configuration->is_onboarded() || ! $this->site_kit_configuration->is_ga_connected() ) {
throw new Data_Source_Not_Available_Exception( 'Daily organic sessions repository' );
}
return $this->site_kit_analytics_4_adapter->get_daily_data( $parameters );
}
}
@@ -0,0 +1,21 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Domain\Analytics_4;
use Exception;
/**
* Exception for when an Analytics 4 request fails.
*/
class Failed_Request_Exception extends Exception {
/**
* Constructor of the exception.
*
* @param string $error_message The error message of the request.
* @param int $error_status_code The error status code of the request.
*/
public function __construct( $error_message, $error_status_code ) {
parent::__construct( 'The Analytics 4 request failed: ' . $error_message, $error_status_code );
}
}
@@ -0,0 +1,20 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Domain\Analytics_4;
use Exception;
/**
* Exception for when an Analytics 4 request is invalid.
*/
class Invalid_Request_Exception extends Exception {
/**
* Constructor of the exception.
*
* @param string $error_message The error message of the request.
*/
public function __construct( $error_message ) {
parent::__construct( 'The Analytics 4 request is invalid: ' . $error_message, 400 );
}
}
@@ -0,0 +1,18 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Domain\Analytics_4;
use Exception;
/**
* Exception for when an Analytics 4 request returns with an unexpected response.
*/
class Unexpected_Response_Exception extends Exception {
/**
* Constructor of the exception.
*/
public function __construct() {
parent::__construct( 'The response from Google Site Kit did not have an expected format.', 400 );
}
}
@@ -0,0 +1,83 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Domain\Content_Types;
use Yoast\WP\SEO\Dashboard\Domain\Taxonomies\Taxonomy;
/**
* This class describes a Content Type.
*/
class Content_Type {
/**
* The name of the content type.
*
* @var string
*/
private $name;
/**
* The label of the content type.
*
* @var string
*/
private $label;
/**
* The taxonomy that filters the content type.
*
* @var Taxonomy
*/
private $taxonomy;
/**
* The constructor.
*
* @param string $name The name of the 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 = null ) {
$this->name = $name;
$this->label = $label;
$this->taxonomy = $taxonomy;
}
/**
* Gets name of the content type.
*
* @return string The name of the content type.
*/
public function get_name(): string {
return $this->name;
}
/**
* Gets label of the content type.
*
* @return string The label of the content type.
*/
public function get_label(): string {
return $this->label;
}
/**
* Gets the taxonomy that filters the content type.
*
* @return Taxonomy|null The taxonomy that filters the content type.
*/
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;
}
}
@@ -0,0 +1,54 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Domain\Content_Types;
/**
* This class describes a list of content types.
*/
class Content_Types_List {
/**
* The content types.
*
* @var array<Content_Type>
*/
private $content_types = [];
/**
* Adds a content type to the list.
*
* @param Content_Type $content_type The content type to add.
*
* @return void
*/
public function add( Content_Type $content_type ): void {
$this->content_types[ $content_type->get_name() ] = $content_type;
}
/**
* Returns the content types in the list.
*
* @return array<Content_Type> The content types in the list.
*/
public function get(): array {
return $this->content_types;
}
/**
* Parses the content type list to the expected key value representation.
*
* @return array<array<string, array<string, array<string, array<string, string|null>>>>> The content type list presented as the expected key value representation.
*/
public function to_array(): array {
$array = [];
foreach ( $this->content_types as $content_type ) {
$array[] = [
'name' => $content_type->get_name(),
'label' => $content_type->get_label(),
'taxonomy' => ( $content_type->get_taxonomy() ) ? $content_type->get_taxonomy()->to_array() : null,
];
}
return $array;
}
}
@@ -0,0 +1,18 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Domain\Data_Provider;
/**
* Interface describing the way to get data for a specific data provider.
*/
interface Dashboard_Repository_Interface {
/**
* Method to get dashboard related data from a provider.
*
* @param Parameters $parameters The parameter to get the dashboard data for.
*
* @return Data_Container
*/
public function get_data( Parameters $parameters ): Data_Container;
}
@@ -0,0 +1,57 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Domain\Data_Provider;
/**
* The data container.
*/
class Data_Container {
/**
* All the data points.
*
* @var array<Data_Interface>
*/
private $data_container;
/**
* The constructor
*/
public function __construct() {
$this->data_container = [];
}
/**
* Method to add data.
*
* @param Data_Interface $data The data.
*
* @return void
*/
public function add_data( Data_Interface $data ) {
$this->data_container[] = $data;
}
/**
* Method to get all the data points.
*
* @return Data_Interface[] All the data points.
*/
public function get_data(): array {
return $this->data_container;
}
/**
* Converts the data points into an array.
*
* @return array<string, string> The array of the data points.
*/
public function to_array(): array {
$result = [];
foreach ( $this->data_container as $data ) {
$result[] = $data->to_array();
}
return $result;
}
}
@@ -0,0 +1,16 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Domain\Data_Provider;
/**
* The interface to describe the data domain.
*/
interface Data_Interface {
/**
* A to array method.
*
* @return array<string>
*/
public function to_array(): array;
}
@@ -0,0 +1,145 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Domain\Data_Provider;
/**
* Object representation of the request parameters.
*/
abstract class Parameters {
/**
* The start date.
*
* @var string
*/
private $start_date;
/**
* The end date.
*
* @var string
*/
private $end_date;
/**
* The amount of results.
*
* @var int
*/
private $limit = 0;
/**
* The compare start date.
*
* @var string
*/
private $compare_start_date;
/**
* The compare end date.
*
* @var string
*/
private $compare_end_date;
/**
* Getter for the start date.
*
* @return string
*/
public function get_start_date(): string {
return $this->start_date;
}
/**
* Getter for the end date.
* The date format should be Y-M-D.
*
* @return string
*/
public function get_end_date(): string {
return $this->end_date;
}
/**
* Getter for the result limit.
*
* @return int
*/
public function get_limit(): int {
return $this->limit;
}
/**
* Getter for the compare start date.
*
* @return string
*/
public function get_compare_start_date(): ?string {
return $this->compare_start_date;
}
/**
* Getter for the compare end date.
* The date format should be Y-M-D.
*
* @return string
*/
public function get_compare_end_date(): ?string {
return $this->compare_end_date;
}
/**
* The start date setter.
*
* @param string $start_date The start date.
*
* @return void
*/
public function set_start_date( string $start_date ): void {
$this->start_date = $start_date;
}
/**
* The end date setter.
*
* @param string $end_date The end date.
*
* @return void
*/
public function set_end_date( string $end_date ): void {
$this->end_date = $end_date;
}
/**
* The result limit.
*
* @param int $limit The result limit.
* @return void
*/
public function set_limit( int $limit ): void {
$this->limit = $limit;
}
/**
* The compare start date setter.
*
* @param string $compare_start_date The compare start date.
*
* @return void
*/
public function set_compare_start_date( string $compare_start_date ): void {
$this->compare_start_date = $compare_start_date;
}
/**
* The compare end date setter.
*
* @param string $compare_end_date The compare end date.
*
* @return void
*/
public function set_compare_end_date( string $compare_end_date ): void {
$this->compare_end_date = $compare_end_date;
}
}
@@ -0,0 +1,34 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Domain\Endpoint;
interface Endpoint_Interface {
/**
* Gets the name.
*
* @return string
*/
public function get_name(): string;
/**
* Gets the namespace.
*
* @return string
*/
public function get_namespace(): string;
/**
* Gets the route.
*
* @return string
*/
public function get_route(): string;
/**
* Gets the URL.
*
* @return string
*/
public function get_url(): string;
}
@@ -0,0 +1,41 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Domain\Endpoint;
/**
* List of endpoints.
*/
class Endpoint_List {
/**
* Holds the endpoints.
*
* @var array<Endpoint_Interface>
*/
private $endpoints = [];
/**
* Adds an endpoint to the list.
*
* @param Endpoint_Interface $endpoint An endpoint.
*
* @return void
*/
public function add_endpoint( Endpoint_Interface $endpoint ): void {
$this->endpoints[] = $endpoint;
}
/**
* Converts the list to an array.
*
* @return array<string, string> The array of endpoints.
*/
public function to_array(): array {
$result = [];
foreach ( $this->endpoints as $endpoint ) {
$result[ $endpoint->get_name() ] = $endpoint->get_url();
}
return $result;
}
}
@@ -0,0 +1,23 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Domain\Filter_Pairs;
/**
* This interface describes a Filter Pair implementation.
*/
interface Filter_Pairs_Interface {
/**
* Gets the filtering taxonomy.
*
* @return string
*/
public function get_filtering_taxonomy(): string;
/**
* Gets the filtered content type.
*
* @return string
*/
public function get_filtered_content_type(): string;
}
@@ -0,0 +1,27 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Domain\Filter_Pairs;
/**
* This class describes the product category filter pair.
*/
class Product_Category_Filter_Pair implements Filter_Pairs_Interface {
/**
* Gets the filtering taxonomy.
*
* @return string The filtering taxonomy.
*/
public function get_filtering_taxonomy(): string {
return 'product_cat';
}
/**
* Gets the filtered content type.
*
* @return string The filtered content type.
*/
public function get_filtered_content_type(): string {
return 'product';
}
}
@@ -0,0 +1,51 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Domain\Score_Groups;
/**
* Abstract class for a score group.
*/
abstract class Abstract_Score_Group implements Score_Groups_Interface {
/**
* The name of the score group.
*
* @var string
*/
private $name;
/**
* The key of the score group that is used when filtering on the posts page.
*
* @var string
*/
private $filter_key;
/**
* The value of the score group that is used when filtering on the posts page.
*
* @var string
*/
private $filter_value;
/**
* The min score of the score group.
*
* @var int
*/
private $min_score;
/**
* The max score of the score group.
*
* @var int
*/
private $max_score;
/**
* The position of the score group.
*
* @var int
*/
private $position;
}
@@ -0,0 +1,21 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
namespace Yoast\WP\SEO\Dashboard\Domain\Score_Groups\Readability_Score_Groups;
use Yoast\WP\SEO\Dashboard\Domain\Score_Groups\Abstract_Score_Group;
/**
* Abstract class for a readability score group.
*/
abstract class Abstract_Readability_Score_Group extends Abstract_Score_Group implements Readability_Score_Groups_Interface {
/**
* Gets the key of the readability score group that is used when filtering on the posts page.
*
* @return string The name of the readability score group that is used when filtering on the posts page.
*/
public function get_filter_key(): string {
return 'readability_filter';
}
}
@@ -0,0 +1,55 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
namespace Yoast\WP\SEO\Dashboard\Domain\Score_Groups\Readability_Score_Groups;
/**
* This class describes a bad readability score group.
*/
class Bad_Readability_Score_Group extends Abstract_Readability_Score_Group {
/**
* Gets the name of the readability score group.
*
* @return string The name of the readability score group.
*/
public function get_name(): string {
return 'bad';
}
/**
* Gets the value of the readability score group that is used when filtering on the posts page.
*
* @return string The name of the readability score group that is used when filtering on the posts page.
*/
public function get_filter_value(): string {
return 'bad';
}
/**
* Gets the position of the readability score group.
*
* @return int The position of the readability score group.
*/
public function get_position(): int {
return 2;
}
/**
* Gets the minimum score of the readability score group.
*
* @return int|null The minimum score of the readability score group.
*/
public function get_min_score(): ?int {
return 1;
}
/**
* Gets the maximum score of the readability score group.
*
* @return int|null The maximum score of the readability score group.
*/
public function get_max_score(): ?int {
return 40;
}
}
@@ -0,0 +1,55 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
namespace Yoast\WP\SEO\Dashboard\Domain\Score_Groups\Readability_Score_Groups;
/**
* This class describes a good readability score group.
*/
class Good_Readability_Score_Group extends Abstract_Readability_Score_Group {
/**
* Gets the name of the readability score group.
*
* @return string The name of the readability score group.
*/
public function get_name(): string {
return 'good';
}
/**
* Gets the value of the readability score group that is used when filtering on the posts page.
*
* @return string The name of the readability score group that is used when filtering on the posts page.
*/
public function get_filter_value(): string {
return 'good';
}
/**
* Gets the position of the readability score group.
*
* @return int The position of the readability score group.
*/
public function get_position(): int {
return 0;
}
/**
* Gets the minimum score of the readability score group.
*
* @return int|null The minimum score of the readability score group.
*/
public function get_min_score(): ?int {
return 71;
}
/**
* Gets the maximum score of the readability score group.
*
* @return int|null The maximum score of the readability score group.
*/
public function get_max_score(): ?int {
return 100;
}
}
@@ -0,0 +1,55 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
namespace Yoast\WP\SEO\Dashboard\Domain\Score_Groups\Readability_Score_Groups;
/**
* This class describes a missing readability score group.
*/
class No_Readability_Score_Group extends Abstract_Readability_Score_Group {
/**
* Gets the name of the readability score group.
*
* @return string The name of the readability score group.
*/
public function get_name(): string {
return 'notAnalyzed';
}
/**
* Gets the value of the readability score group that is used when filtering on the posts page.
*
* @return string The name of the readability score group that is used when filtering on the posts page.
*/
public function get_filter_value(): string {
return 'na';
}
/**
* Gets the position of the readability score group.
*
* @return int The position of the readability score group.
*/
public function get_position(): int {
return 3;
}
/**
* Gets the minimum score of the readability score group.
*
* @return int|null The minimum score of the readability score group.
*/
public function get_min_score(): ?int {
return null;
}
/**
* Gets the maximum score of the readability score group.
*
* @return int|null The maximum score of the readability score group.
*/
public function get_max_score(): ?int {
return null;
}
}
@@ -0,0 +1,55 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
namespace Yoast\WP\SEO\Dashboard\Domain\Score_Groups\Readability_Score_Groups;
/**
* This class describes an OK readability score group.
*/
class Ok_Readability_Score_Group extends Abstract_Readability_Score_Group {
/**
* Gets the name of the readability score group.
*
* @return string The the name of the readability score group.
*/
public function get_name(): string {
return 'ok';
}
/**
* Gets the value of the readability score group that is used when filtering on the posts page.
*
* @return string The name of the readability score group that is used when filtering on the posts page.
*/
public function get_filter_value(): string {
return 'ok';
}
/**
* Gets the position of the readability score group.
*
* @return int The position of the readability score group.
*/
public function get_position(): int {
return 1;
}
/**
* Gets the minimum score of the readability score group.
*
* @return int|null The minimum score of the readability score group.
*/
public function get_min_score(): ?int {
return 41;
}
/**
* Gets the maximum score of the readability score group.
*
* @return int|null The maximum score of the readability score group.
*/
public function get_max_score(): ?int {
return 70;
}
}
@@ -0,0 +1,11 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName
namespace Yoast\WP\SEO\Dashboard\Domain\Score_Groups\Readability_Score_Groups;
use Yoast\WP\SEO\Dashboard\Domain\Score_Groups\Score_Groups_Interface;
/**
* This interface describes a readability score group implementation.
*/
interface Readability_Score_Groups_Interface extends Score_Groups_Interface {
}
@@ -0,0 +1,51 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Domain\Score_Groups;
/**
* This interface describes a score group implementation.
*/
interface Score_Groups_Interface {
/**
* Gets the name of the score group.
*
* @return string
*/
public function get_name(): string;
/**
* Gets the key of the score group that is used when filtering on the posts page.
*
* @return string
*/
public function get_filter_key(): string;
/**
* Gets the value of the score group that is used when filtering on the posts page.
*
* @return string
*/
public function get_filter_value(): string;
/**
* Gets the minimum score of the score group.
*
* @return int|null
*/
public function get_min_score(): ?int;
/**
* Gets the maximum score of the score group.
*
* @return int|null
*/
public function get_max_score(): ?int;
/**
* Gets the position of the score group.
*
* @return int
*/
public function get_position(): int;
}
@@ -0,0 +1,21 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
namespace Yoast\WP\SEO\Dashboard\Domain\Score_Groups\SEO_Score_Groups;
use Yoast\WP\SEO\Dashboard\Domain\Score_Groups\Abstract_Score_Group;
/**
* Abstract class for an SEO score group.
*/
abstract class Abstract_SEO_Score_Group extends Abstract_Score_Group implements SEO_Score_Groups_Interface {
/**
* Gets the key of the SEO score group that is used when filtering on the posts page.
*
* @return string The name of the SEO score group that is used when filtering on the posts page.
*/
public function get_filter_key(): string {
return 'seo_filter';
}
}
@@ -0,0 +1,55 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
namespace Yoast\WP\SEO\Dashboard\Domain\Score_Groups\SEO_Score_Groups;
/**
* This class describes a bad SEO score group.
*/
class Bad_SEO_Score_Group extends Abstract_SEO_Score_Group {
/**
* Gets the name of the SEO score group.
*
* @return string The name of the SEO score group.
*/
public function get_name(): string {
return 'bad';
}
/**
* Gets the value of the SEO score group that is used when filtering on the posts page.
*
* @return string The name of the SEO score group that is used when filtering on the posts page.
*/
public function get_filter_value(): string {
return 'bad';
}
/**
* Gets the position of the SEO score group.
*
* @return int The position of the SEO score group.
*/
public function get_position(): int {
return 2;
}
/**
* Gets the minimum score of the SEO score group.
*
* @return int|null The minimum score of the SEO score group.
*/
public function get_min_score(): ?int {
return 1;
}
/**
* Gets the maximum score of the SEO score group.
*
* @return int|null The maximum score of the SEO score group.
*/
public function get_max_score(): ?int {
return 40;
}
}
@@ -0,0 +1,55 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
namespace Yoast\WP\SEO\Dashboard\Domain\Score_Groups\SEO_Score_Groups;
/**
* This class describes a good SEO score group.
*/
class Good_SEO_Score_Group extends Abstract_SEO_Score_Group {
/**
* Gets the name of the SEO score group.
*
* @return string The name of the SEO score group.
*/
public function get_name(): string {
return 'good';
}
/**
* Gets the value of the SEO score group that is used when filtering on the posts page.
*
* @return string The name of the SEO score group that is used when filtering on the posts page.
*/
public function get_filter_value(): string {
return 'good';
}
/**
* Gets the position of the SEO score group.
*
* @return int The position of the SEO score group.
*/
public function get_position(): int {
return 0;
}
/**
* Gets the minimum score of the SEO score group.
*
* @return int|null The minimum score of the SEO score group.
*/
public function get_min_score(): ?int {
return 71;
}
/**
* Gets the maximum score of the SEO score group.
*
* @return int|null The maximum score of the SEO score group.
*/
public function get_max_score(): ?int {
return 100;
}
}
@@ -0,0 +1,55 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
namespace Yoast\WP\SEO\Dashboard\Domain\Score_Groups\SEO_Score_Groups;
/**
* This class describes a missing SEO score group.
*/
class No_SEO_Score_Group extends Abstract_SEO_Score_Group {
/**
* Gets the name of the SEO score group.
*
* @return string The name of the SEO score group.
*/
public function get_name(): string {
return 'notAnalyzed';
}
/**
* Gets the value of the SEO score group that is used when filtering on the posts page.
*
* @return string The name of the SEO score group that is used when filtering on the posts page.
*/
public function get_filter_value(): string {
return 'na';
}
/**
* Gets the position of the SEO score group.
*
* @return int The position of the SEO score group.
*/
public function get_position(): int {
return 3;
}
/**
* Gets the minimum score of the SEO score group.
*
* @return int|null The minimum score of the SEO score group.
*/
public function get_min_score(): ?int {
return null;
}
/**
* Gets the maximum score of the SEO score group.
*
* @return int|null The maximum score of the SEO score group.
*/
public function get_max_score(): ?int {
return null;
}
}
@@ -0,0 +1,55 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
namespace Yoast\WP\SEO\Dashboard\Domain\Score_Groups\SEO_Score_Groups;
/**
* This class describes an OK SEO score group.
*/
class Ok_SEO_Score_Group extends Abstract_SEO_Score_Group {
/**
* Gets the name of the SEO score group.
*
* @return string The the name of the SEO score group.
*/
public function get_name(): string {
return 'ok';
}
/**
* Gets the value of the SEO score group that is used when filtering on the posts page.
*
* @return string The name of the SEO score group that is used when filtering on the posts page.
*/
public function get_filter_value(): string {
return 'ok';
}
/**
* Gets the position of the SEO score group.
*
* @return int The position of the SEO score group.
*/
public function get_position(): int {
return 1;
}
/**
* Gets the minimum score of the SEO score group.
*
* @return int|null The minimum score of the SEO score group.
*/
public function get_min_score(): ?int {
return 41;
}
/**
* Gets the maximum score of the SEO score group.
*
* @return int|null The maximum score of the SEO score group.
*/
public function get_max_score(): ?int {
return 70;
}
}
@@ -0,0 +1,10 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName
namespace Yoast\WP\SEO\Dashboard\Domain\Score_Groups\SEO_Score_Groups;
use Yoast\WP\SEO\Dashboard\Domain\Score_Groups\Score_Groups_Interface;
/**
* This interface describes an SEO score group implementation.
*/
interface SEO_Score_Groups_Interface extends Score_Groups_Interface {}
@@ -0,0 +1,100 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Domain\Score_Results;
/**
* This class describes a current score.
*/
class Current_Score {
/**
* The name of the current score.
*
* @var string
*/
private $name;
/**
* The amount of the current score.
*
* @var string
*/
private $amount;
/**
* The ids of the current score.
*
* @var string|null
*/
private $ids;
/**
* The links of the current score.
*
* @var array<string, string>|null
*/
private $links;
/**
* The constructor.
*
* @param string $name The name of the current score.
* @param int $amount The amount of the current score.
* @param string|null $ids The ids of the current score.
* @param array<string, string>|null $links The links of the current score.
*/
public function __construct( string $name, int $amount, ?string $ids = null, ?array $links = null ) {
$this->name = $name;
$this->amount = $amount;
$this->ids = $ids;
$this->links = $links;
}
/**
* Gets name of the current score.
*
* @return string The name of the current score.
*/
public function get_name(): string {
return $this->name;
}
/**
* Gets the amount of the current score.
*
* @return int The amount of the current score.
*/
public function get_amount(): int {
return $this->amount;
}
/**
* Gets the ids of the current score.
*
* @return string|null The ids of the current score.
*/
public function get_ids(): ?string {
return $this->ids;
}
/**
* Gets the links of the current score in the expected key value representation.
*
* @return array<string, string> The links of the current score in the expected key value representation.
*/
public function get_links_to_array(): ?array {
$links = [];
if ( $this->links === null ) {
return $links;
}
foreach ( $this->links as $key => $link ) {
if ( $link === null ) {
continue;
}
$links[ $key ] = $link;
}
return $links;
}
}
@@ -0,0 +1,53 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Domain\Score_Results;
/**
* This class describes a list of current scores.
*/
class Current_Scores_List {
/**
* The current scores.
*
* @var Current_Score[]
*/
private $current_scores = [];
/**
* Adds a current score to the list.
*
* @param Current_Score $current_score The current score to add.
* @param int $position The position to add the current score.
*
* @return void
*/
public function add( Current_Score $current_score, int $position ): void {
$this->current_scores[ $position ] = $current_score;
}
/**
* Parses the current score list to the expected key value representation.
*
* @return array<array<string, string|int|array<string, string>>> The score list presented as the expected key value representation.
*/
public function to_array(): array {
$array = [];
\ksort( $this->current_scores );
foreach ( $this->current_scores as $key => $current_score ) {
$array[] = [
'name' => $current_score->get_name(),
'amount' => $current_score->get_amount(),
'links' => $current_score->get_links_to_array(),
];
if ( $current_score->get_ids() !== null ) {
$array[ $key ]['ids'] = $current_score->get_ids();
}
}
return $array;
}
}
@@ -0,0 +1,56 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Domain\Score_Results;
/**
* This class describes a score result.
*/
class Score_Result {
/**
* The list of the current scores of the score result.
*
* @var Current_Scores_List
*/
private $current_scores_list;
/**
* The time the query took to get the score results.
*
* @var float
*/
private $query_time;
/**
* Whether cache was used to get the score results.
*
* @var bool
*/
private $is_cached_used;
/**
* The constructor.
*
* @param Current_Scores_List $current_scores_list The list of the current scores of the score result.
* @param float $query_time The time the query took to get the score results.
* @param bool $is_cached_used Whether cache was used to get the score results.
*/
public function __construct( Current_Scores_List $current_scores_list, float $query_time, bool $is_cached_used ) {
$this->current_scores_list = $current_scores_list;
$this->query_time = $query_time;
$this->is_cached_used = $is_cached_used;
}
/**
* Return this object represented by a key value array.
*
* @return array<string, array<array<string, string|int|array<string, string>>>|float|bool> Returns the name and if the feature is enabled.
*/
public function to_array(): array {
return [
'scores' => $this->current_scores_list->to_array(),
'queryTime' => $this->query_time,
'cacheUsed' => $this->is_cached_used,
];
}
}
@@ -0,0 +1,18 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Domain\Score_Results;
use Exception;
/**
* Exception for when score results are not found.
*/
class Score_Results_Not_Found_Exception extends Exception {
/**
* Constructor of the exception.
*/
public function __construct() {
parent::__construct( 'Score results not found', 500 );
}
}
@@ -0,0 +1,21 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Domain\Search_Console;
use Exception;
/**
* Exception for when a search console request fails.
*/
class Failed_Request_Exception extends Exception {
/**
* Constructor of the exception.
*
* @param string $error_message The error message of the request.
* @param int $error_status_code The error status code of the request.
*/
public function __construct( $error_message, $error_status_code ) {
parent::__construct( 'The Search Console request failed: ' . $error_message, $error_status_code );
}
}
@@ -0,0 +1,18 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Domain\Search_Console;
use Exception;
/**
* Exception for when a Search Console request returns with an unexpected response.
*/
class Unexpected_Response_Exception extends Exception {
/**
* Constructor of the exception.
*/
public function __construct() {
parent::__construct( 'The response from Google Site Kit did not have an expected format.', 400 );
}
}
@@ -0,0 +1,87 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Domain\Search_Rankings;
use Yoast\WP\SEO\Dashboard\Domain\Data_Provider\Data_Interface;
/**
* Domain object that represents a Comparison Search Ranking record.
*/
class Comparison_Search_Ranking_Data implements Data_Interface {
/**
* The current search ranking data.
*
* @var Search_Ranking_Data[]
*/
private $current_search_ranking_data = [];
/**
* The previous search ranking data.
*
* @var Search_Ranking_Data[]
*/
private $previous_search_ranking_data = [];
/**
* Sets the current search ranking data.
*
* @param Search_Ranking_Data $current_search_ranking_data The current search ranking data.
*
* @return void
*/
public function add_current_traffic_data( Search_Ranking_Data $current_search_ranking_data ): void {
\array_push( $this->current_search_ranking_data, $current_search_ranking_data );
}
/**
* Sets the previous search ranking data.
*
* @param Search_Ranking_Data $previous_search_ranking_data The previous search ranking data.
*
* @return void
*/
public function add_previous_traffic_data( Search_Ranking_Data $previous_search_ranking_data ): void {
\array_push( $this->previous_search_ranking_data, $previous_search_ranking_data );
}
/**
* The array representation of this domain object.
*
* @return array<array<string, int>>
*/
public function to_array(): array {
return [
'current' => $this->parse_data( $this->current_search_ranking_data ),
'previous' => $this->parse_data( $this->previous_search_ranking_data ),
];
}
/**
* Parses search ranking data into the expected format.
*
* @param Search_Ranking_Data[] $search_ranking_data The search ranking data to be parsed.
*
* @return array<string, int> The parsed data
*/
private function parse_data( array $search_ranking_data ): array {
$parsed_data = [
'total_clicks' => 0,
'total_impressions' => 0,
];
$weighted_postion = 0;
foreach ( $search_ranking_data as $search_ranking ) {
$parsed_data['total_clicks'] += $search_ranking->get_clicks();
$parsed_data['total_impressions'] += $search_ranking->get_impressions();
$weighted_postion += ( $search_ranking->get_position() * $search_ranking->get_impressions() );
}
if ( $parsed_data['total_impressions'] !== 0 ) {
$parsed_data['average_ctr'] = ( $parsed_data['total_clicks'] / $parsed_data['total_impressions'] );
$parsed_data['average_position'] = ( $weighted_postion / $parsed_data['total_impressions'] );
}
return $parsed_data;
}
}
@@ -0,0 +1,123 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Domain\Search_Rankings;
use Yoast\WP\SEO\Dashboard\Domain\Data_Provider\Data_Interface;
/**
* Domain object that represents a single Search Ranking Data record.
*/
class Search_Ranking_Data implements Data_Interface {
/**
* The amount of clicks a `subject` gets.
*
* @var int
*/
private $clicks;
/**
* The click-through rate a `subject` gets.
*
* @var float
*/
private $ctr;
/**
* The amount of impressions a `subject` gets.
*
* @var int
*/
private $impressions;
/**
* The average position for the given `subject`.
*
* @var float
*/
private $position;
/**
* In the context of this domain object subject can represent a `URI` or a `search term`
*
* @var string
*/
private $subject;
/**
* The constructor.
*
* @param int $clicks The clicks.
* @param float $ctr The ctr.
* @param int $impressions The impressions.
* @param float $position The position.
* @param string $subject The subject of the data.
*/
public function __construct( int $clicks, float $ctr, int $impressions, float $position, string $subject ) {
$this->clicks = $clicks;
$this->ctr = $ctr;
$this->impressions = $impressions;
$this->position = $position;
$this->subject = $subject;
}
/**
* The array representation of this domain object.
*
* @return array<string|float|int|string[]>
*/
public function to_array(): array {
return [
'clicks' => $this->clicks,
'ctr' => $this->ctr,
'impressions' => $this->impressions,
'position' => $this->position,
'subject' => $this->subject,
];
}
/**
* Gets the clicks.
*
* @return string The clicks.
*/
public function get_clicks(): string {
return $this->clicks;
}
/**
* Gets the click-through rate.
*
* @return string The click-through rate.
*/
public function get_ctr(): string {
return $this->ctr;
}
/**
* Gets the impressions.
*
* @return string The impressions.
*/
public function get_impressions(): string {
return $this->impressions;
}
/**
* Gets the position.
*
* @return string The position.
*/
public function get_position(): string {
return $this->position;
}
/**
* Gets the subject.
*
* @return string The subject.
*/
public function get_subject(): string {
return $this->subject;
}
}
@@ -0,0 +1,67 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Domain\Search_Rankings;
use Yoast\WP\SEO\Dashboard\Domain\Data_Provider\Data_Interface;
use Yoast\WP\SEO\Dashboard\Domain\Score_Groups\SEO_Score_Groups\SEO_Score_Groups_Interface;
/**
* Domain object that represents a single Top Page Data record.
*/
class Top_Page_Data implements Data_Interface {
/**
* The search ranking data for the top page.
*
* @var Search_Ranking_Data
*/
private $search_ranking_data;
/**
* The SEO score group the top page belongs to.
*
* @var SEO_Score_Groups_Interface
*/
private $seo_score_group;
/**
* The edit link of the top page.
*
* @var string
*/
private $edit_link;
/**
* The constructor.
*
* @param Search_Ranking_Data $search_ranking_data The search ranking data for the top page.
* @param SEO_Score_Groups_Interface $seo_score_group The SEO score group the top page belongs to.
* @param string $edit_link The edit link of the top page.
*/
public function __construct(
Search_Ranking_Data $search_ranking_data,
SEO_Score_Groups_Interface $seo_score_group,
?string $edit_link = null
) {
$this->search_ranking_data = $search_ranking_data;
$this->seo_score_group = $seo_score_group;
$this->edit_link = $edit_link;
}
/**
* The array representation of this domain object.
*
* @return array<string|float|int|string[]>
*/
public function to_array(): array {
$top_page_data = $this->search_ranking_data->to_array();
$top_page_data['seoScore'] = $this->seo_score_group->get_name();
$top_page_data['links'] = [];
if ( $this->edit_link !== null ) {
$top_page_data['links']['edit'] = $this->edit_link;
}
return $top_page_data;
}
}
@@ -0,0 +1,71 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Domain\Taxonomies;
/**
* This class describes a Taxonomy.
*/
class Taxonomy {
/**
* The name of the taxonomy.
*
* @var string
*/
private $name;
/**
* The label of the taxonomy.
*
* @var string
*/
private $label;
/**
* The REST URL of the taxonomy.
*
* @var string
*/
private $rest_url;
/**
* The constructor.
*
* @param string $name The name of the taxonomy.
* @param string $label The label of the taxonomy.
* @param string $rest_url The REST URL of the taxonomy.
*/
public function __construct(
string $name,
string $label,
string $rest_url
) {
$this->name = $name;
$this->label = $label;
$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.
*
* @return array<string, array<string, string>> The taxonomy presented as the expected key value representation.
*/
public function to_array(): array {
return [
'name' => $this->name,
'label' => $this->label,
'links' => [
'search' => $this->rest_url,
],
];
}
}
@@ -0,0 +1,20 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Domain\Time_Based_Seo_Metrics;
use Exception;
/**
* Exception for when the integration is not yet onboarded.
*/
class Data_Source_Not_Available_Exception extends Exception {
/**
* Constructor of the exception.
*
* @param string $data_source_name The name of the data source that is not found.
*/
public function __construct( $data_source_name ) {
parent::__construct( "$data_source_name is not available yet. Not all prerequisites have been met.", 400 );
}
}
@@ -0,0 +1,18 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Domain\Time_Based_SEO_Metrics;
use Exception;
/**
* Exception for when the repository for the given widget are not found.
*/
class Repository_Not_Found_Exception extends Exception {
/**
* Constructor of the exception.
*/
public function __construct() {
parent::__construct( 'Repository not found', 404 );
}
}
@@ -0,0 +1,73 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Domain\Traffic;
use Yoast\WP\SEO\Dashboard\Domain\Data_Provider\Data_Interface;
/**
* Domain object that represents a single Comparison Traffic record.
*/
class Comparison_Traffic_Data implements Data_Interface {
public const CURRENT_PERIOD_KEY = 'current';
public const PREVIOUS_PERIOD_KEY = 'previous';
/**
* The current traffic data.
*
* @var Traffic_Data
*/
private $current_traffic_data;
/**
* The previous traffic data.
*
* @var Traffic_Data
*/
private $previous_traffic_data;
/**
* The constructor.
*
* @param Traffic_Data $current_traffic_data The current traffic data.
* @param Traffic_Data $previous_traffic_data The previous traffic data.
*/
public function __construct( ?Traffic_Data $current_traffic_data = null, ?Traffic_Data $previous_traffic_data = null ) {
$this->current_traffic_data = $current_traffic_data;
$this->previous_traffic_data = $previous_traffic_data;
}
/**
* Sets the current traffic data.
*
* @param Traffic_Data $current_traffic_data The current traffic data.
*
* @return void
*/
public function set_current_traffic_data( Traffic_Data $current_traffic_data ): void {
$this->current_traffic_data = $current_traffic_data;
}
/**
* Sets the previous traffic data.
*
* @param Traffic_Data $previous_traffic_data The previous traffic data.
*
* @return void
*/
public function set_previous_traffic_data( Traffic_Data $previous_traffic_data ): void {
$this->previous_traffic_data = $previous_traffic_data;
}
/**
* The array representation of this domain object.
*
* @return array<string|float|int|string[]>
*/
public function to_array(): array {
return [
'current' => $this->current_traffic_data->to_array(),
'previous' => $this->previous_traffic_data->to_array(),
];
}
}
@@ -0,0 +1,48 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Domain\Traffic;
use Yoast\WP\SEO\Dashboard\Domain\Data_Provider\Data_Interface;
/**
* Domain object that represents a single Daily Traffic record.
*/
class Daily_Traffic_Data implements Data_Interface {
/**
* The date of the traffic data, in YYYYMMDD format.
*
* @var string
*/
private $date;
/**
* The traffic data for the date.
*
* @var Traffic_Data
*/
private $traffic_data;
/**
* The constructor.
*
* @param string $date The date of the traffic data, in YYYYMMDD format.
* @param Traffic_Data $traffic_data The traffic data for the date.
*/
public function __construct( string $date, Traffic_Data $traffic_data ) {
$this->date = $date;
$this->traffic_data = $traffic_data;
}
/**
* The array representation of this domain object.
*
* @return array<string, string|int>
*/
public function to_array(): array {
$result = [];
$result['date'] = $this->date;
return \array_merge( $result, $this->traffic_data->to_array() );
}
}
@@ -0,0 +1,66 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Domain\Traffic;
use Yoast\WP\SEO\Dashboard\Domain\Data_Provider\Data_Interface;
/**
* Domain object that represents a single Traffic record.
*/
class Traffic_Data implements Data_Interface {
/**
* The sessions, if any.
*
* @var int|null
*/
private $sessions;
/**
* The total users, if any.
*
* @var int|null
*/
private $total_users;
/**
* The array representation of this domain object.
*
* @return array<string, int>
*/
public function to_array(): array {
$result = [];
if ( $this->sessions !== null ) {
$result['sessions'] = $this->sessions;
}
if ( $this->total_users !== null ) {
$result['total_users'] = $this->total_users;
}
return $result;
}
/**
* Sets the sessions.
*
* @param int $sessions The sessions.
*
* @return void
*/
public function set_sessions( int $sessions ): void {
$this->sessions = $sessions;
}
/**
* Sets the total users.
*
* @param int $total_users The total users.
*
* @return void
*/
public function set_total_users( int $total_users ): void {
$this->total_users = $total_users;
}
}
@@ -0,0 +1,138 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Infrastructure\Analytics_4;
use Yoast\WP\SEO\Dashboard\Domain\Data_Provider\Parameters;
/**
* Domain object to add Analytics 4 specific data to the parameters.
*/
class Analytics_4_Parameters extends Parameters {
/**
* The dimensions to query.
*
* @var array<array<string, string>> $dimensions
*/
private $dimensions = [];
/**
* The dimensions filters.
*
* @var array<string, array<string>> $dimension_filters
*/
private $dimension_filters = [];
/**
* The metrics.
*
* @var array<array<string, string>> $metrics
*/
private $metrics = [];
/**
* The order by.
*
* @var array<array<string, array<string, string>>> $order_by
*/
private $order_by = [];
/**
* Sets the dimensions.
*
* @link https://developers.google.com/analytics/devguides/reporting/data/v1/rest/v1beta/Dimension
*
* @param array<string> $dimensions The dimensions.
*
* @return void
*/
public function set_dimensions( array $dimensions ): void {
foreach ( $dimensions as $dimension ) {
$this->dimensions[] = [ 'name' => $dimension ];
}
}
/**
* Getter for the dimensions.
*
* @return array<array<string, string>>
*/
public function get_dimensions(): array {
return $this->dimensions;
}
/**
* Sets the dimension filters.
*
* @param array<string, array<string>> $dimension_filters The dimension filters.
*
* @return void
*/
public function set_dimension_filters( array $dimension_filters ): void {
$this->dimension_filters = $dimension_filters;
}
/**
* Getter for the dimension filters.
*
* @return array<string, array<string>>
*/
public function get_dimension_filters(): array {
return $this->dimension_filters;
}
/**
* Sets the metrics.
*
* @link https://developers.google.com/analytics/devguides/reporting/data/v1/rest/v1beta/Metric
*
* @param array<string> $metrics The metrics.
*
* @return void
*/
public function set_metrics( array $metrics ): void {
foreach ( $metrics as $metric ) {
$this->metrics[] = [ 'name' => $metric ];
}
}
/**
* Getter for the metrics.
*
* @return array<array<string, string>>
*/
public function get_metrics(): array {
return $this->metrics;
}
/**
* Sets the order by.
*
* @link https://developers.google.com/analytics/devguides/reporting/data/v1/rest/v1beta/OrderBy
*
* @param string $key The key to order by.
* @param string $name The name to order by.
*
* @return void
*/
public function set_order_by( string $key, string $name ): void {
$order_by = [
[
$key => [
$key . 'Name' => $name,
],
],
];
$this->order_by = $order_by;
}
/**
* Getter for the order by.
*
* @return array<array<string, array<string, string>>>
*/
public function get_order_by(): array {
return $this->order_by;
}
}
@@ -0,0 +1,281 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
namespace Yoast\WP\SEO\Dashboard\Infrastructure\Analytics_4;
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\Row;
use Google\Site_Kit_Dependencies\Google\Service\AnalyticsData\RunReportResponse;
use WP_REST_Response;
use Yoast\WP\SEO\Dashboard\Domain\Analytics_4\Failed_Request_Exception;
use Yoast\WP\SEO\Dashboard\Domain\Analytics_4\Invalid_Request_Exception;
use Yoast\WP\SEO\Dashboard\Domain\Analytics_4\Unexpected_Response_Exception;
use Yoast\WP\SEO\Dashboard\Domain\Data_Provider\Data_Container;
use Yoast\WP\SEO\Dashboard\Domain\Traffic\Comparison_Traffic_Data;
use Yoast\WP\SEO\Dashboard\Domain\Traffic\Daily_Traffic_Data;
use Yoast\WP\SEO\Dashboard\Domain\Traffic\Traffic_Data;
/**
* The site API adapter to make calls to the Analytics 4 API, via the Site_Kit plugin.
*/
class Site_Kit_Analytics_4_Adapter {
/**
* Holds the api call class.
*
* @var Site_Kit_Analytics_4_Api_Call $site_kit_analytics_4_api_call
*/
private $site_kit_search_console_api_call;
/**
* The register method that sets the instance in the adapter.
*
* @param Site_Kit_Analytics_4_Api_Call $site_kit_analytics_4_api_call The api call class.
*
* @return void
*/
public function __construct( Site_Kit_Analytics_4_Api_Call $site_kit_analytics_4_api_call ) {
$this->site_kit_search_console_api_call = $site_kit_analytics_4_api_call;
}
/**
* The wrapper method to do a comparison Site Kit API request for Analytics.
*
* @param Analytics_4_Parameters $parameters The parameters.
*
* @return Data_Container The Site Kit API response.
*
* @throws Failed_Request_Exception When the request responds with an error from Site Kit.
* @throws Unexpected_Response_Exception When the request responds with an unexpected format.
* @throws Invalid_Request_Exception When the request is invalid due to unexpected parameters.
*/
public function get_comparison_data( Analytics_4_Parameters $parameters ): Data_Container {
$api_parameters = $this->build_parameters( $parameters );
$response = $this->site_kit_search_console_api_call->do_request( $api_parameters );
$this->validate_response( $response );
return $this->parse_comparison_response( $response->get_data() );
}
/**
* The wrapper method to do a daily Site Kit API request for Analytics.
*
* @param Analytics_4_Parameters $parameters The parameters.
*
* @return Data_Container The Site Kit API response.
*
* @throws Failed_Request_Exception When the request responds with an error from Site Kit.
* @throws Unexpected_Response_Exception When the request responds with an unexpected format.
* @throws Invalid_Request_Exception When the request is invalid due to unexpected parameters.
*/
public function get_daily_data( Analytics_4_Parameters $parameters ): Data_Container {
$api_parameters = $this->build_parameters( $parameters );
$response = $this->site_kit_search_console_api_call->do_request( $api_parameters );
$this->validate_response( $response );
return $this->parse_daily_response( $response->get_data() );
}
/**
* Builds the parameters to be used in the Site Kit API request.
*
* @param Analytics_4_Parameters $parameters The parameters.
*
* @return array<string, array<string, string>> The Site Kit API parameters.
*/
private function build_parameters( Analytics_4_Parameters $parameters ): array {
$api_parameters = [
'slug' => 'analytics-4',
'datapoint' => 'report',
'startDate' => $parameters->get_start_date(),
'endDate' => $parameters->get_end_date(),
];
if ( ! empty( $parameters->get_dimension_filters() ) ) {
$api_parameters['dimensionFilters'] = $parameters->get_dimension_filters();
}
if ( ! empty( $parameters->get_dimensions() ) ) {
$api_parameters['dimensions'] = $parameters->get_dimensions();
}
if ( ! empty( $parameters->get_metrics() ) ) {
$api_parameters['metrics'] = $parameters->get_metrics();
}
if ( ! empty( $parameters->get_order_by() ) ) {
$api_parameters['orderby'] = $parameters->get_order_by();
}
if ( ! empty( $parameters->get_compare_start_date() && ! empty( $parameters->get_compare_end_date() ) ) ) {
$api_parameters['compareStartDate'] = $parameters->get_compare_start_date();
$api_parameters['compareEndDate'] = $parameters->get_compare_end_date();
}
return $api_parameters;
}
/**
* Parses a response for a Site Kit API request that requests daily data for Analytics 4.
*
* @param RunReportResponse $response The response to parse.
*
* @return Data_Container The parsed response.
*
* @throws Invalid_Request_Exception When the request is invalid due to unexpected parameters.
*/
private function parse_daily_response( RunReportResponse $response ): Data_Container {
if ( ! $this->is_daily_request( $response ) ) {
throw new Invalid_Request_Exception( 'Unexpected parameters for the request' );
}
$data_container = new Data_Container();
foreach ( $response->getRows() as $daily_traffic ) {
$traffic_data = new Traffic_Data();
foreach ( $response->getMetricHeaders() as $key => $metric ) {
// As per https://developers.google.com/analytics/devguides/reporting/data/v1/basics#read_the_response,
// the order of the columns is consistent in the request, header, and rows.
// So we can use the key of the header to get the correct metric value from the row.
$metric_value = $daily_traffic->getMetricValues()[ $key ]->getValue();
if ( $metric->getName() === 'sessions' ) {
$traffic_data->set_sessions( (int) $metric_value );
}
elseif ( $metric->getName() === 'totalUsers' ) {
$traffic_data->set_total_users( (int) $metric_value );
}
}
// Since we're here, we know that the first dimension is date, so we know that dimensionValues[0]->value is a date.
$data_container->add_data( new Daily_Traffic_Data( $daily_traffic->getDimensionValues()[0]->getValue(), $traffic_data ) );
}
return $data_container;
}
/**
* Parses a response for a Site Kit API request for Analytics 4 that compares data ranges.
*
* @param RunReportResponse $response The response to parse.
*
* @return Data_Container The parsed response.
*
* @throws Invalid_Request_Exception When the request is invalid due to unexpected parameters.
*/
private function parse_comparison_response( RunReportResponse $response ): Data_Container {
if ( ! $this->is_comparison_request( $response ) ) {
throw new Invalid_Request_Exception( 'Unexpected parameters for the request' );
}
$data_container = new Data_Container();
$comparison_traffic_data = new Comparison_Traffic_Data();
// First row is the current date range's data, second row is the previous date range's data.
foreach ( $response->getRows() as $date_range_row ) {
$traffic_data = new Traffic_Data();
// Loop through all the metrics of the date range.
foreach ( $response->getMetricHeaders() as $key => $metric ) {
// As per https://developers.google.com/analytics/devguides/reporting/data/v1/basics#read_the_response,
// the order of the columns is consistent in the request, header, and rows.
// So we can use the key of the header to get the correct metric value from the row.
$metric_value = $date_range_row->getMetricValues()[ $key ]->getValue();
if ( $metric->getName() === 'sessions' ) {
$traffic_data->set_sessions( (int) $metric_value );
}
elseif ( $metric->getName() === 'totalUsers' ) {
$traffic_data->set_total_users( (int) $metric_value );
}
}
$period = $this->get_period( $date_range_row );
if ( $period === Comparison_Traffic_Data::CURRENT_PERIOD_KEY ) {
$comparison_traffic_data->set_current_traffic_data( $traffic_data );
}
elseif ( $period === Comparison_Traffic_Data::PREVIOUS_PERIOD_KEY ) {
$comparison_traffic_data->set_previous_traffic_data( $traffic_data );
}
}
$data_container->add_data( $comparison_traffic_data );
return $data_container;
}
/**
* Parses the response row and returns whether it's about the current period or the previous period.
*
* @see https://developers.google.com/analytics/devguides/reporting/data/v1/rest/v1beta/DateRange
*
* @param Row $date_range_row The response row.
*
* @return string The key associated with the current or the previous period.
*
* @throws Invalid_Request_Exception When the request is invalid due to unexpected parameters.
*/
private function get_period( Row $date_range_row ): string {
foreach ( $date_range_row->getDimensionValues() as $dimension_value ) {
if ( $dimension_value->getValue() === 'date_range_0' ) {
return Comparison_Traffic_Data::CURRENT_PERIOD_KEY;
}
elseif ( $dimension_value->getValue() === 'date_range_1' ) {
return Comparison_Traffic_Data::PREVIOUS_PERIOD_KEY;
}
}
throw new Invalid_Request_Exception( 'Unexpected date range names' );
}
/**
* Checks the response of the request to detect if it's a comparison request.
*
* @param RunReportResponse $response The response.
*
* @return bool Whether it's a comparison request.
*/
private function is_comparison_request( RunReportResponse $response ): bool {
return \count( $response->getDimensionHeaders() ) === 1 && $response->getDimensionHeaders()[0]->getName() === 'dateRange';
}
/**
* Checks the response of the request to detect if it's a daily request.
*
* @param RunReportResponse $response The response.
*
* @return bool Whether it's a daily request.
*/
private function is_daily_request( RunReportResponse $response ): bool {
return \count( $response->getDimensionHeaders() ) === 1 && $response->getDimensionHeaders()[0]->getName() === 'date';
}
/**
* Validates the response coming from Google Analytics.
*
* @param WP_REST_Response $response The response we want to validate.
*
* @return void
*
* @throws Failed_Request_Exception When the request responds with an error from Site Kit.
* @throws Unexpected_Response_Exception When the request responds with an unexpected format.
*/
private function validate_response( WP_REST_Response $response ): void {
if ( $response->is_error() ) {
$error_data = $response->as_error()->get_error_data();
$error_status_code = ( $error_data['status'] ?? 500 );
throw new Failed_Request_Exception( \wp_kses_post( $response->as_error()->get_error_message() ), (int) $error_status_code );
}
if ( ! \is_a( $response->get_data(), RunReportResponse::class ) ) {
throw new Unexpected_Response_Exception();
}
}
}
@@ -0,0 +1,34 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
namespace Yoast\WP\SEO\Dashboard\Infrastructure\Analytics_4;
use Google\Site_Kit\Core\REST_API\REST_Routes;
use WP_REST_Request;
use WP_REST_Response;
/**
* Class that hold the code to do the REST call to the Site Kit api.
*
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
*/
class Site_Kit_Analytics_4_Api_Call {
/**
* The Analytics 4 API route path.
*/
private const ANALYTICS_DATA_REPORT_ROUTE = '/modules/analytics-4/data/report';
/**
* Runs the internal REST api call.
*
* @param array<string, array<string, string>> $api_parameters The api parameters.
*
* @return WP_REST_Response
*/
public function do_request( array $api_parameters ): WP_REST_Response {
$request = new WP_REST_Request( 'GET', '/' . REST_Routes::REST_ROOT . self::ANALYTICS_DATA_REPORT_ROUTE );
$request->set_query_params( $api_parameters );
return \rest_do_request( $request );
}
}
@@ -0,0 +1,82 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\Dashboard\Infrastructure\Browser_Cache;
use Yoast\WP\SEO\Conditionals\Google_Site_Kit_Feature_Conditional;
/**
* Responsible for the browser cache configuration.
*/
class Browser_Cache_Configuration {
/**
* The Site Kit conditional.
*
* @var Google_Site_Kit_Feature_Conditional
*/
private $google_site_kit_feature_conditional;
/**
* The constructor.
*
* @param Google_Site_Kit_Feature_Conditional $google_site_kit_feature_conditional The Site Kit conditional.
*/
public function __construct( Google_Site_Kit_Feature_Conditional $google_site_kit_feature_conditional ) {
$this->google_site_kit_feature_conditional = $google_site_kit_feature_conditional;
}
/**
* Gets the Time To Live for each widget's cache.
*
* @return array<string, array<string, int>> The cache TTL for each widget.
*/
private function get_widgets_cache_ttl() {
return [
'topPages' => [
'ttl' => ( 1 * \MINUTE_IN_SECONDS ),
],
'topQueries' => [
'ttl' => ( 1 * \HOUR_IN_SECONDS ),
],
'searchRankingCompare' => [
'ttl' => ( 1 * \HOUR_IN_SECONDS ),
],
'organicSessions' => [
'ttl' => ( 1 * \HOUR_IN_SECONDS ),
],
];
}
/**
* Gets the prefix for the client side cache key.
*
* Cache key is scoped to user session and blog_id to isolate the
* cache between users and sites (in multisite).
*
* @return string
*/
private function get_storage_prefix() {
$current_user = \wp_get_current_user();
$auth_cookie = \wp_parse_auth_cookie();
$blog_id = \get_current_blog_id();
$session_token = isset( $auth_cookie['token'] ) ? $auth_cookie['token'] : '';
return \wp_hash( $current_user->user_login . '|' . $session_token . '|' . $blog_id );
}
/**
* Returns the browser cache configuration.
*
* @return array<string, string|array<string, array<string, int>>>
*/
public function get_configuration(): array {
if ( ! $this->google_site_kit_feature_conditional->is_met() ) {
return [];
}
return [
'storagePrefix' => $this->get_storage_prefix(),
'yoastVersion' => \WPSEO_VERSION,
'widgetsCacheTtl' => $this->get_widgets_cache_ttl(),
];
}
}
@@ -0,0 +1,28 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Infrastructure\Configuration;
/**
* Interface for the Permanently Dismissed Site Kit configuration Repository.
*
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
*/
interface Permanently_Dismissed_Site_Kit_Configuration_Repository_Interface {
/**
* Sets the Site Kit configuration dismissal status.
*
* @param bool $is_dismissed The dismissal status.
*
* @return bool False when the update failed, true when the update succeeded.
*/
public function set_site_kit_configuration_dismissal( bool $is_dismissed ): bool;
/**
* Checks if the Site Kit configuration is dismissed permanently.
* *
*
* @return bool True when the configuration is dismissed, false when it is not.
*/
public function is_site_kit_configuration_dismissed(): bool;
}
@@ -0,0 +1,50 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Infrastructure\Configuration;
use Yoast\WP\SEO\Helpers\Options_Helper;
/**
* Stores and retrieves whether the Site Kit configuration is permanently dismissed.
*
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
*/
class Permanently_Dismissed_Site_Kit_Configuration_Repository implements Permanently_Dismissed_Site_Kit_Configuration_Repository_Interface {
/**
* Holds the Options_Helper instance.
*
* @var Options_Helper
*/
private $options_helper;
/**
* Constructs the class.
*
* @param Options_Helper $options_helper The options helper.
*/
public function __construct( Options_Helper $options_helper ) {
$this->options_helper = $options_helper;
}
/**
* Sets the Site Kit dismissal status.
*
* @param bool $is_dismissed The dismissal status.
*
* @return bool False when the update failed, true when the update succeeded.
*/
public function set_site_kit_configuration_dismissal( bool $is_dismissed ): bool {
return $this->options_helper->set( 'site_kit_configuration_permanently_dismissed', $is_dismissed );
}
/**
* Checks if the Site Kit configuration is dismissed permanently.
* *
*
* @return bool True when the configuration is dismissed, false when it is not.
*/
public function is_site_kit_configuration_dismissed(): bool {
return $this->options_helper->get( 'site_kit_configuration_permanently_dismissed', false );
}
}
@@ -0,0 +1,28 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Infrastructure\Configuration;
/**
* Interface for theSite Kit Consent Repository.
*
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
*/
interface Site_Kit_Consent_Repository_Interface {
/**
* Sets the Site Kit consent.
*
* @param bool $consent The consent value.
*
* @return bool False when the update failed, true when the update succeeded.
*/
public function set_site_kit_consent( bool $consent ): bool;
/**
* Returns the Site Kit consent status.
* *
*
* @return bool True when the consent has been granted, false when it is not.
*/
public function is_consent_granted(): bool;
}
@@ -0,0 +1,50 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Infrastructure\Configuration;
use Yoast\WP\SEO\Helpers\Options_Helper;
/**
* Stores and retrieves the Site Kit consent status.
*
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
*/
class Site_Kit_Consent_Repository implements Site_Kit_Consent_Repository_Interface {
/**
* Holds the Options_Helper instance.
*
* @var Options_Helper
*/
private $options_helper;
/**
* Constructs the class.
*
* @param Options_Helper $options_helper The options helper.
*/
public function __construct( Options_Helper $options_helper ) {
$this->options_helper = $options_helper;
}
/**
* Sets the Site Kit consent value.
*
* @param bool $consent The consent status.
*
* @return bool False when the update failed, true when the update succeeded.
*/
public function set_site_kit_consent( bool $consent ): bool {
return $this->options_helper->set( 'site_kit_connected', $consent );
}
/**
* Checks if consent has ben given for Site Kit.
* *
*
* @return bool True when consent has been given, false when it is not.
*/
public function is_consent_granted(): bool {
return $this->options_helper->get( 'site_kit_connected', false );
}
}
@@ -0,0 +1,60 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
namespace Yoast\WP\SEO\Dashboard\Infrastructure\Connection;
use Google\Site_Kit\Core\REST_API\REST_Routes;
use WP_REST_Request;
/**
* Class that hold the code to do the REST call to the Site Kit api.
*
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
*/
class Site_Kit_Is_Connected_Call {
/**
* Runs the internal REST api call.
*
* @return bool
*/
public function is_setup_completed(): bool {
if ( ! \class_exists( REST_Routes::class ) ) {
return false;
}
$request = new WP_REST_Request( 'GET', '/' . REST_Routes::REST_ROOT . '/core/site/data/connection' );
$response = \rest_do_request( $request );
if ( $response->is_error() ) {
return false;
}
return $response->get_data()['setupCompleted'];
}
/**
* Runs the internal REST api call.
*
* @return bool
*/
public function is_ga_connected(): bool {
if ( ! \class_exists( REST_Routes::class ) ) {
return false;
}
$request = new WP_REST_Request( 'GET', '/' . REST_Routes::REST_ROOT . '/core/modules/data/list' );
$response = \rest_do_request( $request );
if ( $response->is_error() ) {
return false;
}
$connected = false;
foreach ( $response->get_data() as $module ) {
if ( $module['slug'] === 'analytics-4' ) {
$connected = $module['connected'];
}
}
return $connected;
}
}
@@ -0,0 +1,51 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\Dashboard\Infrastructure\Content_Types;
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;
/**
* Class that collects post types and relevant information.
*/
class Content_Types_Collector {
/**
* The post type helper.
*
* @var Post_Type_Helper
*/
private $post_type_helper;
/**
* The constructor.
*
* @param Post_Type_Helper $post_type_helper The post type helper.
*/
public function __construct( Post_Type_Helper $post_type_helper ) {
$this->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_type_objects();
foreach ( $post_types as $post_type_object ) {
if ( $post_type_object->show_ui === false ) {
continue;
}
$content_type = new Content_Type( $post_type_object->name, $post_type_object->label );
$content_types_list->add( $content_type );
}
return $content_types_list;
}
}
@@ -0,0 +1,52 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Infrastructure\Endpoints;
use Exception;
use Yoast\WP\SEO\Dashboard\Domain\Endpoint\Endpoint_Interface;
use Yoast\WP\SEO\Dashboard\User_Interface\Scores\Abstract_Scores_Route;
use Yoast\WP\SEO\Dashboard\User_Interface\Scores\Readability_Scores_Route;
/**
* Represents the readability scores endpoint.
*/
class Readability_Scores_Endpoint implements Endpoint_Interface {
/**
* Gets the name.
*
* @return string
*/
public function get_name(): string {
return 'readabilityScores';
}
/**
* Gets the namespace.
*
* @return string
*/
public function get_namespace(): string {
return Abstract_Scores_Route::ROUTE_NAMESPACE;
}
/**
* Gets the route.
*
* @return string
*
* @throws Exception If the route prefix is not overwritten this throws.
*/
public function get_route(): string {
return Readability_Scores_Route::get_route_prefix();
}
/**
* Gets the URL.
*
* @return string
*/
public function get_url(): string {
return \rest_url( $this->get_namespace() . $this->get_route() );
}
}
@@ -0,0 +1,52 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Infrastructure\Endpoints;
use Exception;
use Yoast\WP\SEO\Dashboard\Domain\Endpoint\Endpoint_Interface;
use Yoast\WP\SEO\Dashboard\User_Interface\Scores\Abstract_Scores_Route;
use Yoast\WP\SEO\Dashboard\User_Interface\Scores\SEO_Scores_Route;
/**
* Represents the SEO scores endpoint.
*/
class SEO_Scores_Endpoint implements Endpoint_Interface {
/**
* Gets the name.
*
* @return string
*/
public function get_name(): string {
return 'seoScores';
}
/**
* Gets the namespace.
*
* @return string
*/
public function get_namespace(): string {
return Abstract_Scores_Route::ROUTE_NAMESPACE;
}
/**
* Gets the route.
*
* @return string
*
* @throws Exception If the route prefix is not overwritten this throws.
*/
public function get_route(): string {
return SEO_Scores_Route::get_route_prefix();
}
/**
* Gets the URL.
*
* @return string
*/
public function get_url(): string {
return \rest_url( $this->get_namespace() . $this->get_route() );
}
}
@@ -0,0 +1,50 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Infrastructure\Endpoints;
use Exception;
use Yoast\WP\SEO\Dashboard\Domain\Endpoint\Endpoint_Interface;
use Yoast\WP\SEO\Dashboard\User_Interface\Tracking\Setup_Steps_Tracking_Route;
/**
* Represents the setup steps tracking endpoint.
*/
class Setup_Steps_Tracking_Endpoint implements Endpoint_Interface {
/**
* Gets the name.
*
* @return string
*/
public function get_name(): string {
return 'setupStepsTracking';
}
/**
* Gets the namespace.
*
* @return string
*/
public function get_namespace(): string {
return Setup_Steps_Tracking_Route::ROUTE_NAMESPACE;
}
/**
* Gets the route.
*
* @throws Exception If the route prefix is not overwritten this throws.
* @return string
*/
public function get_route(): string {
return Setup_Steps_Tracking_Route::ROUTE_PREFIX;
}
/**
* Gets the URL.
*
* @return string
*/
public function get_url(): string {
return \rest_url( $this->get_namespace() . $this->get_route() );
}
}
@@ -0,0 +1,51 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Infrastructure\Endpoints;
use Exception;
use Yoast\WP\SEO\Dashboard\Domain\Endpoint\Endpoint_Interface;
use Yoast\WP\SEO\Dashboard\User_Interface\Configuration\Site_Kit_Configuration_Dismissal_Route;
/**
* Represents the readability scores endpoint.
*/
class Site_Kit_Configuration_Dismissal_Endpoint implements Endpoint_Interface {
/**
* Gets the name.
*
* @return string
*/
public function get_name(): string {
return 'siteKitConfigurationDismissal';
}
/**
* Gets the namespace.
*
* @return string
*/
public function get_namespace(): string {
return Site_Kit_Configuration_Dismissal_Route::ROUTE_NAMESPACE;
}
/**
* Gets the route.
*
* @return string
*
* @throws Exception If the route prefix is not overwritten this throws.
*/
public function get_route(): string {
return Site_Kit_Configuration_Dismissal_Route::ROUTE_PREFIX;
}
/**
* Gets the URL.
*
* @return string
*/
public function get_url(): string {
return \rest_url( $this->get_namespace() . $this->get_route() );
}
}
@@ -0,0 +1,51 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Infrastructure\Endpoints;
use Exception;
use Yoast\WP\SEO\Dashboard\Domain\Endpoint\Endpoint_Interface;
use Yoast\WP\SEO\Dashboard\User_Interface\Configuration\Site_Kit_Consent_Management_Route;
/**
* Represents the Site Kit consent management endpoint.
*/
class Site_Kit_Consent_Management_Endpoint implements Endpoint_Interface {
/**
* Gets the name.
*
* @return string
*/
public function get_name(): string {
return 'siteKitConsentManagement';
}
/**
* Gets the namespace.
*
* @return string
*/
public function get_namespace(): string {
return Site_Kit_Consent_Management_Route::ROUTE_NAMESPACE;
}
/**
* Gets the route.
*
* @return string
*
* @throws Exception If the route prefix is not overwritten this throws.
*/
public function get_route(): string {
return Site_Kit_Consent_Management_Route::ROUTE_PREFIX;
}
/**
* Gets the URL.
*
* @return string
*/
public function get_url(): string {
return \rest_url( $this->get_namespace() . $this->get_route() );
}
}
@@ -0,0 +1,49 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
namespace Yoast\WP\SEO\Dashboard\Infrastructure\Endpoints;
use Yoast\WP\SEO\Dashboard\Domain\Endpoint\Endpoint_Interface;
use Yoast\WP\SEO\Dashboard\User_Interface\Time_Based_SEO_Metrics\Time_Based_SEO_Metrics_Route;
/**
* Represents the time based SEO metrics endpoint.
*/
class Time_Based_SEO_Metrics_Endpoint implements Endpoint_Interface {
/**
* Gets the name.
*
* @return string
*/
public function get_name(): string {
return 'timeBasedSeoMetrics';
}
/**
* Gets the namespace.
*
* @return string
*/
public function get_namespace(): string {
return Time_Based_SEO_Metrics_Route::ROUTE_NAMESPACE;
}
/**
* Gets the route.
*
* @return string
*/
public function get_route(): string {
return Time_Based_SEO_Metrics_Route::ROUTE_NAME;
}
/**
* Gets the URL.
*
* @return string
*/
public function get_url(): string {
return \rest_url( $this->get_namespace() . $this->get_route() );
}
}
@@ -0,0 +1,111 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
namespace Yoast\WP\SEO\Dashboard\Infrastructure\Indexables;
use Yoast\WP\SEO\Dashboard\Application\Score_Groups\SEO_Score_Groups\SEO_Score_Groups_Repository;
use Yoast\WP\SEO\Dashboard\Domain\Data_Provider\Data_Container;
use Yoast\WP\SEO\Dashboard\Domain\Score_Groups\SEO_Score_Groups\No_SEO_Score_Group;
use Yoast\WP\SEO\Dashboard\Domain\Search_Rankings\Top_Page_Data;
use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Repositories\Indexable_Repository;
/**
* The indexable collector that gets SEO scores from the indexables of top pages.
*/
class Top_Page_Indexable_Collector {
/**
* The indexable repository.
*
* @var Indexable_Repository
*/
private $indexable_repository;
/**
* The SEO score groups repository.
*
* @var SEO_Score_Groups_Repository
*/
private $seo_score_groups_repository;
/**
* The constructor.
*
* @param Indexable_Repository $indexable_repository The indexable repository.
* @param SEO_Score_Groups_Repository $seo_score_groups_repository The SEO score groups repository.
*/
public function __construct(
Indexable_Repository $indexable_repository,
SEO_Score_Groups_Repository $seo_score_groups_repository
) {
$this->indexable_repository = $indexable_repository;
$this->seo_score_groups_repository = $seo_score_groups_repository;
}
/**
* Gets full data for top pages.
*
* @param Data_Container $top_pages The top pages.
*
* @return Data_Container Data about SEO scores of top pages.
*/
public function get_data( Data_Container $top_pages ): Data_Container {
$top_page_data_container = new Data_Container();
foreach ( $top_pages->get_data() as $top_page ) {
$url = $top_page->get_subject();
$indexable = $this->get_top_page_indexable( $url );
if ( $indexable instanceof Indexable ) {
$seo_score_group = $this->seo_score_groups_repository->get_seo_score_group( $indexable->primary_focus_keyword_score );
$edit_link = $this->get_top_page_edit_link( $indexable );
$top_page_data_container->add_data( new Top_Page_Data( $top_page, $seo_score_group, $edit_link ) );
continue;
}
$seo_score_group = new No_SEO_Score_Group();
$top_page_data_container->add_data( new Top_Page_Data( $top_page, $seo_score_group ) );
}
return $top_page_data_container;
}
/**
* Gets indexable for a top page URL.
*
* @param string $url The URL of the top page.
*
* @return bool|Indexable The indexable of the top page URL or false if there is none.
*/
protected function get_top_page_indexable( string $url ) {
// First check if the URL is the static homepage.
if ( \trailingslashit( $url ) === \trailingslashit( \get_home_url() ) && \get_option( 'show_on_front' ) === 'page' ) {
return $this->indexable_repository->find_by_id_and_type( \get_option( 'page_on_front' ), 'post', false );
}
return $this->indexable_repository->find_by_permalink( $url );
}
/**
* Gets edit links from a top page's indexable.
*
* @param Indexable $indexable The top page's indexable.
*
* @return string|null The edit link for the top page.
*/
protected function get_top_page_edit_link( Indexable $indexable ): ?string {
if ( $indexable->object_type === 'post' && \current_user_can( 'edit_post', $indexable->object_id ) ) {
return \get_edit_post_link( $indexable->object_id, '&' );
}
if ( $indexable->object_type === 'term' && \current_user_can( 'edit_term', $indexable->object_id ) ) {
return \get_edit_term_link( $indexable->object_id );
}
return null;
}
}
@@ -0,0 +1,339 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Infrastructure\Integrations;
use Google\Site_Kit\Core\REST_API\REST_Routes;
use Yoast\WP\SEO\Conditionals\Google_Site_Kit_Feature_Conditional;
use Yoast\WP\SEO\Conditionals\Third_Party\Site_Kit_Conditional;
use Yoast\WP\SEO\Dashboard\Infrastructure\Configuration\Permanently_Dismissed_Site_Kit_Configuration_Repository_Interface as Configuration_Repository;
use Yoast\WP\SEO\Dashboard\Infrastructure\Configuration\Site_Kit_Consent_Repository_Interface;
use Yoast\WP\SEO\Dashboard\Infrastructure\Connection\Site_Kit_Is_Connected_Call;
use Yoast\WP\SEO\Dashboard\User_Interface\Setup\Setup_Url_Interceptor;
/**
* Describes if the Site kit integration is enabled and configured.
*/
class Site_Kit {
private const SITE_KIT_FILE = 'google-site-kit/google-site-kit.php';
/**
* The Site Kit feature conditional.
*
* @var Google_Site_Kit_Feature_Conditional
*/
protected $site_kit_feature_conditional;
/**
* The Site Kit conditional.
*
* @var Site_Kit_Conditional
*/
private $site_kit_conditional;
/**
* The Site Kit consent repository.
*
* @var Site_Kit_Consent_Repository_Interface
*/
private $site_kit_consent_repository;
/**
* The Site Kit consent repository.
*
* @var Configuration_Repository
*/
private $permanently_dismissed_site_kit_configuration_repository;
/**
* The call wrapper.
*
* @var Site_Kit_Is_Connected_Call $site_kit_is_connected_call
*/
private $site_kit_is_connected_call;
/**
* The search console module data.
*
* @var array<string, bool> $search_console_module
*/
private $search_console_module = [
'can_view' => null,
];
/**
* The analytics module data.
*
* @var array<string, bool> $ga_module
*/
private $ga_module = [
'can_view' => null,
'connected' => null,
];
/**
* The constructor.
*
* @param Site_Kit_Consent_Repository_Interface $site_kit_consent_repository The Site Kit consent repository.
* @param Configuration_Repository $configuration_repository The Site Kit permanently dismissed
* configuration repository.
* @param Site_Kit_Is_Connected_Call $site_kit_is_connected_call The api call to check if the site is
* connected.
* @param Google_Site_Kit_Feature_Conditional $site_kit_feature_conditional The Site Kit feature conditional.
* @param Site_Kit_Conditional $site_kit_conditional The Site Kit conditional.
*/
public function __construct(
Site_Kit_Consent_Repository_Interface $site_kit_consent_repository,
Configuration_Repository $configuration_repository,
Site_Kit_Is_Connected_Call $site_kit_is_connected_call,
Google_Site_Kit_Feature_Conditional $site_kit_feature_conditional,
Site_Kit_Conditional $site_kit_conditional
) {
$this->site_kit_consent_repository = $site_kit_consent_repository;
$this->permanently_dismissed_site_kit_configuration_repository = $configuration_repository;
$this->site_kit_is_connected_call = $site_kit_is_connected_call;
$this->site_kit_feature_conditional = $site_kit_feature_conditional;
$this->site_kit_conditional = $site_kit_conditional;
}
/**
* If the Site Kit plugin is active.
*
* @return bool If the integration is activated.
*/
public function is_enabled(): bool {
return $this->site_kit_conditional->is_met();
}
/**
* If the Google site kit setup has been completed.
*
* @return bool If the Google site kit setup has been completed.
*/
private function is_setup_completed(): bool {
return $this->site_kit_is_connected_call->is_setup_completed();
}
/**
* If consent has been granted.
*
* @return bool If consent has been granted.
*/
private function is_connected(): bool {
return $this->site_kit_consent_repository->is_consent_granted();
}
/**
* If Google Analytics is connected.
*
* @return bool If Google Analytics is connected.
*/
public function is_ga_connected(): bool {
if ( $this->ga_module['connected'] !== null ) {
return $this->ga_module['connected'];
}
return $this->site_kit_is_connected_call->is_ga_connected();
}
/**
* If the Site Kit plugin is installed. This is needed since we cannot check with `is_plugin_active` in rest
* requests. `Plugin.php` is only loaded on admin pages.
*
* @return bool If the Site Kit plugin is installed.
*/
private function is_site_kit_installed(): bool {
return \class_exists( 'Google\Site_Kit\Plugin' );
}
/**
* If the entire onboarding has been completed.
*
* @return bool If the entire onboarding has been completed.
*/
public function is_onboarded(): bool {
// @TODO: Consider replacing the `is_setup_completed()` check with a `can_read_data( $module )` check (and possibly rename the method to something more genric eg. is_ready() ).
return ( $this->is_site_kit_installed() && $this->is_setup_completed() && $this->is_connected() );
}
/**
* Checks if current user can view dashboard data for a module
*
* @param array<array|null> $module The module.
*
* @return bool If the user can read the data.
*/
private function can_read_data( array $module ): bool {
return ( ! \is_null( $module['can_view'] ) ? $module['can_view'] : false );
}
/**
* Return this object represented by a key value array.
*
* @return array<string, bool> Returns the name and if the feature is enabled.
*/
public function to_array(): array {
if ( ! $this->site_kit_feature_conditional->is_met() ) {
return [];
}
if ( $this->is_enabled() ) {
$this->parse_site_kit_data();
}
return [
'installUrl' => \self_admin_url( 'update.php?page=' . Setup_Url_Interceptor::PAGE . '&redirect_setup_url=' ) . \rawurlencode( $this->get_install_url() ),
'activateUrl' => \self_admin_url( 'update.php?page=' . Setup_Url_Interceptor::PAGE . '&redirect_setup_url=' ) . \rawurlencode( $this->get_activate_url() ),
'setupUrl' => \self_admin_url( 'update.php?page=' . Setup_Url_Interceptor::PAGE . '&redirect_setup_url=' ) . \rawurlencode( $this->get_setup_url() ),
'updateUrl' => \self_admin_url( 'update.php?page=' . Setup_Url_Interceptor::PAGE . '&redirect_setup_url=' ) . \rawurlencode( $this->get_update_url() ),
'dashboardUrl' => \self_admin_url( 'admin.php?page=googlesitekit-dashboard' ),
'isAnalyticsConnected' => $this->is_ga_connected(),
'isFeatureEnabled' => true,
'isSetupWidgetDismissed' => $this->permanently_dismissed_site_kit_configuration_repository->is_site_kit_configuration_dismissed(),
'capabilities' => [
'installPlugins' => \current_user_can( 'install_plugins' ),
'viewSearchConsoleData' => $this->can_read_data( $this->search_console_module ),
'viewAnalyticsData' => $this->can_read_data( $this->ga_module ),
],
'connectionStepsStatuses' => [
'isInstalled' => \file_exists( \WP_PLUGIN_DIR . '/' . self::SITE_KIT_FILE ),
'isActive' => $this->is_enabled(),
'isSetupCompleted' => $this->can_read_data( $this->search_console_module ) || $this->can_read_data( $this->ga_module ),
'isConsentGranted' => $this->is_connected(),
],
'isVersionSupported' => \defined( 'GOOGLESITEKIT_VERSION' ) ? \version_compare( \GOOGLESITEKIT_VERSION, '1.148.0', '>=' ) : false,
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information.
'isRedirectedFromSiteKit' => isset( $_GET['redirected_from_site_kit'] ),
];
}
/**
* Return this object represented by a key value array. This is not used yet.
*
* @codeCoverageIgnore
*
* @return array<string, bool> Returns the name and if the feature is enabled.
*/
public function to_legacy_array(): array {
return $this->to_array();
}
/**
* Parses the Site Kit configuration data.
*
* @return void
*/
public function parse_site_kit_data(): void {
$paths = $this->get_preload_paths();
$preloaded = $this->get_preloaded_data( $paths );
if ( empty( $preloaded ) ) {
return;
}
$modules_data = ! empty( $preloaded[ $paths['modules'] ]['body'] ) ? $preloaded[ $paths['modules'] ]['body'] : [];
$modules_permissions = ! empty( $preloaded[ $paths['permissions'] ]['body'] ) ? $preloaded[ $paths['permissions'] ]['body'] : [];
$can_view_dashboard = ( $modules_permissions['googlesitekit_view_authenticated_dashboard'] ?? false );
foreach ( $modules_data as $module ) {
$slug = $module['slug'];
// We have to also check if the module is recoverable, because if we rely on the module being shared, we have to make also sure the module owner is still connected.
$is_recoverable = ( $module['recoverable'] ?? null );
if ( $slug === 'analytics-4' ) {
$can_read_shared_module_data = ( $modules_permissions['googlesitekit_read_shared_module_data::["analytics-4"]'] ?? false );
$this->ga_module['can_view'] = $can_view_dashboard || ( $can_read_shared_module_data && ! $is_recoverable );
$this->ga_module['connected'] = ( $module['connected'] ?? false );
}
if ( $slug === 'search-console' ) {
$can_read_shared_module_data = ( $modules_permissions['googlesitekit_read_shared_module_data::["search-console"]'] ?? false );
$this->search_console_module['can_view'] = $can_view_dashboard || ( $can_read_shared_module_data && ! $is_recoverable );
}
}
}
/**
* Holds the parsed preload paths for preloading some Site Kit API data.
*
* @return string[]
*/
public function get_preload_paths(): array {
$rest_root = ( \class_exists( REST_Routes::class ) ) ? REST_Routes::REST_ROOT : '';
return [
'permissions' => '/' . $rest_root . '/core/user/data/permissions',
'modules' => '/' . $rest_root . '/core/modules/data/list',
];
}
/**
* Runs the given paths through the `rest_preload_api_request` method.
*
* @param string[] $paths The paths to add to `rest_preload_api_request`.
*
* @return array<array|null> The array with all the now filled in preloaded data.
*/
public function get_preloaded_data( array $paths ): array {
$preload_paths = \apply_filters( 'googlesitekit_apifetch_preload_paths', [] );
$actual_paths = \array_intersect( $paths, $preload_paths );
return \array_reduce(
\array_unique( $actual_paths ),
'rest_preload_api_request',
[]
);
}
/**
* Creates a valid activation URL for the Site Kit plugin.
*
* @return string
*/
public function get_activate_url(): string {
return \html_entity_decode(
\wp_nonce_url(
\self_admin_url( 'plugins.php?action=activate&plugin=' . self::SITE_KIT_FILE ),
'activate-plugin_' . self::SITE_KIT_FILE
)
);
}
/**
* Creates a valid install URL for the Site Kit plugin.
*
* @return string
*/
public function get_install_url(): string {
return \html_entity_decode(
\wp_nonce_url(
\self_admin_url( 'update.php?action=install-plugin&plugin=google-site-kit' ),
'install-plugin_google-site-kit'
)
);
}
/**
* Creates a valid update URL for the Site Kit plugin.
*
* @return string
*/
public function get_update_url(): string {
return \html_entity_decode(
\wp_nonce_url(
\self_admin_url( 'update.php?action=upgrade-plugin&plugin=' . self::SITE_KIT_FILE ),
'upgrade-plugin_' . self::SITE_KIT_FILE
)
);
}
/**
* Creates a valid setup URL for the Site Kit plugin.
*
* @return string
*/
public function get_setup_url(): string {
return \self_admin_url( 'admin.php?page=googlesitekit-splash' );
}
}
@@ -0,0 +1,18 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Infrastructure\Nonces;
/**
* Repository for WP nonces.
*/
class Nonce_Repository {
/**
* Creates the nonce for a WP REST request.
*
* @return string
*/
public function get_rest_nonce(): string {
return \wp_create_nonce( 'wp_rest' );
}
}
@@ -0,0 +1,49 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
namespace Yoast\WP\SEO\Dashboard\Infrastructure\Score_Groups;
use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Type;
use Yoast\WP\SEO\Dashboard\Domain\Score_Groups\Score_Groups_Interface;
use Yoast\WP\SEO\Dashboard\Domain\Taxonomies\Taxonomy;
/**
* Getting links for score groups.
*/
class Score_Group_Link_Collector {
/**
* Builds the view link of the score group.
*
* @param Score_Groups_Interface $score_group The score group.
* @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|null The view link of the score.
*/
public function get_view_link( Score_Groups_Interface $score_group, Content_Type $content_type, ?Taxonomy $taxonomy, ?int $term_id ): ?string {
$posts_page = \admin_url( 'edit.php' );
$args = [
'post_status' => 'publish',
'post_type' => $content_type->get_name(),
$score_group->get_filter_key() => $score_group->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 ) {
return null;
}
$term = \get_term( $term_id );
$args[ $query_var ] = $term->slug;
return \add_query_arg( $args, $posts_page );
}
}
@@ -0,0 +1,75 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
namespace Yoast\WP\SEO\Dashboard\Infrastructure\Score_Results\Readability_Score_Results;
use WPSEO_Utils;
use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Type;
use Yoast\WP\SEO\Dashboard\Domain\Score_Groups\Readability_Score_Groups\Readability_Score_Groups_Interface;
use Yoast\WP\SEO\Dashboard\Domain\Score_Results\Score_Results_Not_Found_Exception;
use Yoast\WP\SEO\Dashboard\Infrastructure\Score_Results\Score_Results_Collector_Interface;
/**
* The caching decorator to get readability score results.
*/
class Cached_Readability_Score_Results_Collector implements Score_Results_Collector_Interface {
public const READABILITY_SCORES_TRANSIENT = 'wpseo_readability_scores';
/**
* The actual collector implementation.
*
* @var Readability_Score_Results_Collector
*/
private $readability_score_results_collector;
/**
* The constructor.
*
* @param Readability_Score_Results_Collector $readability_score_results_collector The collector implementation to
* use.
*/
public function __construct( Readability_Score_Results_Collector $readability_score_results_collector ) {
$this->readability_score_results_collector = $readability_score_results_collector;
}
/**
* Retrieves readability score results for a content type.
* Based on caching returns either the result or gets it from the collector.
*
* @param Readability_Score_Groups_Interface[] $score_groups All readability score groups.
* @param Content_Type $content_type The content type.
* @param int|null $term_id The ID of the term we're filtering for.
* @param bool|null $is_troubleshooting Whether we're in troubleshooting mode.
*
* @return array<string, object|bool|float> The readability score results for a content type.
*
* @throws Score_Results_Not_Found_Exception When the query of getting score results fails.
*/
public function get_score_results(
array $score_groups,
Content_Type $content_type,
?int $term_id,
?bool $is_troubleshooting
) {
$content_type_name = $content_type->get_name();
$transient_name = self::READABILITY_SCORES_TRANSIENT . '_' . $content_type_name . ( ( $term_id === null ) ? '' : '_' . $term_id );
$results = [];
$transient = \get_transient( $transient_name );
if ( $is_troubleshooting !== true && $transient !== false ) {
$results['scores'] = \json_decode( $transient, false );
$results['cache_used'] = true;
$results['query_time'] = 0;
return $results;
}
$results = $this->readability_score_results_collector->get_score_results( $score_groups, $content_type, $term_id, $is_troubleshooting );
$results['cache_used'] = false;
if ( $is_troubleshooting !== true ) {
\set_transient( $transient_name, WPSEO_Utils::format_json_encode( $results['scores'] ), \MINUTE_IN_SECONDS );
}
return $results;
}
}
@@ -0,0 +1,141 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
namespace Yoast\WP\SEO\Dashboard\Infrastructure\Score_Results\Readability_Score_Results;
use Yoast\WP\Lib\Model;
use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Type;
use Yoast\WP\SEO\Dashboard\Domain\Score_Groups\Readability_Score_Groups\Readability_Score_Groups_Interface;
use Yoast\WP\SEO\Dashboard\Domain\Score_Results\Score_Results_Not_Found_Exception;
use Yoast\WP\SEO\Dashboard\Infrastructure\Score_Results\Score_Results_Collector_Interface;
/**
* Getting readability score results from the indexable database table.
*/
class Readability_Score_Results_Collector implements Score_Results_Collector_Interface {
/**
* Retrieves readability score results for a content type.
*
* @param Readability_Score_Groups_Interface[] $readability_score_groups All readability score groups.
* @param Content_Type $content_type The content type.
* @param int|null $term_id The ID of the term we're filtering for.
* @param bool|null $is_troubleshooting Whether we're in troubleshooting mode.
*
* @return array<string, object|bool|float> The readability score results for a content type.
*
* @throws Score_Results_Not_Found_Exception When the query of getting score results fails.
*/
public function get_score_results( array $readability_score_groups, Content_Type $content_type, ?int $term_id, ?bool $is_troubleshooting ) {
global $wpdb;
$results = [];
$content_type_name = $content_type->get_name();
$select = $this->build_select( $readability_score_groups, $is_troubleshooting );
$replacements = \array_merge(
\array_values( $select['replacements'] ),
[
Model::get_table_name( 'Indexable' ),
$content_type_name,
]
);
if ( $term_id === null ) {
//phpcs:disable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- $replacements is an array with the correct replacements.
//phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $select['fields'] is an array of simple strings with placeholders.
$query = $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
}
else {
$replacements[] = $wpdb->term_relationships;
$replacements[] = $term_id;
//phpcs:disable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- $replacements is an array with the correct replacements.
//phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $select['fields'] is an array of simple strings with placeholders.
$query = $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
}
$start_time = \microtime( true );
//phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- $query is prepared above.
//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( $query );
//phpcs:enable
if ( $current_scores === null ) {
throw new Score_Results_Not_Found_Exception();
}
$end_time = \microtime( true );
$results['scores'] = $current_scores;
$results['query_time'] = ( $end_time - $start_time );
return $results;
}
/**
* Builds the select statement for the readability scores query.
*
* @param Readability_Score_Groups_Interface[] $readability_score_groups All readability score groups.
* @param bool|null $is_troubleshooting Whether we're in troubleshooting mode.
*
* @return array<string, string> The select statement for the readability scores query.
*/
private function build_select( array $readability_score_groups, ?bool $is_troubleshooting ): array {
$select_fields = [];
$select_replacements = [];
// When we don't troubleshoot, we're interested in the amount of posts in a group, when we troubleshoot we want to gather the actual IDs.
$select_operation = ( $is_troubleshooting === true ) ? 'GROUP_CONCAT' : 'COUNT';
$selected_info = ( $is_troubleshooting === true ) ? 'I.object_id' : '1';
foreach ( $readability_score_groups as $readability_score_group ) {
$min = $readability_score_group->get_min_score();
$max = $readability_score_group->get_max_score();
$name = $readability_score_group->get_name();
if ( $min === null && $max === null ) {
$select_fields[] = "{$select_operation}(CASE WHEN I.readability_score = 0 AND I.estimated_reading_time_minutes IS NULL THEN {$selected_info} END) AS %i";
$select_replacements[] = $name;
}
else {
$needs_ert = ( $min === 1 ) ? ' OR (I.readability_score = 0 AND I.estimated_reading_time_minutes IS NOT NULL)' : '';
$select_fields[] = "{$select_operation}(CASE WHEN ( I.readability_score >= %d AND I.readability_score <= %d ){$needs_ert} THEN {$selected_info} END) AS %i";
$select_replacements[] = $min;
$select_replacements[] = $max;
$select_replacements[] = $name;
}
}
$select_fields = \implode( ', ', $select_fields );
return [
'fields' => $select_fields,
'replacements' => $select_replacements,
];
}
}
@@ -0,0 +1,25 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
namespace Yoast\WP\SEO\Dashboard\Infrastructure\Score_Results;
use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Type;
use Yoast\WP\SEO\Dashboard\Domain\Score_Groups\Score_Groups_Interface;
/**
* The interface of score result collectors.
*/
interface Score_Results_Collector_Interface {
/**
* Retrieves the score results for a content type.
*
* @param Score_Groups_Interface[] $score_groups All score groups.
* @param Content_Type $content_type The content type.
* @param int|null $term_id The ID of the term we're filtering for.
* @param bool|null $is_troubleshooting Whether we're in troubleshooting mode.
*
* @return array<string, string> The score results for a content type.
*/
public function get_score_results( array $score_groups, Content_Type $content_type, ?int $term_id, ?bool $is_troubleshooting );
}
@@ -0,0 +1,73 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
namespace Yoast\WP\SEO\Dashboard\Infrastructure\Score_Results\SEO_Score_Results;
use WPSEO_Utils;
use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Type;
use Yoast\WP\SEO\Dashboard\Domain\Score_Groups\SEO_Score_Groups\SEO_Score_Groups_Interface;
use Yoast\WP\SEO\Dashboard\Domain\Score_Results\Score_Results_Not_Found_Exception;
use Yoast\WP\SEO\Dashboard\Infrastructure\Score_Results\Score_Results_Collector_Interface;
/**
* The caching decorator to get readability score results.
*/
class Cached_SEO_Score_Results_Collector implements Score_Results_Collector_Interface {
public const SEO_SCORES_TRANSIENT = 'wpseo_seo_scores';
/**
* The actual collector implementation.
*
* @var SEO_Score_Results_Collector
*/
private $seo_score_results_collector;
/**
* The constructor.
*
* @param SEO_Score_Results_Collector $seo_score_results_collector The collector implementation to use.
*/
public function __construct( SEO_Score_Results_Collector $seo_score_results_collector ) {
$this->seo_score_results_collector = $seo_score_results_collector;
}
/**
* Retrieves the SEO score results for a content type.
* Based on caching returns either the result or gets it from the collector.
*
* @param SEO_Score_Groups_Interface[] $score_groups All SEO score groups.
* @param Content_Type $content_type The content type.
* @param int|null $term_id The ID of the term we're filtering for.
* @param bool|null $is_troubleshooting Whether we're in troubleshooting mode.
*
* @return array<string, object|bool|float> The SEO score results for a content type.
*
* @throws Score_Results_Not_Found_Exception When the query of getting score results fails.
*/
public function get_score_results(
array $score_groups,
Content_Type $content_type,
?int $term_id,
?bool $is_troubleshooting
) {
$content_type_name = $content_type->get_name();
$transient_name = self::SEO_SCORES_TRANSIENT . '_' . $content_type_name . ( ( $term_id === null ) ? '' : '_' . $term_id );
$results = [];
$transient = \get_transient( $transient_name );
if ( $is_troubleshooting !== true && $transient !== false ) {
$results['scores'] = \json_decode( $transient, false );
$results['cache_used'] = true;
$results['query_time'] = 0;
return $results;
}
$results = $this->seo_score_results_collector->get_score_results( $score_groups, $content_type, $term_id, $is_troubleshooting );
$results['cache_used'] = false;
if ( $is_troubleshooting !== true ) {
\set_transient( $transient_name, WPSEO_Utils::format_json_encode( $results['scores'] ), \MINUTE_IN_SECONDS );
}
return $results;
}
}
@@ -0,0 +1,142 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
namespace Yoast\WP\SEO\Dashboard\Infrastructure\Score_Results\SEO_Score_Results;
use Yoast\WP\Lib\Model;
use Yoast\WP\SEO\Dashboard\Domain\Content_Types\Content_Type;
use Yoast\WP\SEO\Dashboard\Domain\Score_Groups\SEO_Score_Groups\SEO_Score_Groups_Interface;
use Yoast\WP\SEO\Dashboard\Domain\Score_Results\Score_Results_Not_Found_Exception;
use Yoast\WP\SEO\Dashboard\Infrastructure\Score_Results\Score_Results_Collector_Interface;
/**
* Getting SEO score results from the indexable database table.
*/
class SEO_Score_Results_Collector implements Score_Results_Collector_Interface {
/**
* Retrieves the SEO score results for a content type.
*
* @param SEO_Score_Groups_Interface[] $seo_score_groups All SEO score groups.
* @param Content_Type $content_type The content type.
* @param int|null $term_id The ID of the term we're filtering for.
* @param bool|null $is_troubleshooting Whether we're in troubleshooting mode.
*
* @return array<string, object|bool|float> The SEO score results for a content type.
*
* @throws Score_Results_Not_Found_Exception When the query of getting score results fails.
*/
public function get_score_results( array $seo_score_groups, Content_Type $content_type, ?int $term_id, ?bool $is_troubleshooting ): array {
global $wpdb;
$results = [];
$content_type_name = $content_type->get_name();
$select = $this->build_select( $seo_score_groups, $is_troubleshooting );
$replacements = \array_merge(
\array_values( $select['replacements'] ),
[
Model::get_table_name( 'Indexable' ),
$content_type_name,
]
);
if ( $term_id === null ) {
//phpcs:disable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- $replacements is an array with the correct replacements.
//phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $select['fields'] is an array of simple strings with placeholders.
$query = $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
}
else {
$replacements[] = $wpdb->term_relationships;
$replacements[] = $term_id;
//phpcs:disable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- $replacements is an array with the correct replacements.
//phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $select['fields'] is an array of simple strings with placeholders.
$query = $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
}
$start_time = \microtime( true );
//phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- $query is prepared above.
//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( $query );
//phpcs:enable
if ( $current_scores === null ) {
throw new Score_Results_Not_Found_Exception();
}
$end_time = \microtime( true );
$results['scores'] = $current_scores;
$results['query_time'] = ( $end_time - $start_time );
return $results;
}
/**
* Builds the select statement for the SEO scores query.
*
* @param SEO_Score_Groups_Interface[] $seo_score_groups All SEO score groups.
* @param bool|null $is_troubleshooting Whether we're in troubleshooting mode.
*
* @return array<string, string> The select statement for the SEO scores query.
*/
private function build_select( array $seo_score_groups, ?bool $is_troubleshooting ): array {
$select_fields = [];
$select_replacements = [];
// When we don't troubleshoot, we're interested in the amount of posts in a group, when we troubleshoot we want to gather the actual IDs.
$select_operation = ( $is_troubleshooting === true ) ? 'GROUP_CONCAT' : 'COUNT';
$selected_info = ( $is_troubleshooting === true ) ? 'I.object_id' : '1';
foreach ( $seo_score_groups as $seo_score_group ) {
$min = $seo_score_group->get_min_score();
$max = $seo_score_group->get_max_score();
$name = $seo_score_group->get_name();
if ( $min === null || $max === null ) {
$select_fields[] = "{$select_operation}(CASE WHEN I.primary_focus_keyword_score = 0 OR I.primary_focus_keyword_score IS NULL THEN {$selected_info} END) AS %i";
$select_replacements[] = $name;
}
else {
$select_fields[] = "{$select_operation}(CASE WHEN I.primary_focus_keyword_score >= %d AND I.primary_focus_keyword_score <= %d THEN {$selected_info} END) AS %i";
$select_replacements[] = $min;
$select_replacements[] = $max;
$select_replacements[] = $name;
}
}
$select_fields = \implode( ', ', $select_fields );
return [
'fields' => $select_fields,
'replacements' => $select_replacements,
];
}
}
@@ -0,0 +1,38 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Infrastructure\Search_Console;
use Yoast\WP\SEO\Dashboard\Domain\Data_Provider\Parameters;
/**
* Domain object to add search console specific data to the parameters.
*/
class Search_Console_Parameters extends Parameters {
/**
* The search dimensions to query.
*
* @var string[]
*/
private $dimensions;
/**
* Sets the dimension parameter.
*
* @param array<string> $dimensions The dimensions.
*
* @return void
*/
public function set_dimensions( array $dimensions ): void {
$this->dimensions = $dimensions;
}
/**
* Getter for the dimensions.
*
* @return string[]
*/
public function get_dimensions(): array {
return $this->dimensions;
}
}
@@ -0,0 +1,191 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
namespace Yoast\WP\SEO\Dashboard\Infrastructure\Search_Console;
use Google\Site_Kit_Dependencies\Google\Service\SearchConsole\ApiDataRow;
use WP_REST_Response;
use Yoast\WP\SEO\Dashboard\Domain\Data_Provider\Data_Container;
use Yoast\WP\SEO\Dashboard\Domain\Search_Console\Failed_Request_Exception;
use Yoast\WP\SEO\Dashboard\Domain\Search_Console\Unexpected_Response_Exception;
use Yoast\WP\SEO\Dashboard\Domain\Search_Rankings\Comparison_Search_Ranking_Data;
use Yoast\WP\SEO\Dashboard\Domain\Search_Rankings\Search_Ranking_Data;
/**
* The site API adapter to make calls to the Search Console API, via the Site_Kit plugin.
*/
class Site_Kit_Search_Console_Adapter {
/**
* Holds the api call class.
*
* @var Site_Kit_Search_Console_Api_Call $site_kit_search_console_api_call
*/
private $site_kit_search_console_api_call;
/**
* The constructor.
*
* @param Site_Kit_Search_Console_Api_Call $site_kit_search_console_api_call The api call class.
*/
public function __construct( Site_Kit_Search_Console_Api_Call $site_kit_search_console_api_call ) {
$this->site_kit_search_console_api_call = $site_kit_search_console_api_call;
}
/**
* The wrapper method to do a Site Kit API request for Search Console.
*
* @param Search_Console_Parameters $parameters The parameters.
*
* @throws Failed_Request_Exception When the request responds with an error from Site Kit.
* @throws Unexpected_Response_Exception When the request responds with an unexpected format.
* @return Data_Container The Site Kit API response.
*/
public function get_data( Search_Console_Parameters $parameters ): Data_Container {
$api_parameters = $this->build_parameters( $parameters );
$response = $this->site_kit_search_console_api_call->do_request( $api_parameters );
$this->validate_response( $response );
return $this->parse_response( $response->get_data() );
}
/**
* The wrapper method to do a comparison Site Kit API request for Search Console.
*
* @param Search_Console_Parameters $parameters The parameters.
*
* @throws Failed_Request_Exception When the request responds with an error from Site Kit.
* @throws Unexpected_Response_Exception When the request responds with an unexpected format.
* @return Data_Container The Site Kit API response.
*/
public function get_comparison_data( Search_Console_Parameters $parameters ): Data_Container {
$api_parameters = $this->build_parameters( $parameters );
// Since we're doing a comparison request, we need to increase the date range to the start of the previous period. We'll later split the data into two periods.
$api_parameters['startDate'] = $parameters->get_compare_start_date();
$response = $this->site_kit_search_console_api_call->do_request( $api_parameters );
$this->validate_response( $response );
return $this->parse_comparison_response( $response->get_data(), $parameters->get_compare_end_date() );
}
/**
* Builds the parameters to be used in the Site Kit API request.
*
* @param Search_Console_Parameters $parameters The parameters.
*
* @return array<string, array<string, string>> The Site Kit API parameters.
*/
private function build_parameters( Search_Console_Parameters $parameters ): array {
$api_parameters = [
'startDate' => $parameters->get_start_date(),
'endDate' => $parameters->get_end_date(),
'dimensions' => $parameters->get_dimensions(),
];
if ( $parameters->get_limit() !== 0 ) {
$api_parameters['limit'] = $parameters->get_limit();
}
return $api_parameters;
}
/**
* Parses a response for a comparison Site Kit API request for Search Analytics.
*
* @param ApiDataRow[] $response The response to parse.
* @param string $compare_end_date The compare end date.
*
* @throws Unexpected_Response_Exception When the comparison request responds with an unexpected format.
* @return Data_Container The parsed comparison Site Kit API response.
*/
private function parse_comparison_response( array $response, ?string $compare_end_date ): Data_Container {
$data_container = new Data_Container();
$comparison_search_ranking_data = new Comparison_Search_Ranking_Data();
foreach ( $response as $ranking_date ) {
if ( ! \is_a( $ranking_date, ApiDataRow::class ) ) {
throw new Unexpected_Response_Exception();
}
$ranking_data = new Search_Ranking_Data( $ranking_date->getClicks(), $ranking_date->getCtr(), $ranking_date->getImpressions(), $ranking_date->getPosition(), $ranking_date->getKeys()[0] );
// Now split the data into two periods.
if ( $ranking_date->getKeys()[0] <= $compare_end_date ) {
$comparison_search_ranking_data->add_previous_traffic_data( $ranking_data );
}
else {
$comparison_search_ranking_data->add_current_traffic_data( $ranking_data );
}
}
$data_container->add_data( $comparison_search_ranking_data );
return $data_container;
}
/**
* Parses a response for a Site Kit API request for Search Analytics.
*
* @param ApiDataRow[] $response The response to parse.
*
* @throws Unexpected_Response_Exception When the request responds with an unexpected format.
* @return Data_Container The parsed Site Kit API response.
*/
private function parse_response( array $response ): Data_Container {
$search_ranking_data_container = new Data_Container();
foreach ( $response as $ranking ) {
if ( ! \is_a( $ranking, ApiDataRow::class ) ) {
throw new Unexpected_Response_Exception();
}
/**
* Filter: 'wpseo_transform_dashboard_subject_for_testing' - Allows overriding subjects like URLs for the dashboard, to facilitate testing in local environments.
*
* @param string $url The subject to be transformed.
*
* @internal
*/
$subject = \apply_filters( 'wpseo_transform_dashboard_subject_for_testing', $ranking->getKeys()[0] );
$search_ranking_data_container->add_data( new Search_Ranking_Data( $ranking->getClicks(), $ranking->getCtr(), $ranking->getImpressions(), $ranking->getPosition(), $subject ) );
}
return $search_ranking_data_container;
}
/**
* Validates the response coming from Search Console.
*
* @param WP_REST_Response $response The response we want to validate.
*
* @return void.
*
* @throws Failed_Request_Exception When the request responds with an error from Site Kit.
* @throws Unexpected_Response_Exception When the request responds with an unexpected format.
*/
private function validate_response( WP_REST_Response $response ): void {
if ( $response->is_error() ) {
$error_data = $response->as_error()->get_error_data();
$error_status_code = ( $error_data['status'] ?? 500 );
throw new Failed_Request_Exception(
\wp_kses_post(
$response->as_error()
->get_error_message()
),
(int) $error_status_code
);
}
if ( ! \is_array( $response->get_data() ) ) {
throw new Unexpected_Response_Exception();
}
}
}
@@ -0,0 +1,33 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
namespace Yoast\WP\SEO\Dashboard\Infrastructure\Search_Console;
use WP_REST_Request;
use WP_REST_Response;
/**
* Class that hold the code to do the REST call to the Site Kit api.
*
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
*/
class Site_Kit_Search_Console_Api_Call {
/**
* The search analytics API route path.
*/
private const SEARCH_CONSOLE_DATA_SEARCH_ANALYTICS_ROUTE = '/google-site-kit/v1/modules/search-console/data/searchanalytics';
/**
* Runs the internal REST api call.
*
* @param array<string, array<string, string>> $api_parameters The api parameters.
*
* @return WP_REST_Response
*/
public function do_request( array $api_parameters ): WP_REST_Response {
$request = new WP_REST_Request( 'GET', self::SEARCH_CONSOLE_DATA_SEARCH_ANALYTICS_ROUTE );
$request->set_query_params( $api_parameters );
return \rest_do_request( $request );
}
}
@@ -0,0 +1,106 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\Dashboard\Infrastructure\Taxonomies;
use WP_Taxonomy;
use Yoast\WP\SEO\Dashboard\Domain\Taxonomies\Taxonomy;
/**
* Class that collects taxonomies and relevant information.
*/
class Taxonomies_Collector {
/**
* The taxonomy validator.
*
* @var Taxonomy_Validator
*/
private $taxonomy_validator;
/**
* The constructor.
*
* @param Taxonomy_Validator $taxonomy_validator The taxonomy validator.
*/
public function __construct( Taxonomy_Validator $taxonomy_validator ) {
$this->taxonomy_validator = $taxonomy_validator;
}
/**
* Returns a custom pair of taxonomy/content type, that's been given by users via hooks.
*
* @param string $content_type The content type we're hooking for.
*
* @return Taxonomy|null The hooked filtering taxonomy.
*/
public function get_custom_filtering_taxonomy( string $content_type ) {
/**
* Filter: 'wpseo_{$content_type}_filtering_taxonomy' - Allows overriding which taxonomy filters the content type.
*
* @internal
*
* @param string $filtering_taxonomy The taxonomy that filters the content type.
*/
$filtering_taxonomy = \apply_filters( "wpseo_{$content_type}_filtering_taxonomy", '' );
if ( $filtering_taxonomy !== '' ) {
$taxonomy = $this->get_taxonomy( $filtering_taxonomy, $content_type );
if ( $taxonomy ) {
return $taxonomy;
}
\_doing_it_wrong(
'Filter: \'wpseo_{$content_type}_filtering_taxonomy\'',
'The `wpseo_{$content_type}_filtering_taxonomy` filter should return a public taxonomy, available in REST API, that is associated with that content type.',
'YoastSEO v24.1'
);
}
return null;
}
/**
* Returns the fallback, WP-native category taxonomy, if it's associated with the specific content type.
*
* @param string $content_type The content type.
*
* @return Taxonomy|null The taxonomy object for the category taxonomy.
*/
public function get_fallback_taxonomy( string $content_type ): ?Taxonomy {
return $this->get_taxonomy( 'category', $content_type );
}
/**
* Returns the taxonomy object that filters a specific content type.
*
* @param string $taxonomy_name The name of the taxonomy we're going to build the object for.
* @param string $content_type The content type that the taxonomy object is filtering.
*
* @return Taxonomy|null The taxonomy object.
*/
public function get_taxonomy( string $taxonomy_name, string $content_type ): ?Taxonomy {
$taxonomy = \get_taxonomy( $taxonomy_name );
if ( $this->taxonomy_validator->is_valid_taxonomy( $taxonomy, $content_type ) ) {
return new Taxonomy( $taxonomy->name, $taxonomy->label, $this->get_taxonomy_rest_url( $taxonomy ) );
}
return null;
}
/**
* Builds the REST API URL for the taxonomy.
*
* @param WP_Taxonomy $taxonomy The taxonomy we want to build the REST API URL for.
*
* @return string The REST API URL for the taxonomy.
*/
protected function get_taxonomy_rest_url( WP_Taxonomy $taxonomy ): string {
$rest_base = ( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
$rest_namespace = ( $taxonomy->rest_namespace ) ? $taxonomy->rest_namespace : 'wp/v2';
return \rest_url( "{$rest_namespace}/{$rest_base}" );
}
}
@@ -0,0 +1,27 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong
namespace Yoast\WP\SEO\Dashboard\Infrastructure\Taxonomies;
use WP_Taxonomy;
/**
* Class that validates taxonomies.
*/
class Taxonomy_Validator {
/**
* Returns whether the taxonomy in question is valid and associated with a given content type.
*
* @param WP_Taxonomy|false|null $taxonomy The taxonomy to check.
* @param string $content_type The name of the content type to check.
*
* @return bool Whether the taxonomy in question is valid.
*/
public function is_valid_taxonomy( $taxonomy, string $content_type ): bool {
return \is_a( $taxonomy, 'WP_Taxonomy' )
&& $taxonomy->public
&& $taxonomy->show_in_rest
&& \in_array( $taxonomy->name, \get_object_taxonomies( $content_type ), true );
}
}
@@ -0,0 +1,30 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Infrastructure\Tracking;
/**
* Interface for the Site Kit Usage Tracking Repository.
*
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
*/
interface Setup_Steps_Tracking_Repository_Interface {
/**
* Sets an option inside the Site Kit usage options array..
*
* @param string $element_name The name of the array element to set.
* @param string $element_value The value of the array element to set.
*
* @return bool False when the update failed, true when the update succeeded.
*/
public function set_setup_steps_tracking_element( string $element_name, string $element_value ): bool;
/**
* Gets an option inside the Site Kit usage options array..
*
* @param string $element_name The name of the array element to get.
*
* @return string The value if present, empty string if not.
*/
public function get_setup_steps_tracking_element( string $element_name ): string;
}
@@ -0,0 +1,52 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\Infrastructure\Tracking;
use Yoast\WP\SEO\Helpers\Options_Helper;
/**
* Stores and retrieves data about Site Kit usage.
*
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
*/
class Setup_Steps_Tracking_Repository implements Setup_Steps_Tracking_Repository_Interface {
/**
* Holds the Options_Helper instance.
*
* @var Options_Helper
*/
private $options_helper;
/**
* Constructs the class.
*
* @param Options_Helper $options_helper The options helper.
*/
public function __construct( Options_Helper $options_helper ) {
$this->options_helper = $options_helper;
}
/**
* Sets an option inside the Site Kit usage options array.
*
* @param string $element_name The name of the option to set.
* @param string $element_value The value of the option to set.
*
* @return bool False when the update failed, true when the update succeeded.
*/
public function set_setup_steps_tracking_element( string $element_name, string $element_value ): bool {
return $this->options_helper->set( 'site_kit_tracking_' . $element_name, $element_value );
}
/**
* Gets an option inside the Site Kit usage options array.
*
* @param string $element_name The name of the option to get.
*
* @return string The value if present, empty string if not.
*/
public function get_setup_steps_tracking_element( string $element_name ): string {
return $this->options_helper->get( 'site_kit_tracking_' . $element_name, '' );
}
}
@@ -0,0 +1,57 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\User_Interface\Configuration;
use Google\Site_Kit\Core\Permissions\Permissions;
use Yoast\WP\SEO\Conditionals\Google_Site_Kit_Feature_Conditional;
use Yoast\WP\SEO\Conditionals\Third_Party\Site_Kit_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;
/**
* Enables the needed Site Kit capabilities for the SEO manager role.
*/
class Site_Kit_Capabilities_Integration implements Integration_Interface {
/**
* Registers needed filters.
*
* @return void
*/
public function register_hooks() {
\add_filter( 'user_has_cap', [ $this, 'enable_site_kit_capabilities' ], 10, 2 );
}
/**
* The needed conditionals.
*
* @return array<string>
*/
public static function get_conditionals() {
// This cannot have the Admin Conditional since it also needs to run in Rest requests.
return [ Google_Site_Kit_Feature_Conditional::class, Site_Kit_Conditional::class ];
}
/**
* Checks if the Site Kit capabilities need to be enabled for a manager.
*
* @param array<string> $all_caps All the current capabilities of the current user.
* @param array<string> $cap_to_check The capability to check against.
*
* @return array<string>
*/
public function enable_site_kit_capabilities( $all_caps, $cap_to_check ) {
if ( ! isset( $cap_to_check[0] ) || ! \class_exists( Permissions::class ) ) {
return $all_caps;
}
$user = \wp_get_current_user();
$caps_to_check = [
Permissions::SETUP,
Permissions::VIEW_DASHBOARD,
];
if ( \in_array( $cap_to_check[0], $caps_to_check, true ) && \in_array( 'wpseo_manager', $user->roles, true ) ) {
$all_caps[ $cap_to_check[0] ] = true;
}
return $all_caps;
}
}
@@ -0,0 +1,139 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\User_Interface\Configuration;
use Exception;
use WP_Error;
use WP_REST_Request;
use WP_REST_Response;
use Yoast\WP\SEO\Conditionals\Google_Site_Kit_Feature_Conditional;
use Yoast\WP\SEO\Dashboard\Infrastructure\Configuration\Permanently_Dismissed_Site_Kit_Configuration_Repository_Interface;
use Yoast\WP\SEO\Helpers\Capability_Helper;
use Yoast\WP\SEO\Main;
use Yoast\WP\SEO\Routes\Route_Interface;
/**
* Registers a route to set whether the Site Kit configuration is permanently dismissed.
*
* @makePublic
*
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
*/
class Site_Kit_Configuration_Dismissal_Route implements Route_Interface {
/**
* The namespace for this route.
*
* @var string
*/
public const ROUTE_NAMESPACE = Main::API_V1_NAMESPACE;
/**
* The prefix for this route.
*
* @var string
*/
public const ROUTE_PREFIX = '/site_kit_configuration_permanent_dismissal';
/**
* Holds the introductions collector instance.
*
* @var Permanently_Dismissed_Site_Kit_Configuration_Repository_Interface
*/
private $permanently_dismissed_site_kit_configuration_repository;
/**
* Holds the capabilit helper instance.
*
* @var Capability_Helper
*/
private $capability_helper;
/**
* The needed conditionals.
*
* @return array<string>
*/
public static function get_conditionals() {
// This cannot have the Admin Conditional since it also needs to run in Rest requests.
return [ Google_Site_Kit_Feature_Conditional::class ];
}
/**
* Constructs the class.
*
* @param Permanently_Dismissed_Site_Kit_Configuration_Repository_Interface $permanently_dismissed_site_kit_configuration_repository The repository.
* @param Capability_Helper $capability_helper The capability helper.
*/
public function __construct(
Permanently_Dismissed_Site_Kit_Configuration_Repository_Interface $permanently_dismissed_site_kit_configuration_repository,
Capability_Helper $capability_helper
) {
$this->permanently_dismissed_site_kit_configuration_repository = $permanently_dismissed_site_kit_configuration_repository;
$this->capability_helper = $capability_helper;
}
/**
* Registers routes with WordPress.
*
* @return void
*/
public function register_routes() {
\register_rest_route(
self::ROUTE_NAMESPACE,
self::ROUTE_PREFIX,
[
[
'methods' => 'POST',
'callback' => [ $this, 'set_site_kit_configuration_permanent_dismissal' ],
'permission_callback' => [ $this, 'check_capabilities' ],
'args' => [
'is_dismissed' => [
'required' => true,
'type' => 'bool',
'sanitize_callback' => 'rest_sanitize_boolean',
],
],
],
]
);
}
/**
* Sets whether the Site Kit configuration is permanently dismissed.
*
* @param WP_REST_Request $request The request object.
*
* @return WP_REST_Response|WP_Error The success or failure response.
*/
public function set_site_kit_configuration_permanent_dismissal( WP_REST_Request $request ) {
$is_dismissed = $request->get_param( 'is_dismissed' );
try {
$result = $this->permanently_dismissed_site_kit_configuration_repository->set_site_kit_configuration_dismissal( $is_dismissed );
} catch ( Exception $exception ) {
return new WP_Error(
'wpseo_set_site_kit_configuration_permanent_dismissal_error',
$exception->getMessage(),
(object) []
);
}
return new WP_REST_Response(
[
'success' => $result,
],
( $result ) ? 200 : 400
);
}
/**
* Checks if the current user has the required capabilities.
*
* @return bool
*/
public function check_capabilities() {
return $this->capability_helper->current_user_can( 'wpseo_manage_options' );
}
}
@@ -0,0 +1,140 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\User_Interface\Configuration;
use Exception;
use WP_Error;
use WP_REST_Request;
use WP_REST_Response;
use Yoast\WP\SEO\Conditionals\Google_Site_Kit_Feature_Conditional;
use Yoast\WP\SEO\Conditionals\Third_Party\Site_Kit_Conditional;
use Yoast\WP\SEO\Dashboard\Infrastructure\Configuration\Site_Kit_Consent_Repository_Interface;
use Yoast\WP\SEO\Helpers\Capability_Helper;
use Yoast\WP\SEO\Main;
use Yoast\WP\SEO\Routes\Route_Interface;
/**
* Registers a route to set whether the Site Kit configuration is permanently dismissed.
*
* @makePublic
*
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
*/
class Site_Kit_Consent_Management_Route implements Route_Interface {
/**
* The namespace for this route.
*
* @var string
*/
public const ROUTE_NAMESPACE = Main::API_V1_NAMESPACE;
/**
* The prefix for this route.
*
* @var string
*/
public const ROUTE_PREFIX = '/site_kit_manage_consent';
/**
* Holds the repository instance.
*
* @var Site_Kit_Consent_Repository_Interface
*/
private $site_kit_consent_repository;
/**
* Holds the capabilit helper instance.
*
* @var Capability_Helper
*/
private $capability_helper;
/**
* The needed conditionals.
*
* @return array<string>
*/
public static function get_conditionals() {
// This cannot have the Admin Conditional since it also needs to run in Rest requests.
return [ Google_Site_Kit_Feature_Conditional::class, Site_Kit_Conditional::class ];
}
/**
* Constructs the class.
*
* @param Site_Kit_Consent_Repository_Interface $site_kit_consent_repository The repository.
* @param Capability_Helper $capability_helper The capability helper.
*/
public function __construct(
Site_Kit_Consent_Repository_Interface $site_kit_consent_repository,
Capability_Helper $capability_helper
) {
$this->site_kit_consent_repository = $site_kit_consent_repository;
$this->capability_helper = $capability_helper;
}
/**
* Registers routes with WordPress.
*
* @return void
*/
public function register_routes() {
\register_rest_route(
self::ROUTE_NAMESPACE,
self::ROUTE_PREFIX,
[
[
'methods' => 'POST',
'callback' => [ $this, 'set_site_kit_consent' ],
'permission_callback' => [ $this, 'check_capabilities' ],
'args' => [
'consent' => [
'required' => true,
'type' => 'bool',
'sanitize_callback' => 'rest_sanitize_boolean',
],
],
],
]
);
}
/**
* Sets whether the Site Kit configuration is permanently dismissed.
*
* @param WP_REST_Request $request The request object.
*
* @return WP_REST_Response|WP_Error The success or failure response.
*/
public function set_site_kit_consent( WP_REST_Request $request ) {
$consent = $request->get_param( 'consent' );
try {
$result = $this->site_kit_consent_repository->set_site_kit_consent( $consent );
} catch ( Exception $exception ) {
return new WP_Error(
'wpseo_set_site_kit_consent_error',
$exception->getMessage(),
(object) []
);
}
return new WP_REST_Response(
[
'success' => $result,
],
( $result ) ? 200 : 400
);
}
/**
* Checks if the current user has the required capabilities.
*
* @return bool
*/
public function check_capabilities() {
return $this->capability_helper->current_user_can( 'wpseo_manage_options' );
}
}
@@ -0,0 +1,276 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\User_Interface\Scores;
use Exception;
use WP_REST_Request;
use WP_REST_Response;
use WPSEO_Capability_Utils;
use Yoast\WP\SEO\Conditionals\No_Conditionals;
use Yoast\WP\SEO\Dashboard\Application\Score_Results\Abstract_Score_Results_Repository;
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\Taxonomies\Taxonomy;
use Yoast\WP\SEO\Dashboard\Infrastructure\Content_Types\Content_Types_Collector;
use Yoast\WP\SEO\Main;
use Yoast\WP\SEO\Repositories\Indexable_Repository;
use Yoast\WP\SEO\Routes\Route_Interface;
/**
* Abstract scores route.
*/
abstract class Abstract_Scores_Route implements Route_Interface {
use No_Conditionals;
/**
* The namespace of the rout.
*
* @var string
*/
public const ROUTE_NAMESPACE = Main::API_V1_NAMESPACE;
/**
* The prefix of the rout.
*
* @var string
*/
public const ROUTE_PREFIX = null;
/**
* The content types collector.
*
* @var Content_Types_Collector
*/
protected $content_types_collector;
/**
* The taxonomies repository.
*
* @var Taxonomies_Repository
*/
protected $taxonomies_repository;
/**
* The indexable repository.
*
* @var Indexable_Repository
*/
protected $indexable_repository;
/**
* The scores repository.
*
* @var Abstract_Score_Results_Repository
*/
protected $score_results_repository;
/**
* Sets the collectors.
*
* @required
*
* @param Content_Types_Collector $content_types_collector The content type collector.
*
* @return void
*/
public function set_collectors( Content_Types_Collector $content_types_collector ) {
$this->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 static 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(
self::ROUTE_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 );
},
],
'troubleshooting' => [
'required' => false,
'type' => 'bool',
'default' => null,
'sanitize_callback' => 'rest_sanitize_boolean',
],
],
],
]
);
}
/**
* 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 );
$results = $this->score_results_repository->get_score_results( $content_type, $taxonomy, $term_id, $request['troubleshooting'] );
} catch ( Exception $exception ) {
return new WP_REST_Response(
[
'error' => $exception->getMessage(),
],
$exception->getCode()
);
}
return new WP_REST_Response(
$results,
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 int|null 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' );
}
}
@@ -0,0 +1,27 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\User_Interface\Scores;
use Yoast\WP\SEO\Dashboard\Application\Score_Results\Readability_Score_Results\Readability_Score_Results_Repository;
/**
* Registers a route to get readability scores.
*/
class Readability_Scores_Route extends Abstract_Scores_Route {
/**
* The prefix of the route.
*
* @var string
*/
public const ROUTE_PREFIX = '/readability_scores';
/**
* Constructs the class.
*
* @param Readability_Score_Results_Repository $readability_score_results_repository The readability score results repository.
*/
public function __construct( Readability_Score_Results_Repository $readability_score_results_repository ) {
$this->score_results_repository = $readability_score_results_repository;
}
}
@@ -0,0 +1,27 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\User_Interface\Scores;
use Yoast\WP\SEO\Dashboard\Application\Score_Results\SEO_Score_Results\SEO_Score_Results_Repository;
/**
* Registers a route to get SEO scores.
*/
class SEO_Scores_Route extends Abstract_Scores_Route {
/**
* The prefix of the route.
*
* @var string
*/
public const ROUTE_PREFIX = '/seo_scores';
/**
* Constructs the class.
*
* @param SEO_Score_Results_Repository $seo_score_results_repository The SEO score results repository.
*/
public function __construct( SEO_Score_Results_Repository $seo_score_results_repository ) {
$this->score_results_repository = $seo_score_results_repository;
}
}
@@ -0,0 +1,109 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\User_Interface\Setup;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Conditionals\Google_Site_Kit_Feature_Conditional;
use Yoast\WP\SEO\Dashboard\Infrastructure\Integrations\Site_Kit;
use Yoast\WP\SEO\Helpers\Current_Page_Helper;
use Yoast\WP\SEO\Helpers\Redirect_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
/**
* Setup flow interceptor class.
*/
class Setup_Flow_Interceptor implements Integration_Interface {
/**
* The page name of the Site Kit Setup finished page.
*/
private const GOOGLE_SITE_KIT_SEARCH_CONSOLE_SETUP_FINISHED_PAGE = 'googlesitekit-splash';
private const GOOGLE_SITE_KIT_ANALYTICS_SETUP_FINISHED_PAGE = 'googlesitekit-dashboard';
/**
* The Site Kit configuration object.
*
* @var Site_Kit $site_kit_configuration
*/
private $site_kit_configuration;
/**
* Holds the Current_Page_Helper.
*
* @var Current_Page_Helper
*/
private $current_page_helper;
/**
* Holds the redirect helper.
*
* @var Redirect_Helper $redirect_helper
*/
private $redirect_helper;
/**
* The constructor.
*
* @param Current_Page_Helper $current_page_helper The current page helper.
* @param Redirect_Helper $redirect_helper The redirect helper to abstract away the actual redirecting.
* @param Site_Kit $site_kit_configuration The Site Kit configuration object.
*/
public function __construct( Current_Page_Helper $current_page_helper, Redirect_Helper $redirect_helper, Site_Kit $site_kit_configuration ) {
$this->current_page_helper = $current_page_helper;
$this->redirect_helper = $redirect_helper;
$this->site_kit_configuration = $site_kit_configuration;
}
/**
* Registers our redirect back to our dashboard all the way at the end of the admin init to make sure everything from the Site Kit callback can be finished.
*
* @return void
*/
public function register_hooks() {
\add_action( 'admin_init', [ $this, 'intercept_site_kit_setup_flow' ], 999 );
}
/**
* The conditions for this integration to load.
*
* @return string[] The conditionals.
*/
public static function get_conditionals() {
return [ Google_Site_Kit_Feature_Conditional::class, Admin_Conditional::class ];
}
/**
* Checks if we should intercept the final page from the Site Kit flow.
*
* @return void
*/
public function intercept_site_kit_setup_flow() {
if ( \get_transient( Setup_Url_Interceptor::SITE_KIT_SETUP_TRANSIENT ) === '1' && $this->is_site_kit_setup_completed_page() ) {
\delete_transient( Setup_Url_Interceptor::SITE_KIT_SETUP_TRANSIENT );
$this->redirect_helper->do_safe_redirect( \self_admin_url( 'admin.php?page=wpseo_dashboard&redirected_from_site_kit' ), 302, 'Yoast SEO' );
}
}
/**
* Checks if we are on the site kit setup completed page.
*
* @return bool
*/
private function is_site_kit_setup_completed_page(): bool {
$current_page = $this->current_page_helper->get_current_yoast_seo_page();
$on_search_console_setup_page = $current_page === self::GOOGLE_SITE_KIT_SEARCH_CONSOLE_SETUP_FINISHED_PAGE;
$on_analytics_setup_page = $current_page === self::GOOGLE_SITE_KIT_ANALYTICS_SETUP_FINISHED_PAGE;
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information.
$authentication_success_notification = isset( $_GET['notification'] ) && \sanitize_text_field( \wp_unslash( $_GET['notification'] ) ) === 'authentication_success';
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information.
$analytics_4_slug = isset( $_GET['slug'] ) && \sanitize_text_field( \wp_unslash( $_GET['slug'] ) ) === 'analytics-4';
/**
* This checks two scenarios
* 1. The user only wants Search Console. In this case just checking if you are on the thank-you page from Site Kit is enough.
* 2. The user also wants analytics. So we need to check another page and also check if the analytics 4 connection is finalized.
*/
return ( $on_search_console_setup_page && $authentication_success_notification ) || ( $on_analytics_setup_page && $authentication_success_notification && $analytics_4_slug && $this->site_kit_configuration->is_ga_connected() );
}
}
@@ -0,0 +1,132 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\User_Interface\Setup;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Conditionals\Google_Site_Kit_Feature_Conditional;
use Yoast\WP\SEO\Dashboard\Infrastructure\Integrations\Site_Kit;
use Yoast\WP\SEO\Helpers\Current_Page_Helper;
use Yoast\WP\SEO\Helpers\Redirect_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
/**
* Setup url Interceptor class.
*/
class Setup_Url_Interceptor implements Integration_Interface {
/**
* The page url where this check lives.
*/
public const PAGE = 'wpseo_page_site_kit_set_up';
/**
* The name of the transient.
*/
public const SITE_KIT_SETUP_TRANSIENT = 'wpseo_site_kit_set_up_transient';
/**
* Holds the Current_Page_Helper.
*
* @var Current_Page_Helper $current_page_helper
*/
private $current_page_helper;
/**
* Holds the redirect helper.
*
* @var Redirect_Helper $redirect_helper
*/
private $redirect_helper;
/**
* The Site Kit configuration object.
*
* @var Site_Kit $site_kit_configuration
*/
private $site_kit_configuration;
/**
* The constructor.
*
* @param Current_Page_Helper $current_page_helper The current page helper.
* @param Site_Kit $site_kit_configuration The Site Kit configuration object.
* @param Redirect_Helper $redirect_helper The redirect helper to abstract away the actual redirecting.
*/
public function __construct( Current_Page_Helper $current_page_helper, Site_Kit $site_kit_configuration, Redirect_Helper $redirect_helper ) {
$this->current_page_helper = $current_page_helper;
$this->redirect_helper = $redirect_helper;
$this->site_kit_configuration = $site_kit_configuration;
}
/**
* The conditions for this integration to load.
*
* @return string[] The conditionals.
*/
public static function get_conditionals() {
return [ Google_Site_Kit_Feature_Conditional::class, Admin_Conditional::class ];
}
/**
* Registers the interceptor code to the admin_init function.
*
* @return void
*/
public function register_hooks() {
\add_filter( 'admin_menu', [ $this, 'add_redirect_page' ] );
\add_action( 'admin_init', [ $this, 'intercept_site_kit_setup_url_redirect' ], 1 );
}
/**
* Adds a dummy page.
*
* We need to register this in between page.
*
* @param array<array<string>> $pages The pages.
*
* @return array<array<string>> The pages.
*/
public function add_redirect_page( $pages ) {
\add_submenu_page(
'',
'',
'',
'wpseo_manage_options',
self::PAGE
);
return $pages;
}
/**
* Checks if we are trying to reach a site kit setup url and sets the needed transient in between.
*
* @return void
*/
public function intercept_site_kit_setup_url_redirect() {
$allowed_setup_links = [
$this->site_kit_configuration->get_install_url(),
$this->site_kit_configuration->get_activate_url(),
$this->site_kit_configuration->get_setup_url(),
$this->site_kit_configuration->get_update_url(),
];
// Are we on the in-between page?
if ( $this->current_page_helper->get_current_yoast_seo_page() === self::PAGE ) {
// Check if parameter is there and is valid.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information.
if ( isset( $_GET['redirect_setup_url'] ) && \in_array( \wp_unslash( $_GET['redirect_setup_url'] ), $allowed_setup_links, true ) ) {
// We overwrite the transient for each time this redirect is hit to keep refreshing the time.
\set_transient( self::SITE_KIT_SETUP_TRANSIENT, 1, ( \MINUTE_IN_SECONDS * 15 ) );
// phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: Only allowed pre verified links can end up here.
$redirect_url = \wp_unslash( $_GET['redirect_setup_url'] );
$this->redirect_helper->do_safe_redirect( $redirect_url, 302, 'Yoast SEO' );
}
else {
$this->redirect_helper->do_safe_redirect( \self_admin_url( 'admin.php?page=wpseo_dashboard' ), 302, 'Yoast SEO' );
}
}
}
}
@@ -0,0 +1,309 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\User_Interface\Time_Based_SEO_Metrics;
use DateTime;
use DateTimeZone;
use Exception;
use WP_REST_Request;
use WP_REST_Response;
use Yoast\WP\SEO\Conditionals\Google_Site_Kit_Feature_Conditional;
use Yoast\WP\SEO\Conditionals\Third_Party\Site_Kit_Conditional;
use Yoast\WP\SEO\Dashboard\Application\Search_Rankings\Search_Ranking_Compare_Repository;
use Yoast\WP\SEO\Dashboard\Application\Search_Rankings\Top_Page_Repository;
use Yoast\WP\SEO\Dashboard\Application\Search_Rankings\Top_Query_Repository;
use Yoast\WP\SEO\Dashboard\Application\Traffic\Organic_Sessions_Compare_Repository;
use Yoast\WP\SEO\Dashboard\Application\Traffic\Organic_Sessions_Daily_Repository;
use Yoast\WP\SEO\Dashboard\Domain\Data_Provider\Parameters;
use Yoast\WP\SEO\Dashboard\Domain\Time_Based_SEO_Metrics\Repository_Not_Found_Exception;
use Yoast\WP\SEO\Dashboard\Infrastructure\Analytics_4\Analytics_4_Parameters;
use Yoast\WP\SEO\Dashboard\Infrastructure\Search_Console\Search_Console_Parameters;
use Yoast\WP\SEO\Helpers\Capability_Helper;
use Yoast\WP\SEO\Main;
use Yoast\WP\SEO\Routes\Route_Interface;
/**
* Abstract scores route.
*/
final class Time_Based_SEO_Metrics_Route implements Route_Interface {
/**
* The namespace of the route.
*
* @var string
*/
public const ROUTE_NAMESPACE = Main::API_V1_NAMESPACE;
/**
* The prefix of the route.
*
* @var string
*/
public const ROUTE_NAME = '/time_based_seo_metrics';
/**
* The data provider for page based search rankings.
*
* @var Top_Page_Repository
*/
private $top_page_repository;
/**
* The data provider for query based search rankings.
*
* @var Top_Query_Repository
*/
private $top_query_repository;
/**
* The data provider for comparison organic session traffic.
*
* @var Organic_Sessions_Compare_Repository
*/
private $organic_sessions_compare_repository;
/**
* The data provider for daily organic session traffic.
*
* @var Organic_Sessions_Daily_Repository
*/
private $organic_sessions_daily_repository;
/**
* The data provider for searching ranking comparison.
*
* @var Search_Ranking_Compare_Repository
*/
private $search_ranking_compare_repository;
/**
* Holds the capability helper instance.
*
* @var Capability_Helper
*/
private $capability_helper;
/**
* Returns the needed conditionals.
*
* @return array<string> The conditionals that must be met to load this.
*/
public static function get_conditionals(): array {
return [ Google_Site_Kit_Feature_Conditional::class, Site_Kit_Conditional::class ];
}
/**
* The constructor.
*
* @param Top_Page_Repository $top_page_repository The data provider for page based search rankings.
* @param Top_Query_Repository $top_query_repository The data provider for query based search rankings.
* @param Organic_Sessions_Compare_Repository $organic_sessions_compare_repository The data provider for comparison organic session traffic.
* @param Organic_Sessions_Daily_Repository $organic_sessions_daily_repository The data provider for daily organic session traffic.
* @param Search_Ranking_Compare_Repository $search_ranking_compare_repository The data provider for searching ranking comparison.
* @param Capability_Helper $capability_helper The capability helper.
*/
public function __construct(
Top_Page_Repository $top_page_repository,
Top_Query_Repository $top_query_repository,
Organic_Sessions_Compare_Repository $organic_sessions_compare_repository,
Organic_Sessions_Daily_Repository $organic_sessions_daily_repository,
Search_Ranking_Compare_Repository $search_ranking_compare_repository,
Capability_Helper $capability_helper
) {
$this->top_page_repository = $top_page_repository;
$this->top_query_repository = $top_query_repository;
$this->organic_sessions_compare_repository = $organic_sessions_compare_repository;
$this->organic_sessions_daily_repository = $organic_sessions_daily_repository;
$this->search_ranking_compare_repository = $search_ranking_compare_repository;
$this->capability_helper = $capability_helper;
}
/**
* Registers routes for scores.
*
* @return void
*/
public function register_routes() {
\register_rest_route(
self::ROUTE_NAMESPACE,
self::ROUTE_NAME,
[
[
'methods' => 'GET',
'callback' => [ $this, 'get_time_based_seo_metrics' ],
'permission_callback' => [ $this, 'permission_manage_options' ],
'args' => [
'limit' => [
'type' => 'int',
'sanitize_callback' => 'absint',
'default' => 5,
],
'options' => [
'type' => 'object',
'required' => true,
'properties' => [
'widget' => [
'type' => 'string',
'required' => true,
'sanitize_callback' => 'sanitize_text_field',
],
],
],
],
],
]
);
}
/**
* Gets the time based SEO metrics.
*
* @param WP_REST_Request $request The request object.
*
* @return WP_REST_Response The success or failure response.
*
* @throws Repository_Not_Found_Exception When the given widget name is not implemented yet.
*/
public function get_time_based_seo_metrics( WP_REST_Request $request ): WP_REST_Response {
try {
$widget_name = $request->get_param( 'options' )['widget'];
switch ( $widget_name ) {
case 'query':
$request_parameters = new Search_Console_Parameters();
$request_parameters = $this->set_date_range_parameters( $request_parameters );
$request_parameters->set_limit( $request->get_param( 'limit' ) );
$request_parameters->set_dimensions( [ 'query' ] );
$time_based_seo_metrics_container = $this->top_query_repository->get_data( $request_parameters );
break;
case 'page':
$request_parameters = new Search_Console_Parameters();
$request_parameters = $this->set_date_range_parameters( $request_parameters );
$request_parameters->set_limit( $request->get_param( 'limit' ) );
$request_parameters->set_dimensions( [ 'page' ] );
$time_based_seo_metrics_container = $this->top_page_repository->get_data( $request_parameters );
break;
case 'organicSessionsDaily':
$request_parameters = new Analytics_4_Parameters();
$request_parameters = $this->set_date_range_parameters( $request_parameters );
$request_parameters->set_dimensions( [ 'date' ] );
$request_parameters->set_metrics( [ 'sessions' ] );
$request_parameters->set_dimension_filters( [ 'sessionDefaultChannelGrouping' => [ 'Organic Search' ] ] );
$request_parameters->set_order_by( 'dimension', 'date' );
$time_based_seo_metrics_container = $this->organic_sessions_daily_repository->get_data( $request_parameters );
break;
case 'organicSessionsCompare':
$request_parameters = new Analytics_4_Parameters();
$request_parameters = $this->set_date_range_parameters( $request_parameters );
$request_parameters = $this->set_comparison_date_range_parameters( $request_parameters );
$request_parameters->set_metrics( [ 'sessions' ] );
$request_parameters->set_dimension_filters( [ 'sessionDefaultChannelGrouping' => [ 'Organic Search' ] ] );
$time_based_seo_metrics_container = $this->organic_sessions_compare_repository->get_data( $request_parameters );
break;
case 'searchRankingCompare':
$request_parameters = new Search_Console_Parameters();
$request_parameters = $this->set_date_range_parameters( $request_parameters );
$request_parameters = $this->set_comparison_date_range_parameters( $request_parameters );
$request_parameters->set_dimensions( [ 'date' ] );
$time_based_seo_metrics_container = $this->search_ranking_compare_repository->get_data( $request_parameters );
break;
default:
throw new Repository_Not_Found_Exception();
}
} catch ( Exception $exception ) {
return new WP_REST_Response(
[
'error' => $exception->getMessage(),
],
$exception->getCode()
);
}
return new WP_REST_Response(
$time_based_seo_metrics_container->to_array(),
200
);
}
/**
* Sets date range parameters.
*
* @param Parameters $request_parameters The request parameters.
*
* @return Parameters The request parameters with configured date range.
*/
public function set_date_range_parameters( Parameters $request_parameters ): Parameters {
$date = $this->get_base_date();
$date->modify( '-28 days' );
$start_date = $date->format( 'Y-m-d' );
$date = $this->get_base_date();
$date->modify( '-1 days' );
$end_date = $date->format( 'Y-m-d' );
$request_parameters->set_start_date( $start_date );
$request_parameters->set_end_date( $end_date );
return $request_parameters;
}
/**
* Sets comparison date range parameters.
*
* @param Parameters $request_parameters The request parameters.
*
* @return Parameters The request parameters with configured comparison date range.
*/
public function set_comparison_date_range_parameters( Parameters $request_parameters ): Parameters {
$date = $this->get_base_date();
$date->modify( '-29 days' );
$compare_end_date = $date->format( 'Y-m-d' );
$date->modify( '-27 days' );
$compare_start_date = $date->format( 'Y-m-d' );
$request_parameters->set_compare_start_date( $compare_start_date );
$request_parameters->set_compare_end_date( $compare_end_date );
return $request_parameters;
}
/**
* Gets the base date.
*
* @return DateTime The base date.
*/
private function get_base_date() {
/**
* Filter: 'wpseo_custom_site_kit_base_date' - Allow the base date for Site Kit requests to be dynamically set.
*
* @param string $base_date The custom base date for Site Kit requests, defaults to 'now'.
*/
$base_date = \apply_filters( 'wpseo_custom_site_kit_base_date', 'now' );
try {
return new DateTime( $base_date, new DateTimeZone( 'UTC' ) );
} catch ( Exception $e ) {
return new DateTime( 'now', new DateTimeZone( 'UTC' ) );
}
}
/**
* Permission callback.
*
* @return bool True when user has the 'wpseo_manage_options' capability.
*/
public function permission_manage_options() {
return $this->capability_helper->current_user_can( 'wpseo_manage_options' );
}
}
@@ -0,0 +1,186 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Dashboard\User_Interface\Tracking;
use Exception;
use WP_Error;
use WP_REST_Request;
use WP_REST_Response;
use Yoast\WP\SEO\Conditionals\Google_Site_Kit_Feature_Conditional;
use Yoast\WP\SEO\Dashboard\Infrastructure\Tracking\Setup_Steps_Tracking_Repository_Interface;
use Yoast\WP\SEO\Helpers\Capability_Helper;
use Yoast\WP\SEO\Main;
use Yoast\WP\SEO\Routes\Route_Interface;
/**
* Registers a route to keep track of the Site Kit usage.
*
* @makePublic
*
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
*/
class Setup_Steps_Tracking_Route implements Route_Interface {
/**
* The namespace for this route.
*
* @var string
*/
public const ROUTE_NAMESPACE = Main::API_V1_NAMESPACE;
/**
* The prefix for this route.
*
* @var string
*/
public const ROUTE_PREFIX = '/setup_steps_tracking';
/**
* Holds the repository instance.
*
* @var Setup_Steps_Tracking_Repository_Interface
*/
private $setup_steps_tracking_repository;
/**
* Holds the capability helper instance.
*
* @var Capability_Helper
*/
private $capability_helper;
/**
* Returns the needed conditionals.
*
* @return array<string> The conditionals that must be met to load this.
*/
public static function get_conditionals(): array {
return [ Google_Site_Kit_Feature_Conditional::class ];
}
/**
* Constructs the class.
*
* @param Setup_Steps_Tracking_Repository_Interface $setup_steps_tracking_repository The repository.
* @param Capability_Helper $capability_helper The capability helper.
*/
public function __construct(
Setup_Steps_Tracking_Repository_Interface $setup_steps_tracking_repository,
Capability_Helper $capability_helper
) {
$this->setup_steps_tracking_repository = $setup_steps_tracking_repository;
$this->capability_helper = $capability_helper;
}
/**
* Registers routes with WordPress.
*
* @return void
*/
public function register_routes() {
\register_rest_route(
self::ROUTE_NAMESPACE,
self::ROUTE_PREFIX,
[
[
'methods' => 'POST',
'callback' => [ $this, 'track_setup_steps' ],
'permission_callback' => [ $this, 'check_capabilities' ],
'args' => [
'setup_widget_loaded' => [
'required' => false,
'type' => 'string',
'enum' => [ 'yes', 'no' ],
],
'first_interaction_stage' => [
'required' => false,
'type' => 'string',
'enum' => [ 'install', 'activate', 'setup', 'grantConsent', 'successfullyConnected' ],
],
'last_interaction_stage' => [
'required' => false,
'type' => 'string',
'enum' => [ 'install', 'activate', 'setup', 'grantConsent', 'successfullyConnected' ],
],
'setup_widget_temporarily_dismissed' => [
'required' => false,
'type' => 'string',
'enum' => [ 'yes', 'no' ],
],
'setup_widget_permanently_dismissed' => [
'required' => false,
'type' => 'string',
'enum' => [ 'yes', 'no' ],
],
],
],
]
);
}
/**
* Stores tracking information.
*
* @param WP_REST_Request $request The request object.
*
* @return WP_REST_Response|WP_Error The success or failure response.
*/
public function track_setup_steps( WP_REST_Request $request ) {
$data = [
'setup_widget_loaded' => $request->get_param( 'setupWidgetLoaded' ),
'first_interaction_stage' => $request->get_param( 'firstInteractionStage' ),
'last_interaction_stage' => $request->get_param( 'lastInteractionStage' ),
'setup_widget_temporarily_dismissed' => $request->get_param( 'setupWidgetTemporarilyDismissed' ),
'setup_widget_permanently_dismissed' => $request->get_param( 'setupWidgetPermanentlyDismissed' ),
];
// Filter out null values from the data array.
$data = \array_filter(
$data,
static function ( $value ) {
return $value !== null;
}
);
// Check if all values are null then return an error that no valid params were passed.
if ( empty( $data ) ) {
return new WP_Error(
'wpseo_set_site_kit_usage_tracking',
\__( 'No valid parameters were passed.', 'wordpress-seo' ),
[ 'status' => 400 ]
);
}
$result = true;
foreach ( $data as $key => $value ) {
try {
$result = $this->setup_steps_tracking_repository->set_setup_steps_tracking_element( $key, $value );
} catch ( Exception $exception ) {
return new WP_Error(
'wpseo_set_site_kit_usage_tracking',
$exception->getMessage(),
(object) []
);
}
if ( ! $result ) {
break;
}
}
return new WP_REST_Response(
[
'success' => $result,
],
( $result ) ? 200 : 400
);
}
/**
* Checks if the current user has the required capabilities.
*
* @return bool
*/
public function check_capabilities() {
return $this->capability_helper->current_user_can( 'wpseo_manage_options' );
}
}