Merged in feature/MAW-855-import-code-into-aws (pull request #2)

code import from pantheon

* code import from pantheon
This commit is contained in:
Tony Volpe
2023-12-04 23:08:14 +00:00
parent 8c9b1312bc
commit 8f4b5efda6
4766 changed files with 185592 additions and 239967 deletions

View File

@@ -0,0 +1,401 @@
<?php
namespace Yoast\WP\SEO\Premium\Actions;
use RuntimeException;
use WP_User;
use WPSEO_Addon_Manager;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\User_Helper;
use Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Forbidden_Exception;
use Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Unauthorized_Exception;
use Yoast\WP\SEO\Premium\Helpers\AI_Generator_Helper;
/**
* Handles the actual requests to our API endpoints.
*/
class AI_Generator_Action {
/**
* The AI_Generator helper.
*
* @var AI_Generator_Helper
*/
protected $ai_generator_helper;
/**
* The Options helper.
*
* @var Options_Helper
*/
protected $options_helper;
/**
* The User helper.
*
* @var User_Helper
*/
protected $user_helper;
/**
* The add-on manager.
*
* @var WPSEO_Addon_Manager
*/
private $addon_manager;
/**
* AI_Generator_Action constructor.
*
* @param AI_Generator_Helper $ai_generator_helper The AI_Generator helper.
* @param Options_Helper $options_helper The Options helper.
* @param User_Helper $user_helper The User helper.
* @param WPSEO_Addon_Manager $addon_manager The add-on manager.
*/
public function __construct(
AI_Generator_Helper $ai_generator_helper,
Options_Helper $options_helper,
User_Helper $user_helper,
WPSEO_Addon_Manager $addon_manager
) {
$this->ai_generator_helper = $ai_generator_helper;
$this->options_helper = $options_helper;
$this->user_helper = $user_helper;
$this->addon_manager = $addon_manager;
}
/**
* Requests a new set of JWT tokens.
*
* Requests a new JWT access and refresh token for a user from the Yoast AI Service and stores it in the database
* under usermeta. The storing of the token happens in a HTTP callback that is triggered by this request.
*
* @param \WP_User $user The WP user.
*
* @throws \Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Bad_Request_Exception Bad_Request_Exception.
* @throws \Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Forbidden_Exception Forbidden_Exception.
* @throws \Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Internal_Server_Error_Exception Internal_Server_Error_Exception.
* @throws \Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Not_Found_Exception Not_Found_Exception.
* @throws \Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Payment_Required_Exception Payment_Required_Exception.
* @throws \Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Request_Timeout_Exception Request_Timeout_Exception.
* @throws \Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Service_Unavailable_Exception Service_Unavailable_Exception.
* @throws \Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Too_Many_Requests_Exception Too_Many_Requests_Exception.
* @throws \Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Unauthorized_Exception Unauthorized_Exception.
*
* @return void
*/
public function token_request( WP_User $user ): void {
// Ensure the user has given consent.
if ( $this->user_helper->get_meta( $user->ID, '_yoast_wpseo_ai_consent', true ) !== '1' ) {
throw $this->handle_consent_revoked( $user->ID );
}
// Generate a verification code and store it in the database.
$code_verifier = $this->ai_generator_helper->generate_code_verifier( $user );
$this->ai_generator_helper->set_code_verifier( $user->ID, $code_verifier );
$request_body = [
'service' => 'openai',
'code_challenge' => \hash( 'sha256', $code_verifier ),
'license_site_url' => $this->ai_generator_helper->get_license_url(),
'user_id' => (string) $user->ID,
'callback_url' => $this->ai_generator_helper->get_callback_url(),
'refresh_callback_url' => $this->ai_generator_helper->get_refresh_callback_url(),
];
$this->ai_generator_helper->request( '/token/request', $request_body );
// The callback saves the metadata. Because that is in another session, we need to delete the current cache here. Or we may get the old token.
\wp_cache_delete( $user->ID, 'user_meta' );
}
/**
* Refreshes the JWT access token.
*
* Refreshes a stored JWT access token for a user with the Yoast AI Service and stores it in the database under
* usermeta. The storing of the token happens in a HTTP callback that is triggered by this request.
*
* @param \WP_User $user The WP user.
*
* @throws \Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Bad_Request_Exception Bad_Request_Exception.
* @throws \Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Forbidden_Exception Forbidden_Exception.
* @throws \Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Internal_Server_Error_Exception Internal_Server_Error_Exception.
* @throws \Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Not_Found_Exception Not_Found_Exception.
* @throws \Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Payment_Required_Exception Payment_Required_Exception.
* @throws \Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Request_Timeout_Exception Request_Timeout_Exception.
* @throws \Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Service_Unavailable_Exception Service_Unavailable_Exception.
* @throws \Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Too_Many_Requests_Exception Too_Many_Requests_Exception.
* @throws \Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Unauthorized_Exception Unauthorized_Exception.
* @throws \RuntimeException Unable to retrieve the refresh token.
*
* @return void
*/
public function token_refresh( WP_User $user ): void {
$refresh_jwt = $this->ai_generator_helper->get_refresh_token( $user->ID );
// Generate a verification code and store it in the database.
$code_verifier = $this->ai_generator_helper->generate_code_verifier( $user );
$this->ai_generator_helper->set_code_verifier( $user->ID, $code_verifier );
$request_body = [
'code_challenge' => \hash( 'sha256', $code_verifier ),
];
$request_headers = [
'Authorization' => "Bearer $refresh_jwt",
];
$this->ai_generator_helper->request( '/token/refresh', $request_body, $request_headers );
// The callback saves the metadata. Because that is in another session, we need to delete the current cache here. Or we may get the old token.
\wp_cache_delete( $user->ID, 'user_meta' );
}
/**
* Callback function that will be invoked by our API.
*
* @param string $access_jwt The access JWT.
* @param string $refresh_jwt The refresh JWT.
* @param string $code_challenge The verification code.
* @param int $user_id The user ID.
*
* @throws \Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Unauthorized_Exception Unauthorized_Exception.
*
* @return string The code verifier.
*/
public function callback(
string $access_jwt,
string $refresh_jwt,
string $code_challenge,
int $user_id
): string {
try {
$code_verifier = $this->ai_generator_helper->get_code_verifier( $user_id );
} catch ( RuntimeException $exception ) {
throw new Unauthorized_Exception( 'Unauthorized' );
}
if ( $code_challenge !== \hash( 'sha256', $code_verifier ) ) {
throw new Unauthorized_Exception( 'Unauthorized' );
}
$this->user_helper->update_meta( $user_id, '_yoast_wpseo_ai_generator_access_jwt', $access_jwt );
$this->user_helper->update_meta( $user_id, '_yoast_wpseo_ai_generator_refresh_jwt', $refresh_jwt );
$this->ai_generator_helper->delete_code_verifier( $user_id );
return $code_verifier;
}
// phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber -- PHPCS doesn't take into account exceptions thrown in called methods.
/**
* Action used to generate suggestions through AI.
*
* @param \WP_User $user The WP user.
* @param string $suggestion_type The type of the requested suggestion.
* @param string $prompt_content The excerpt taken from the post.
* @param string $focus_keyphrase The focus keyphrase associated to the post.
* @param string $language The language of the post.
* @param string $platform The platform the post is intended for.
* @param bool $retry_on_unauthorized Whether to retry when unauthorized (mechanism to retry once).
*
* @throws \Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Bad_Request_Exception Bad_Request_Exception.
* @throws \Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Forbidden_Exception Forbidden_Exception.
* @throws \Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Internal_Server_Error_Exception Internal_Server_Error_Exception.
* @throws \Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Not_Found_Exception Not_Found_Exception.
* @throws \Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Payment_Required_Exception Payment_Required_Exception.
* @throws \Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Request_Timeout_Exception Request_Timeout_Exception.
* @throws \Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Service_Unavailable_Exception Service_Unavailable_Exception.
* @throws \Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Too_Many_Requests_Exception Too_Many_Requests_Exception.
* @throws \Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Unauthorized_Exception Unauthorized_Exception.
* @throws \RuntimeException Unable to retrieve the access token.
*
* @return array The suggestions.
*/
public function get_suggestions(
WP_User $user,
string $suggestion_type,
string $prompt_content,
string $focus_keyphrase,
string $language,
string $platform,
bool $retry_on_unauthorized = true
): array {
$token = $this->get_or_request_access_token( $user );
$request_body = [
'service' => 'openai',
'user_id' => (string) $user->ID,
'subject' => [
'content' => $prompt_content,
'focus_keyphrase' => $focus_keyphrase,
'language' => $language,
'platform' => $platform,
],
];
$request_headers = [
'Authorization' => "Bearer $token",
];
try {
$response = $this->ai_generator_helper->request( "/openai/suggestions/$suggestion_type", $request_body, $request_headers );
} catch ( Unauthorized_Exception $exception ) {
// Delete the stored JWT tokens, as they appear to be no longer valid.
$this->user_helper->delete_meta( $user->ID, '_yoast_wpseo_ai_generator_access_jwt' );
$this->user_helper->delete_meta( $user->ID, '_yoast_wpseo_ai_generator_refresh_jwt' );
if ( ! $retry_on_unauthorized ) {
throw $exception;
}
// Try again once more by fetching a new set of tokens and trying the suggestions endpoint again.
return $this->get_suggestions( $user, $suggestion_type, $prompt_content, $focus_keyphrase, $language, $platform, false );
} catch ( Forbidden_Exception $exception ) {
// Follow the API in the consent being revoked (Use case: user sent an e-mail to revoke?).
throw $this->handle_consent_revoked( $user->ID, $exception->getCode() );
}
return $this->ai_generator_helper->build_suggestions_array( $response );
}
// phpcs:enable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber
/**
* Stores the consent given or revoked by the user.
*
* @param int $user_id The user ID.
* @param bool $consent Whether the consent has been given.
*
* @throws \Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Bad_Request_Exception Bad_Request_Exception.
* @throws \Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Internal_Server_Error_Exception Internal_Server_Error_Exception.
* @throws \Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Not_Found_Exception Not_Found_Exception.
* @throws \Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Payment_Required_Exception Payment_Required_Exception.
* @throws \Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Request_Timeout_Exception Request_Timeout_Exception.
* @throws \Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Service_Unavailable_Exception Service_Unavailable_Exception.
* @throws \Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Too_Many_Requests_Exception Too_Many_Requests_Exception.
* @throws \RuntimeException Unable to retrieve the access token.
*
* @return void
*/
public function consent( int $user_id, bool $consent ): void {
if ( $consent ) {
// Store the consent at user level.
$this->user_helper->update_meta( $user_id, '_yoast_wpseo_ai_consent', true );
}
else {
$this->token_invalidate( $user_id );
// Delete the consent at user level.
$this->user_helper->delete_meta( $user_id, '_yoast_wpseo_ai_consent' );
}
}
/**
* Busts the subscription cache.
*
* @return void
*/
public function bust_subscription_cache(): void {
$this->addon_manager->remove_site_information_transients();
}
// phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber -- PHPCS doesn't take into account exceptions thrown in called methods.
/**
* Retrieves the access token.
*
* @param WP_User $user The WP user.
*
* @throws \Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Bad_Request_Exception Bad_Request_Exception.
* @throws \Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Forbidden_Exception Forbidden_Exception.
* @throws \Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Internal_Server_Error_Exception Internal_Server_Error_Exception.
* @throws \Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Not_Found_Exception Not_Found_Exception.
* @throws \Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Payment_Required_Exception Payment_Required_Exception.
* @throws \Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Request_Timeout_Exception Request_Timeout_Exception.
* @throws \Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Service_Unavailable_Exception Service_Unavailable_Exception.
* @throws \Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Too_Many_Requests_Exception Too_Many_Requests_Exception.
* @throws \Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Unauthorized_Exception Unauthorized_Exception.
* @throws \RuntimeException Unable to retrieve the access or refresh token.
*
* @return string The access token.
*/
private function get_or_request_access_token( WP_User $user ): string {
$access_jwt = $this->user_helper->get_meta( $user->ID, '_yoast_wpseo_ai_generator_access_jwt', true );
if ( ! \is_string( $access_jwt ) || $access_jwt === '' ) {
$this->token_request( $user );
$access_jwt = $this->ai_generator_helper->get_access_token( $user->ID );
}
elseif ( $this->ai_generator_helper->has_token_expired( $access_jwt ) ) {
try {
$this->token_refresh( $user );
} catch ( Unauthorized_Exception $exception ) {
$this->token_request( $user );
} catch ( Forbidden_Exception $exception ) {
// Follow the API in the consent being revoked (Use case: user sent an e-mail to revoke?).
throw $this->handle_consent_revoked( $user->ID, $exception->getCode() );
}
$access_jwt = $this->ai_generator_helper->get_access_token( $user->ID );
}
return $access_jwt;
}
// phpcs:enable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber
/**
* Invalidates the access token.
*
* @param string $user_id The user ID.
*
* @throws \Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Bad_Request_Exception Bad_Request_Exception.
* @throws \Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Internal_Server_Error_Exception Internal_Server_Error_Exception.
* @throws \Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Not_Found_Exception Not_Found_Exception.
* @throws \Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Payment_Required_Exception Payment_Required_Exception.
* @throws \Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Request_Timeout_Exception Request_Timeout_Exception.
* @throws \Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Service_Unavailable_Exception Service_Unavailable_Exception.
* @throws \Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Too_Many_Requests_Exception Too_Many_Requests_Exception.
* @throws \RuntimeException Unable to retrieve the access token.
*
* @return void
*/
private function token_invalidate( string $user_id ): void {
try {
$access_jwt = $this->ai_generator_helper->get_access_token( $user_id );
} catch ( RuntimeException $e ) {
$access_jwt = '';
}
$request_body = [
'user_id' => (string) $user_id,
];
$request_headers = [
'Authorization' => "Bearer $access_jwt",
];
try {
$this->ai_generator_helper->request( '/token/invalidate', $request_body, $request_headers );
} catch ( Unauthorized_Exception | Forbidden_Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch -- Reason: Ignored on purpose.
// We do nothing in this case, we trust nonce verification and try to remove the user data anyway.
// I.e. we fallthrough to the same logic as if we got a 200 OK.
}
// Delete the stored JWT tokens.
$this->user_helper->delete_meta( $user_id, '_yoast_wpseo_ai_generator_access_jwt' );
$this->user_helper->delete_meta( $user_id, '_yoast_wpseo_ai_generator_refresh_jwt' );
}
/**
* Handles consent revoked.
*
* By deleting the consent user metadata from the database.
* And then throwing a Forbidden_Exception.
*
* @param int $user_id The user ID.
* @param int $status_code The status code. Defaults to 403.
*
* @return Forbidden_Exception The Forbidden_Exception.
*/
private function handle_consent_revoked( int $user_id, int $status_code = 403 ): Forbidden_Exception {
$this->user_helper->delete_meta( $user_id, '_yoast_wpseo_ai_consent' );
return new Forbidden_Exception( 'CONSENT_REVOKED', $status_code );
}
}

View File

@@ -4,16 +4,23 @@ namespace Yoast\WP\SEO\Premium\Actions;
use WP_Query;
use WPSEO_Premium_Prominent_Words_Support;
use Yoast\WP\SEO\Helpers\Prominent_Words_Helper;
use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Premium\Helpers\Prominent_Words_Helper;
use Yoast\WP\SEO\Premium\Repositories\Prominent_Words_Repository;
use Yoast\WP\SEO\Repositories\Indexable_Repository;
use Yoast\WP\SEO\Repositories\Prominent_Words_Repository;
use Yoast\WP\SEO\Repositories\SEO_Links_Repository;
/**
* Handles the actual requests to the prominent words endpoints.
*/
class Link_Suggestions_Action {
/**
* The amount of indexables to retrieve in one go
* when generating internal linking suggestions.
*/
const BATCH_SIZE = 1000;
/**
* The repository to retrieve prominent words from.
*
@@ -28,6 +35,13 @@ class Link_Suggestions_Action {
*/
protected $indexable_repository;
/**
* The repository to retrieve links from.
*
* @var SEO_Links_Repository
*/
protected $links_repository;
/**
* Contains helper functions for calculating with and comparing prominent words.
*
@@ -49,30 +63,36 @@ class Link_Suggestions_Action {
* @param Indexable_Repository $indexable_repository The repository to retrieve indexables from.
* @param Prominent_Words_Helper $prominent_words_helper Class with helper methods for prominent words.
* @param WPSEO_Premium_Prominent_Words_Support $prominent_words_support The prominent words support class.
* @param SEO_Links_Repository $links_repository The repository to retrieve links from.
*/
public function __construct(
Prominent_Words_Repository $prominent_words_repository,
Indexable_Repository $indexable_repository,
Prominent_Words_Helper $prominent_words_helper,
WPSEO_Premium_Prominent_Words_Support $prominent_words_support
WPSEO_Premium_Prominent_Words_Support $prominent_words_support,
SEO_Links_Repository $links_repository
) {
$this->prominent_words_repository = $prominent_words_repository;
$this->indexable_repository = $indexable_repository;
$this->prominent_words_helper = $prominent_words_helper;
$this->prominent_words_support = $prominent_words_support;
$this->links_repository = $links_repository;
}
/**
* Suggests a list of links, based on the given array of prominent words.
*
* @param array $words_from_request The prominent words as an array mapping words to weights.
* @param int $limit The maximum number of link suggestions to retrieve.
* @param int $object_id The object id for the current indexable.
* @param string $object_type The object type for the current indexable.
* @param array $words_from_request The prominent words as an array mapping words to weights.
* @param int $limit The maximum number of link suggestions to retrieve.
* @param int $object_id The object id for the current indexable.
* @param string $object_type The object type for the current indexable.
* @param bool $include_existing_links Optional. Whether or not to include existing links, defaults to true.
* @param array $post_type Optional. The list of post types where suggestions may come from.
* @param bool $only_include_public Optional. Only include public indexables, defaults to false.
*
* @return array Links for the post that are suggested.
*/
public function get_suggestions( $words_from_request, $limit, $object_id, $object_type ) {
public function get_suggestions( $words_from_request, $limit, $object_id, $object_type, $include_existing_links = true, $post_type = [], $only_include_public = false ) {
$current_indexable_id = null;
$current_indexable = $this->indexable_repository->find_by_id_and_type( $object_id, $object_type );
if ( $current_indexable ) {
@@ -81,9 +101,9 @@ class Link_Suggestions_Action {
/*
* Gets best suggestions (returns a sorted array [$indexable_id => score]).
* The indexables are processed in batches of 100 indexables each.
* The indexables are processed in batches of 1000 indexables each.
*/
$suggestions_scores = $this->retrieve_suggested_indexable_ids( $words_from_request, $limit, 100, $current_indexable_id );
$suggestions_scores = $this->retrieve_suggested_indexable_ids( $words_from_request, $limit, self::BATCH_SIZE, $current_indexable_id, $include_existing_links, $post_type, $only_include_public );
$indexable_ids = \array_keys( $suggestions_scores );
@@ -109,6 +129,41 @@ class Link_Suggestions_Action {
return $this->create_suggestions( $suggestions_indexables, $suggestions_scores );
}
/**
* Suggests a list of links, based on the given array of prominent words.
*
* @param int $id The object id for the current indexable.
* @param int $limit The maximum number of link suggestions to retrieve.
* @param bool $include_existing_links Optional. Whether or not to include existing links, defaults to true.
*
* @return array Links for the post that are suggested.
*/
public function get_indexable_suggestions_for_indexable( $id, $limit, $include_existing_links = true ) {
$weighted_words = [];
$prominent_words = $this->prominent_words_repository->query()
->where( 'indexable_id', $id )
->find_array();
foreach ( $prominent_words as $prominent_word ) {
$weighted_words[ $prominent_word['stem'] ] = $prominent_word['weight'];
}
/*
* Gets best suggestions (returns a sorted array [$indexable_id => score]).
* The indexables are processed in batches of 1000 indexables each.
*/
$suggestions_scores = $this->retrieve_suggested_indexable_ids( $weighted_words, $limit, self::BATCH_SIZE, $id, $include_existing_links );
$indexable_ids = \array_keys( $suggestions_scores );
// Return the empty list if no suggestions have been found.
if ( empty( $indexable_ids ) ) {
return [];
}
// Retrieve indexables for suggestions.
return $this->indexable_repository->query()->where_id_in( $indexable_ids )->find_array();
}
/**
* Retrieves the titles of the posts with the given IDs.
*
@@ -297,16 +352,20 @@ class Link_Suggestions_Action {
* Request prominent words for indexables in the batch (including the iDF of all words) to calculate
* their vector length later.
*
* @param array $stems The stems in the request.
* @param int $batch_size How many indexables to request in one query.
* @param int $page The start of the current batch (in pages).
* @param array $stems The stems in the request.
* @param int $batch_size How many indexables to request in one query.
* @param int $page The start of the current batch (in pages).
* @param int[] $excluded_ids The indexable IDs to exclude.
* @param array $post_type The post types that will be searched.
* @param bool $only_include_public If only public indexables are included.
*
* @return array An array of ProminentWords objects, containing their stem, weight, indexable id,
* and document frequency.
*/
protected function get_candidate_words( $stems, $batch_size, $page ) {
protected function get_candidate_words( $stems, $batch_size, $page, $excluded_ids = [], $post_type = [], $only_include_public = false ) {
return $this->prominent_words_repository->find_by_list_of_ids(
$this->prominent_words_repository->find_ids_by_stems( $stems, $batch_size, $page )
$this->prominent_words_repository->find_ids_by_stems( $stems, $batch_size, $page, $excluded_ids, $post_type, $only_include_public )
);
}
@@ -315,27 +374,42 @@ class Link_Suggestions_Action {
* The candidate indexables are analyzed in batches.
* After having computed scores for a batch the function saves the best candidates until now.
*
* @param array $request_words The words to match, as an array mapping words to weights.
* @param int $limit The max number of suggestions that should be returned by the function.
* @param int $batch_size The number of indexables that should be analyzed in every batch.
* @param int|null $current_indexable_id The id for the current indexable.
* @param array $request_words The words to match, as an array mapping words to weights.
* @param int $limit The max number of suggestions that should be returned by the function.
* @param int $batch_size The number of indexables that should be analyzed in every batch.
* @param int|null $current_indexable_id The id for the current indexable.
* @param bool $include_existing_links Optional. Whether or not to include existing links, defaults to true.
* @param array $post_type Optional. The list of post types where suggestions may come from.
* @param bool $only_include_public Optional. Only include public indexables, defaults to false.
*
* @return array An array mapping indexable IDs to scores. Higher scores mean better matches.
*/
protected function retrieve_suggested_indexable_ids( $request_words, $limit, $batch_size, $current_indexable_id ) {
protected function retrieve_suggested_indexable_ids( $request_words, $limit, $batch_size, $current_indexable_id, $include_existing_links = true, $post_type = [], $only_include_public = false ) {
// Combine stems, weights and DFs from request.
$request_data = $this->compose_request_data( $request_words );
// Calculate vector length of the request set (needed for score normalization later).
$request_vector_length = $this->prominent_words_helper->compute_vector_length( $request_data );
// Get all links the post already links to, those shouldn't be suggested.
$excluded_indexable_ids = [ $current_indexable_id ];
if ( ! $include_existing_links && $current_indexable_id ) {
$links = $this->links_repository->query()
->distinct()
->select( 'indexable_id' )
->where( 'target_indexable_id', $current_indexable_id )
->find_many();
$excluded_indexable_ids = \array_merge( $excluded_indexable_ids, \wp_list_pluck( $links, 'indexable_id' ) );
}
$excluded_indexable_ids = \array_filter( $excluded_indexable_ids );
$request_stems = \array_keys( $request_data );
$scores = [];
$page = 1;
do {
// Retrieve the words of all indexables in this batch that share prominent word stems with request.
$candidates_words = $this->get_candidate_words( $request_stems, $batch_size, $page );
$candidates_words = $this->get_candidate_words( $request_stems, $batch_size, $page, $excluded_indexable_ids, $post_type, $only_include_public );
// Transform the prominent words table so that it is indexed by indexable_ids.
$candidates_words_by_indexable_ids = $this->group_words_by_indexable_id( $candidates_words );
@@ -347,10 +421,6 @@ class Link_Suggestions_Action {
++$batch_scores_size;
}
if ( $current_indexable_id && isset( $scores[ $current_indexable_id ] ) ) {
unset( $scores[ $current_indexable_id ] );
}
// Sort the list of scores and keep only the top $limit of the scores.
$scores = $this->get_top_suggestions( $scores, $limit );
@@ -393,10 +463,11 @@ class Link_Suggestions_Action {
// Sort the indexables by descending score.
\uasort(
$scores,
static function( $score_1, $score_2 ) {
static function ( $score_1, $score_2 ) {
if ( $score_1 === $score_2 ) {
return 0;
}
return ( ( $score_1 < $score_2 ) ? 1 : -1 );
}
);
@@ -506,7 +577,7 @@ class Link_Suggestions_Action {
protected function sort_suggestions_by_field( array &$link_suggestions, $field ) {
\usort(
$link_suggestions,
static function( $suggestion_1, $suggestion_2 ) use ( $field ) {
static function ( $suggestion_1, $suggestion_2 ) use ( $field ) {
if ( $suggestion_1[ $field ] === $suggestion_2[ $field ] ) {
return 0;
}
@@ -527,7 +598,7 @@ class Link_Suggestions_Action {
protected function filter_suggestions( $link_suggestions, $cornerstone ) {
return \array_filter(
$link_suggestions,
static function( $suggestion ) use ( $cornerstone ) {
static function ( $suggestion ) use ( $cornerstone ) {
return (bool) $suggestion['isCornerstone'] === $cornerstone;
}
);

View File

@@ -2,7 +2,7 @@
namespace Yoast\WP\SEO\Premium\Actions\Prominent_Words;
use Yoast\WP\SEO\Helpers\Prominent_Words_Helper;
use Yoast\WP\SEO\Premium\Helpers\Prominent_Words_Helper;
/**
* Action for completing the prominent words indexing.

View File

@@ -127,16 +127,14 @@ class Content_Action implements Indexation_Action_Interface {
}
/**
* Retrieves a batch of indexables, to be indexed for internal linking suggestions.
* The total number of indexables without prominent words.
*
* @deprecated 15.1
* @codeCoverageIgnore
* @param int $limit Limit the number of unindexed objects that are counted.
*
* @return array The indexables data to use for generating prominent words.
* @return int|false The total number of indexables without prominent words. False if the query fails.
*/
public function get() {
\_deprecated_function( __METHOD__, '15.1', 'Content_Action::index' );
return $this->index();
public function get_limited_unindexed_count( $limit ) {
return $this->get_total_unindexed();
}
/**
@@ -155,7 +153,9 @@ class Content_Action implements Indexation_Action_Interface {
->limit( $this->get_limit() )
->find_many();
\delete_transient( static::TRANSIENT_CACHE_KEY );
if ( \count( $indexables ) > 0 ) {
\delete_transient( static::TRANSIENT_CACHE_KEY );
}
// If no indexables have been left unindexed, return the empty array.
if ( ! $indexables ) {

View File

@@ -4,15 +4,16 @@ namespace Yoast\WP\SEO\Premium\Actions\Prominent_Words;
use Exception;
use WPSEO_Premium_Prominent_Words_Versioning;
use Yoast\WP\SEO\Helpers\Prominent_Words_Helper;
use Yoast\WP\SEO\Models\Prominent_Words;
use Yoast\WP\SEO\Premium\Helpers\Prominent_Words_Helper;
use Yoast\WP\SEO\Premium\Repositories\Prominent_Words_Repository;
use Yoast\WP\SEO\Repositories\Indexable_Repository;
use Yoast\WP\SEO\Repositories\Prominent_Words_Repository;
/**
* Action for linking a list of prominent words to an indexable.
* Action for updating the prominent words in the prominent words table,
* and linking them to an indexable.
*
* @see \Yoast\WP\SEO\Routes\Prominent_Words_Route;
* @see \Yoast\WP\SEO\Premium\Routes\Prominent_Words_Route;
*/
class Save_Action {
@@ -58,11 +59,11 @@ class Save_Action {
}
/**
* Links a list of prominent words to an indexable.
* Passes to-be-linked prominent words to the link function, together with the object type and object id of the
* indexable to which they will need to be linked.
*
* Deletes the prominent words that have been stored previously, but are not in the new list of prominent words.
*
* @param array $data The data to process.
* @param array $data The data to process. This is an array consisting of associative arrays (1 per indexable) with the keys
* 'object_id', 'object_type' and 'prominent_words' (an array with 'stem' => 'weight' mappings).
*/
public function save( $data ) {
if ( $data ) {
@@ -75,7 +76,7 @@ class Save_Action {
}
/**
* Links a list of prominent words to an indexable.
* Inserts, updates and removes prominent words that are now, or are no longer, associated with an indexable.
*
* @param string $object_type The object type of the indexable (e.g. `post` or `term`).
* @param int $object_id The object id of the indexable.
@@ -84,41 +85,87 @@ class Save_Action {
public function link( $object_type, $object_id, $words ) {
$indexable = $this->indexable_repository->find_by_id_and_type( $object_id, $object_type );
$indexable->prominent_words_version = WPSEO_Premium_Prominent_Words_Versioning::get_version_number();
if ( $indexable ) {
// Set the prominent words version number on the indexable.
$indexable->prominent_words_version = WPSEO_Premium_Prominent_Words_Versioning::get_version_number();
/*
* It is correct to save here, because find_by_id_and_type will auto create an indexable object
* with the correct data. So we are not saving an incomplete indexable.
*/
$indexable->save();
/*
* It is correct to save here, because if the indexable didn't exist yet,
* find_by_id_and_type (in the above 'save' function) will have auto-created an indexable object
* with the correct data. So we are not saving an incomplete indexable.
*/
$indexable->save();
$old_words = $this->prominent_words_repository->find_by_indexable_id( $indexable->id );
// Find the prominent words that were already associated with this indexable.
$old_words = $this->prominent_words_repository->find_by_indexable_id( $indexable->id );
// Handle these words.
$words = $this->handle_old_words( $indexable->id, $old_words, $words );
// Create database entries for all new words that are not yet in the database.
$this->create_words( $indexable->id, $words );
}
}
/**
* Deletes outdated prominent words from the database, and otherwise considers
* whether the old words need to have their weights updated.
*
* @param int $indexable_id The id of the indexable which needs to have its
* old words updated.
* @param Prominent_Words[] $old_words An array with prominent words that were already
* present in the database for a given indexable.
* @param array $words The new prominent words for a given indexable.
*
* @return array The words that need to be created.
*/
protected function handle_old_words( $indexable_id, $old_words, $words ) {
// Return early if the indexable didn't already have any prominent words associated with it.
if ( empty( $old_words ) ) {
return $words;
}
$outdated_stems = [];
foreach ( $old_words as $old_word ) {
// Remove when old word isn't found.
// If an old prominent word is no longer associated with an indexable,
// add it to the array with outdated stems, so that at a later step
// it can be deleted from the database.
if ( ! \array_key_exists( $old_word->stem, $words ) ) {
$old_word->delete();
$outdated_stems[] = $old_word->stem;
continue;
}
// If the old word should still be associated with the indexable,
// update its weight if that has changed.
$this->update_weight_if_changed( $old_word, $words[ $old_word->stem ] );
// Remove the key from the array with the new prominent words.
unset( $words[ $old_word->stem ] );
}
// Create all new words that are not yet in the database.
$this->create_words( $indexable->id, $words );
// Delete all the outdated prominent words in one query.
try {
$this->prominent_words_repository->delete_by_indexable_id_and_stems( $indexable_id, $outdated_stems );
// phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch -- There is nothing to do.
} catch ( Exception $exception ) {
// Do nothing.
}
return $words;
}
/**
* Updates the weight of the given prominent word.
* (Does not update when the weights are the same).
* Updates the weight of the given prominent word, if the weight has changed significantly.
*
* @param Prominent_Words $word The prominent word of which to update the weight.
* @param float $new_weight The new weight.
*
* @return void
*/
protected function update_weight_if_changed( $word, $new_weight ) {
if ( $word->weight !== $new_weight ) {
if ( \abs( $word->weight - $new_weight ) > 0.1 ) {
$word->weight = $new_weight;
$word->save();
}
@@ -128,24 +175,34 @@ class Save_Action {
* Creates the given words in the database and links them to the indexable with the given id.
*
* @param int $indexable_id The ID of the indexable.
* @param array $words The words to create, as a `'stem'` => weight` map.
* @param array $words The prominent words to create, as a `'stem'` => weight` map.
*
* @return void
*/
protected function create_words( $indexable_id, $words ) {
// Return early if there are no new words to add to the database.
if ( empty( $words ) ) {
return;
}
$new_models = [];
foreach ( $words as $stem => $weight ) {
$new_word = $this->prominent_words_repository->query()->create(
$new_model = $this->prominent_words_repository->query()->create(
[
'indexable_id' => $indexable_id,
'stem' => $stem,
'weight' => $weight,
]
);
$new_models[] = $new_model;
}
try {
$new_word->save();
try {
$this->prominent_words_repository->query()->insert_many( $new_models );
// phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch -- There is nothing to do.
} catch ( Exception $exception ) {
// Do nothing.
}
} catch ( Exception $exception ) {
// Do nothing.
}
}
}

View File

@@ -17,7 +17,7 @@ class Addon_Installer {
/**
* The minimum Yoast SEO version required.
*/
const MINIMUM_YOAST_SEO_VERSION = '16.4';
const MINIMUM_YOAST_SEO_VERSION = '21.5';
/**
* The base directory for the installer.
@@ -198,13 +198,22 @@ class Addon_Installer {
);
}
/**
* Checks if Yoast SEO is at a minimum required version.
*
* @return bool True if Yoast SEO is at a minimal required version
*/
public static function is_yoast_seo_up_to_date() {
return ( \defined( 'WPSEO_VERSION' ) && \version_compare( \WPSEO_VERSION, self::MINIMUM_YOAST_SEO_VERSION . '-RC0', '>=' ) );
}
/**
* Resets the installation status if Yoast SEO is not installed or outdated.
*
* @return void
*/
public function validate_installation_status() {
if ( ! \defined( 'WPSEO_VERSION' ) || \version_compare( \WPSEO_VERSION, self::MINIMUM_YOAST_SEO_VERSION . '-RC0', '<' ) ) {
if ( ! self::is_yoast_seo_up_to_date() ) {
\delete_option( self::OPTION_KEY );
if ( ! \defined( 'WPSEO_VERSION' ) ) {
$this->load_yoast_seo_from_vendor_directory();
@@ -361,8 +370,9 @@ class Addon_Installer {
* @throws Exception If the move failed.
*/
protected function move_vendor_directory() {
if ( ! \rename( $this->base_dir . '/vendor/yoast/wordpress-seo', $this->yoast_seo_dir ) ) {
throw new Exception( 'Could not automatically installed Yoast SEO' );
// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- Prevent a potential PHP warning on Windows.
if ( ! @\rename( $this->base_dir . '/vendor/yoast/wordpress-seo', $this->yoast_seo_dir ) ) {
throw new Exception( 'Could not automatically install Yoast SEO' );
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace Yoast\WP\SEO\Premium\Conditionals;
use Yoast\WP\SEO\Conditionals\Admin\Post_Conditional;
use Yoast\WP\SEO\Conditionals\Conditional;
use Yoast\WP\SEO\Helpers\Current_Page_Helper;
/**
* Conditional that is met when the AI editor integration should be active.
*/
class Ai_Editor_Conditional implements Conditional {
/**
* Holds the Post_Conditional.
*
* @var Post_Conditional
*/
private $post_conditional;
/**
* Holds the Current_Page_Helper.
*
* @var Current_Page_Helper
*/
private $current_page_helper;
/**
* Constructs Ai_Editor_Conditional.
*
* @param Post_Conditional $post_conditional The Post_Conditional.
* @param Current_Page_Helper $current_page_helper The Current_Page_Helper.
*/
public function __construct( Post_Conditional $post_conditional, Current_Page_Helper $current_page_helper ) {
$this->post_conditional = $post_conditional;
$this->current_page_helper = $current_page_helper;
}
/**
* Returns `true` when the AI editor integration should be active.
*
* @return bool `true` when the AI editor integration should be active.
*/
public function is_met() {
return $this->post_conditional->is_met() || $this->is_elementor_editor();
}
/**
* Returns `true` when the page is the elementor editor.
*
* @return bool `true` when the page is the elementor editor.
*/
private function is_elementor_editor() {
if ( $this->current_page_helper->get_current_admin_page() !== 'post.php' ) {
return false;
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information.
if ( isset( $_GET['action'] ) && \is_string( $_GET['action'] ) ) {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are not processing form information, We are only strictly comparing.
if ( \wp_unslash( $_GET['action'] ) === 'elementor' ) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace Yoast\WP\SEO\Premium\Conditionals;
use Yoast\WP\SEO\Conditionals\Conditional;
use Yoast\WP\SEO\Helpers\Options_Helper;
/**
* Conditional that is only met when the Algolia integration is enabled.
*/
class Algolia_Enabled_Conditional implements Conditional {
/**
* The options helper.
*
* @var Options_Helper
*/
private $options_helper;
/**
* Algolia_Enabled_Conditional constructor.
*
* @param Options_Helper $options_helper The options helper.
*/
public function __construct( Options_Helper $options_helper ) {
$this->options_helper = $options_helper;
}
/**
* Returns whether or not this conditional is met.
*
* @return bool Whether or not the conditional is met.
*/
public function is_met() {
return $this->options_helper->get( 'algolia_integration_active' ) === true;
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Yoast\WP\SEO\Premium\Conditionals;
use Yoast\WP\SEO\Conditionals\Conditional;
use Yoast\WP\SEO\Helpers\Options_Helper;
/**
* Cornerstone_Enabled_Conditional class.
*/
class Cornerstone_Enabled_Conditional implements Conditional {
/**
* The options helper.
*
* @var Options_Helper
*/
protected $options_helper;
/**
* Cornerstone_Enabled_Conditional constructor.
*
* @codeCoverageIgnore
*
* @param Options_Helper $options_helper The options helper.
*/
public function __construct( Options_Helper $options_helper ) {
$this->options_helper = $options_helper;
}
/**
* Returns `true` when the cornerstone content feature is enabled.
*
* @return bool `true` when the cornerstone content feature is enabled.
*/
public function is_met() {
return $this->options_helper->get( 'enable_cornerstone_content' );
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Yoast\WP\SEO\Premium\Conditionals;
use Yoast\WP\SEO\Conditionals\Conditional;
/**
* Conditional that is only met when Easy Digital Downloads is active.
*/
class EDD_Conditional implements Conditional {
/**
* Returns `true` when the Easy Digital Downloads plugin is installed and activated.
*
* @return bool `true` when the Easy Digital Downloads plugin is installed and activated.
*/
public function is_met() {
return \class_exists( 'Easy_Digital_Downloads' );
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Yoast\WP\SEO\Premium\Conditionals;
use WPSEO_Metabox_Analysis_Inclusive_Language;
use Yoast\WP\SEO\Conditionals\Conditional;
/**
* Inclusive_Language_Enabled_Conditional class.
*
* phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
*/
class Inclusive_Language_Enabled_Conditional implements Conditional {
/**
* Returns `true` when the inclusive language analysis is enabled.
*
* @return bool `true` when the inclusive language analysis is enabled.
*/
public function is_met() {
$analysis_inclusive_language = new WPSEO_Metabox_Analysis_Inclusive_Language();
return $analysis_inclusive_language->is_enabled();
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Yoast\WP\SEO\Premium\Conditionals;
use Yoast\WP\SEO\Conditionals\Conditional;
/**
* Conditional that is only met when on a term overview page or during an ajax request.
*
* phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
*/
class Term_Overview_Or_Ajax_Conditional implements Conditional {
/**
* Returns whether or not this conditional is met.
*
* @return bool Whether or not the conditional is met.
*/
public function is_met() {
global $pagenow;
return $pagenow === 'edit-tags.php' || \wp_doing_ajax();
}
}

View File

@@ -11,6 +11,7 @@ use Yoast\WP\SEO\Config\Badge_Group_Names as New_Badge_Group_Names;
* to be "new".
*/
class Badge_Group_Names extends New_Badge_Group_Names {
const GROUP_GLOBAL_TEMPLATES = 'global-templates';
/**
@@ -23,7 +24,7 @@ class Badge_Group_Names extends New_Badge_Group_Names {
/**
* Badge_Group_Names constructor.
*
* @param string $version Optional: the current version number.
* @param string|null $version Optional. The current version number.
*/
public function __construct( $version = null ) {
parent::__construct( $version );

View File

@@ -0,0 +1,85 @@
<?php
namespace Yoast\WP\SEO\Premium\Config\Migrations;
use Yoast\WP\Lib\Migrations\Migration;
use Yoast\WP\Lib\Model;
/**
* AddIndexOnIndexableIdAndStem class.
*/
class AddIndexOnIndexableIdAndStem extends Migration {
/**
* The plugin this migration belongs to.
*
* @var string
*/
public static $plugin = 'premium';
/**
* The columns on which an index should be added.
*
* @var string[]
*/
protected $columns_with_index = [
'indexable_id',
'stem',
];
/**
* Migration up. Adds a combined index on 'indexable_id' and 'stem'.
*
* @return void
*/
public function up() {
$table_name = $this->get_table_name();
$adapter = $this->get_adapter();
if ( ! $adapter->has_table( $table_name ) ) {
return;
}
// Create the index if it doesn't exist already.
if ( ! $adapter->has_index( $table_name, $this->columns_with_index, [ 'name' => 'indexable_id_and_stem' ] ) ) {
$this->add_index(
$this->get_table_name(),
$this->columns_with_index,
[ 'name' => 'indexable_id_and_stem' ]
);
}
}
/**
* Migration down. Removes the combined index on 'indexable_id' and 'stem'.
*
* @return void
*/
public function down() {
$table_name = $this->get_table_name();
$adapter = $this->get_adapter();
if ( ! $adapter->has_table( $table_name ) ) {
return;
}
// Remove the index if it exists.
if ( $adapter->has_index( $table_name, $this->columns_with_index, [ 'name' => 'indexable_id_and_stem' ] ) ) {
$this->remove_index(
$this->get_table_name(),
$this->columns_with_index,
[ 'name' => 'indexable_id_and_stem' ]
);
}
}
/**
* Retrieves the table name to use for storing prominent words.
*
* @return string The table name to use.
*/
protected function get_table_name() {
return Model::get_table_name( 'Prominent_Words' );
}
}

View File

@@ -13,7 +13,7 @@ class Migration_Runner_Premium extends Migration_Runner {
/**
* Runs this initializer.
*
* @inheritDoc
* {@inheritDoc}
*/
public function initialize() {
$this->run_premium_migrations();

View File

@@ -0,0 +1,244 @@
<?php
namespace Yoast\WP\SEO\Premium\Actions;
use Yoast\WP\SEO\Premium\Helpers\Zapier_Helper;
use Yoast\WP\SEO\Repositories\Indexable_Repository;
/**
* Handles the actual requests to the Zapier endpoints.
*
* @deprecated 20.7
* @codeCoverageIgnore
*/
class Zapier_Action {
/**
* The Zapier helper.
*
* @var Zapier_Helper
*/
protected $zapier_helper;
/**
* The Indexable repository.
*
* @var Indexable_Repository
*/
protected $indexable_repository;
/**
* Zapier_Action constructor.
*
* @deprecated 20.7
* @codeCoverageIgnore
*
* @param Zapier_Helper $zapier_helper The Zapier helper.
* @param Indexable_Repository $indexable_repository The Indexable repository.
*/
public function __construct( Zapier_Helper $zapier_helper, Indexable_Repository $indexable_repository ) {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.7' );
$this->zapier_helper = $zapier_helper;
$this->indexable_repository = $indexable_repository;
}
/**
* Subscribes Zapier and stores the passed URL for later usage.
*
* @deprecated 20.7
* @codeCoverageIgnore
*
* @param string $url The URL to subscribe.
* @param string $api_key The API key from Zapier to check against the one stored in the options.
*
* @return object The response object.
*/
public function subscribe( $url, $api_key ) {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.7' );
if ( ! $this->zapier_helper->is_valid_api_key( $api_key ) ) {
return (object) [
'data' => [],
'message' => 'The API key does not match.',
'status' => 500,
];
}
if ( $this->zapier_helper->is_connected() ) {
return (object) [
'data' => [],
'message' => 'Subscribing failed. A subscription already exists.',
'status' => 500,
];
}
$subscription_data = $this->zapier_helper->subscribe_url( $url );
if ( ! $subscription_data ) {
return (object) [
'data' => [],
'message' => 'Subscribing failed.',
'status' => 500,
];
}
return (object) [
'data' => $subscription_data,
'status' => 200,
];
}
/**
* Unsubscribes Zapier based on the passed ID.
*
* @deprecated 20.7
* @codeCoverageIgnore
*
* @param string $id The ID to unsubscribe.
*
* @return object The response object.
*/
public function unsubscribe( $id ) {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.7' );
if ( ! $this->zapier_helper->is_subscribed_id( $id ) ) {
return (object) [
'message' => \sprintf( 'Unsubscribing failed. Subscription with ID `%s` does not exist.', $id ),
'status' => 404,
];
}
if ( ! $this->zapier_helper->unsubscribe_id( $id ) ) {
return (object) [
'message' => 'Unsubscribing failed. Unable to delete subscription.',
'status' => 500,
];
}
return (object) [
'message' => \sprintf( 'Successfully unsubscribed subscription with ID `%s`.', $id ),
'status' => 200,
];
}
/**
* Checks the API key submitted by Zapier.
*
* @deprecated 20.7
* @codeCoverageIgnore
*
* @param string $api_key The API key from Zapier to check against the one
* stored in the options.
*
* @return object The response object.
*/
public function check_api_key( $api_key ) {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.7' );
if ( ! $this->zapier_helper->is_valid_api_key( $api_key ) ) {
return (object) [
'data' => [],
'message' => 'The API key does not match.',
'status' => 500,
];
}
return (object) [
'data' => [],
'message' => 'The API key is valid.',
'status' => 200,
];
}
/**
* Sends an array of the last published post URLs.
*
* @deprecated 20.7
* @codeCoverageIgnore
*
* @param string $api_key The API key from Zapier to check against the one
* stored in the options.
*
* @return object The response object.
*/
public function perform_list( $api_key ) {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.7' );
if ( ! $this->zapier_helper->is_valid_api_key( $api_key ) ) {
return (object) [
'data' => [],
'message' => 'The API key does not match.',
'status' => 500,
];
}
$latest_post = \get_posts(
[
'numberposts' => 1,
]
);
$zapier_data = [];
foreach ( $latest_post as $item ) {
$indexable = $this->indexable_repository->find_by_id_and_type( $item->ID, 'post' );
if ( $indexable ) {
$zapier_data[] = (object) $this->zapier_helper->get_data_for_zapier( $indexable );
}
}
return (object) [
'data' => $zapier_data,
'status' => 200,
];
}
/**
* Checks if Zapier is connected.
*
* @deprecated 20.7
* @codeCoverageIgnore
*
* @return object The response object.
*/
public function is_connected() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.7' );
return (object) [
'data' => [
'is_connected' => $this->zapier_helper->is_connected(),
],
'status' => 200,
];
}
/**
* Resets the API key in the DB.
*
* @deprecated 20.7
* @codeCoverageIgnore
*
* @param string $api_key The API key to be reset.
*
* @return object The response object.
*/
public function reset_api_key( $api_key ) {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.7' );
if ( ! $this->zapier_helper->is_valid_api_key( $api_key ) ) {
return (object) [
'data' => [],
'message' => 'The API key does not match.',
'status' => 500,
];
}
$this->zapier_helper->reset_api_key_and_subscription();
$new_api_key = $this->zapier_helper->get_or_generate_zapier_api_key();
return (object) [
'data' => [
'zapier_api_key' => $new_api_key,
],
];
}
}

View File

@@ -0,0 +1,185 @@
<?php
/**
* WPSEO Premium plugin file.
*
* @package WPSEO\Premium\Classes
*/
/**
* This class represents the fetching of a full name for a user who has filled in a Facebook profile url. The class
* will try to fetch the full name via the Facebook following plugin (widget). If the user has chosen to disallow
* following of his profile, there isn't returned any name - only an empty string.
*
* To prevent doing request all the time, the obtained name will be stored as user meta for the user.
*
* @deprecated 20.3
* @codeCoverageIgnore
*/
class WPSEO_Facebook_Profile {
const TRANSIENT_NAME = 'yoast_facebook_profiles';
/**
* URL providing us the full name belonging to the user.
*
* @var string
*/
private $facebook_endpoint = 'https://www.facebook.com/plugins/follow.php?href=';
/**
* Sets the AJAX action hook, to catch the AJAX request for getting the name on Facebook.
*
* @deprecated 20.3
* @codeCoverageIgnore
*/
public function set_hooks() {
_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.3' );
add_action( 'wp_ajax_wpseo_get_facebook_name', [ $this, 'ajax_get_facebook_name' ] );
}
/**
* Sets the user id and prints the full Facebook name.
*
* @deprecated 20.3
* @codeCoverageIgnore
*/
public function ajax_get_facebook_name() {
_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.3' );
if ( wp_doing_ajax() ) {
check_ajax_referer( 'get_facebook_name' );
$user_id = (int) filter_input( INPUT_GET, 'user_id' );
$facebook_profile = $this->get_facebook_profile( $user_id );
// Only try to get the name when the user has a profile set.
if ( $facebook_profile !== '' ) {
wp_die( esc_html( $this->get_name( $facebook_profile ) ) );
}
wp_die();
}
}
/**
* Get the Facebook profile url from the user profile.
*
* @codeCoverageIgnore
*
* @param int $user_id The user to get the Facebook profile field for.
*
* @return string URL or empty string if the field is not set or empty.
*/
private function get_facebook_profile( $user_id ) {
$facebook_profile = get_the_author_meta( 'facebook', $user_id );
if ( ! empty( $facebook_profile ) ) {
return $facebook_profile;
}
return '';
}
/**
* Get the name used on Facebook from the transient cache, if the name isn't
* fetched already get it from the Facebook follow widget.
*
* @codeCoverageIgnore
*
* @param string $facebook_profile The profile to get.
*
* @return string
*/
private function get_name( $facebook_profile ) {
$cached_facebook_name = $this->get_cached_name( $facebook_profile );
if ( $cached_facebook_name !== false ) {
return $cached_facebook_name;
}
$facebook_name = $this->get_name_from_facebook( $facebook_profile );
$this->set_cached_name( $facebook_profile, $facebook_name );
return $facebook_name;
}
/**
* Returns the stored name from the user meta.
*
* @codeCoverageIgnore
*
* @param string $facebook_profile The Facebook profile to look for.
*
* @return string|bool
*/
private function get_cached_name( $facebook_profile ) {
$facebook_profiles = get_transient( self::TRANSIENT_NAME );
if ( is_array( $facebook_profiles ) && array_key_exists( $facebook_profile, $facebook_profiles ) ) {
return $facebook_profiles[ $facebook_profile ];
}
return false;
}
/**
* Stores the fetched Facebook name to the user meta.
*
* @codeCoverageIgnore
*
* @param string $facebook_profile The Facebook profile belonging to the name.
* @param string $facebook_name The name the user got on Facebook.
*/
private function set_cached_name( $facebook_profile, $facebook_name ) {
$facebook_profiles = get_transient( self::TRANSIENT_NAME );
$facebook_profiles[ $facebook_profile ] = $facebook_name;
set_transient( self::TRANSIENT_NAME, $facebook_profiles, DAY_IN_SECONDS );
}
/**
* Do request to Facebook to get the HTML for the follow widget.
*
* @codeCoverageIgnore
*
* @param string $facebook_profile The profile URL to lookup.
*
* @return string
*/
private function get_name_from_facebook( $facebook_profile ) {
$response = wp_remote_get(
$this->facebook_endpoint . $facebook_profile,
[
'headers' => [ 'Accept-Language' => 'en_US' ],
]
);
if ( wp_remote_retrieve_response_code( $response ) === 200 ) {
return $this->extract_name_from_response(
wp_remote_retrieve_body( $response )
);
}
return '';
}
/**
* Try to extract the full name from the response.
*
* @codeCoverageIgnore
*
* @param string $response_body The response HTML to lookup for the full name.
*
* @return string
*/
private function extract_name_from_response( $response_body ) {
$full_name_regex = '/<div class="pluginButton pluginButtonInline pluginConnectButtonDisconnected" title="Follow(.*)&#039;s public updates">/i';
if ( preg_match( $full_name_regex, $response_body, $matches ) ) {
if ( ! empty( $matches[1] ) ) {
return $matches[1];
}
}
return '';
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace Yoast\WP\SEO\Premium\Conditionals;
use Yoast\WP\SEO\Conditionals\Conditional;
use Yoast\WP\SEO\Premium\Helpers\Zapier_Helper;
/**
* Conditional that is only met when the Zapier integration is enabled.
*
* @deprecated 20.7
* @codeCoverageIgnore
*/
class Zapier_Enabled_Conditional implements Conditional {
/**
* The Zapier helper.
*
* @var Zapier_Helper
*/
private $zapier;
/**
* Zapier_Enabled_Conditional constructor.
*
* @deprecated 20.7
* @codeCoverageIgnore
*
* @param Zapier_Helper $zapier The Zapier helper.
*/
public function __construct( Zapier_Helper $zapier ) {
\_deprecated_function( __METHOD__, 'WPSEO Premium 20.7' );
$this->zapier = $zapier;
}
/**
* Returns whether or not this conditional is met.
*
* @deprecated 20.7
* @codeCoverageIgnore
*
* @return bool Whether or not the conditional is met.
*/
public function is_met() {
\_deprecated_function( __METHOD__, 'WPSEO Premium 20.7' );
return $this->zapier->is_enabled();
}
}

View File

@@ -0,0 +1,292 @@
<?php
namespace Yoast\WP\SEO\Premium\Helpers;
use WPSEO_Utils;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Surfaces\Meta_Surface;
/**
* Class Zapier_Helper
*
* @deprecated 20.7
* @codeCoverageIgnore
*
* @package Yoast\WP\SEO\Helpers
*/
class Zapier_Helper {
/**
* The options helper.
*
* @var Options_Helper
*/
protected $options;
/**
* The meta surface.
*
* @var Meta_Surface
*/
protected $meta_surface;
/**
* Zapier_Helper constructor.
*
* @deprecated 20.7
* @codeCoverageIgnore It only sets dependencies.
*
* @param Options_Helper $options The options helper.
* @param Meta_Surface $meta_surface The Meta surface.
*/
public function __construct( Options_Helper $options, Meta_Surface $meta_surface ) {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.7' );
$this->options = $options;
$this->meta_surface = $meta_surface;
}
/**
* Checks if a subscription exists in the database.
*
* @deprecated 20.7
* @codeCoverageIgnore
*
* @return bool Whether a subscription exists in the database.
*/
public function is_connected() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.7' );
$subscription = $this->options->get( 'zapier_subscription' );
if ( \is_array( $subscription )
&& ! empty( $subscription['id'] )
&& \filter_var( $subscription['url'], \FILTER_VALIDATE_URL )
) {
return true;
}
return false;
}
/**
* Checks if the Zapier integration is currently enabled.
*
* @deprecated 20.7
* @codeCoverageIgnore
*
* @return bool Whether the integration is enabled.
*/
public function is_enabled() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.7' );
return (bool) $this->options->get( 'zapier_integration_active', false );
}
/**
* Gets the stored Zapier API Key.
*
* @deprecated 20.7
* @codeCoverageIgnore
*
* @return string The Zapier API Key.
*/
public function get_or_generate_zapier_api_key() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.7' );
$zapier_api_key = $this->options->get( 'zapier_api_key' );
if ( empty( $zapier_api_key ) ) {
$zapier_api_key = \wp_generate_password( 32, false );
$this->options->set( 'zapier_api_key', $zapier_api_key );
}
return $zapier_api_key;
}
/**
* Resets the stored Zapier API Key and subscription data.
*
* @deprecated 20.7
* @codeCoverageIgnore
*
* @return void
*/
public function reset_api_key_and_subscription() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.7' );
$this->options->set( 'zapier_api_key', '' );
$this->options->set( 'zapier_subscription', [] );
}
/**
* Check if a string matches the API key in the DB, if present.
*
* @deprecated 20.7
* @codeCoverageIgnore
*
* @param string $api_key The API key to test.
*
* @return bool Whether the API key is valid or not.
*/
public function is_valid_api_key( $api_key ) {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.7' );
return ( ! empty( $api_key ) && $this->options->get( 'zapier_api_key' ) === $api_key );
}
/**
* Returns the Zapier hook URL of the trigger if present, null otherwise.
*
* @deprecated 20.7
* @codeCoverageIgnore
*
* @return string|null The hook URL, null if not set.
*/
public function get_trigger_url() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.7' );
if ( $this->is_connected() ) {
$subscription = $this->options->get( 'zapier_subscription', [] );
return $subscription['url'];
}
return null;
}
/**
* Returns whether the submitted id is present in the subscriptions.
*
* @deprecated 20.7
* @codeCoverageIgnore
*
* @param string $id The id to be tested.
*
* @return bool Whether the id is present in the subscriptions.
*/
public function is_subscribed_id( $id ) {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.7' );
if ( $this->is_connected() ) {
$subscription = $this->options->get( 'zapier_subscription', [] );
return $subscription['id'] === $id;
}
return false;
}
/**
* Unsubscribes the submitted id.
*
* @deprecated 20.7
* @codeCoverageIgnore
*
* @param string $id The id to be unsubscribed.
*
* @return bool Whether the unsubscription was successful.
*/
public function unsubscribe_id( $id ) {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.7' );
if ( $this->is_connected() && $this->is_subscribed_id( $id ) ) {
return $this->options->set( 'zapier_subscription', [] );
}
return false;
}
/**
* Creates a new subscription with the submitted URL.
*
* @deprecated 20.7
* @codeCoverageIgnore
*
* @param string $url The URL to be subscribed.
*
* @return array|bool The subscription data (id and URL) if successful, false otherwise.
*/
public function subscribe_url( $url ) {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.7' );
if ( ! $this->is_connected() ) {
$subscription_data = [
'id' => \wp_generate_password( 32, false ),
'url' => \esc_url_raw( $url, [ 'http', 'https' ] ),
];
if ( $this->options->set( 'zapier_subscription', $subscription_data ) ) {
return $subscription_data;
}
}
return false;
}
/**
* Builds and returns the data for Zapier.
*
* @deprecated 20.7
* @codeCoverageIgnore
*
* @param Indexable $indexable The indexable from which the data must be extracted.
*
* @return array[] The array of data ready to be sent to Zapier.
*/
public function get_data_for_zapier( Indexable $indexable ) {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.7' );
$post = \get_post( $indexable->object_id );
if ( ! $post ) {
return [];
}
$meta = $this->meta_surface->for_indexable( $indexable );
$open_graph_image = '';
if ( \count( $meta->open_graph_images ) > 0 ) {
$open_graph_image_array = \reset( $meta->open_graph_images );
$open_graph_image = $open_graph_image_array['url'];
}
return [
'url' => $indexable->permalink,
'post_type' => $post->post_type,
'post_title' => \html_entity_decode( $post->post_title ),
'author' => \get_the_author_meta( 'display_name', $post->post_author ),
'tags' => \html_entity_decode( \implode( ', ', \wp_get_post_tags( $post->ID, [ 'fields' => 'names' ] ) ) ),
'categories' => \html_entity_decode( \implode( ', ', \wp_get_post_categories( $post->ID, [ 'fields' => 'names' ] ) ) ),
'primary_category' => \html_entity_decode( \yoast_get_primary_term( 'category', $post ) ),
'meta_description' => \html_entity_decode( $meta->description ),
'open_graph_title' => \html_entity_decode( $meta->open_graph_title ),
'open_graph_description' => \html_entity_decode( $meta->open_graph_description ),
'open_graph_image' => $open_graph_image,
'twitter_title' => \html_entity_decode( $meta->twitter_title ),
'twitter_description' => \html_entity_decode( $meta->twitter_description ),
'twitter_image' => $meta->twitter_image,
];
}
/**
* Returns whether the post type is supported by the Zapier integration.
*
* The Zapier integration should be visible and working only for post types
* that support the Yoast Metabox. We filter out attachments regardless of
* the Yoast SEO settings, anyway.
*
* @deprecated 20.7
* @codeCoverageIgnore
*
* @param string $post_type The post type to be checked.
*
* @return bool Whether the post type is supported by the Zapier integration.
*/
public function is_post_type_supported( $post_type ) {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.7' );
return $post_type !== 'attachment' && WPSEO_Utils::is_metabox_active( $post_type, 'post_type' );
}
}

View File

@@ -0,0 +1,315 @@
<?php
namespace Yoast\WP\SEO\Premium\Initializers;
use Yoast\WP\SEO\Conditionals\Front_End_Conditional;
use Yoast\WP\SEO\Helpers\Current_Page_Helper;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Url_Helper;
use Yoast\WP\SEO\Initializers\Initializer_Interface;
/**
* Class Crawl_Cleanup_Permalinks.
*
* @deprecated 20.4
* @codeCoverageIgnore
*/
class Crawl_Cleanup_Permalinks implements Initializer_Interface {
/**
* The current page helper
*
* @var Current_Page_Helper
*/
private $current_page_helper;
/**
* The options helper.
*
* @var Options_Helper
*/
private $options_helper;
/**
* The URL helper.
*
* @var Url_Helper
*/
private $url_helper;
/**
* Crawl Cleanup Permalinks constructor.
*
* @deprecated 20.4
* @codeCoverageIgnore
*
* @param Current_Page_Helper $current_page_helper The current page helper.
* @param Options_Helper $options_helper The option helper.
* @param Url_Helper $url_helper The URL helper.
*/
public function __construct(
Current_Page_Helper $current_page_helper,
Options_Helper $options_helper,
Url_Helper $url_helper
) {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.4', 'Yoast\WP\SEO\Initializers\Crawl_Cleanup_Permalinks' );
$this->current_page_helper = $current_page_helper;
$this->options_helper = $options_helper;
$this->url_helper = $url_helper;
}
/**
* Initializes the integration.
*
* @deprecated 20.4
* @codeCoverageIgnore
*
* @return void
*/
public function initialize() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.4', 'Yoast\WP\SEO\Initializers\Crawl_Cleanup_Permalinks::initialize()' );
// We need to hook after 10 because otherwise our options helper isn't available yet.
\add_action( 'plugins_loaded', [ $this, 'register_hooks' ], 15 );
}
/**
* Hooks our required hooks.
*
* This is the place to register hooks and filters.
*
* @deprecated 20.4
* @codeCoverageIgnore
*
* @return void
*/
public function register_hooks() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.4', 'Yoast\WP\SEO\Initializers\Crawl_Cleanup_Permalinks::register_hooks()' );
if ( $this->options_helper->get( 'clean_campaign_tracking_urls' ) && ! empty( \get_option( 'permalink_structure' ) ) ) {
\add_action( 'template_redirect', [ $this, 'utm_redirect' ], 0 );
}
if ( $this->options_helper->get( 'clean_permalinks' ) && ! empty( \get_option( 'permalink_structure' ) ) ) {
\add_action( 'template_redirect', [ $this, 'clean_permalinks' ], 1 );
}
}
/**
* Returns the conditionals based in which this loadable should be active.
*
* @deprecated 20.4
* @codeCoverageIgnore
*
* @return array The array of conditionals.
*/
public static function get_conditionals() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.4', 'Yoast\WP\SEO\Initializers\Crawl_Cleanup_Permalinks::get_conditionals()' );
return [ Front_End_Conditional::class ];
}
/**
* Redirect utm variables away.
*
* @deprecated 20.4
* @codeCoverageIgnore
*/
public function utm_redirect() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.4', 'Yoast\WP\SEO\Initializers\Crawl_Cleanup_Permalinks::utm_redirect()' );
// Prevents WP CLI from throwing an error.
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput
if ( ! isset( $_SERVER['REQUEST_URI'] ) || \strpos( $_SERVER['REQUEST_URI'], '?' ) === false ) {
return;
}
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput
if ( ! \stripos( $_SERVER['REQUEST_URI'], 'utm_' ) ) {
return;
}
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput
$parsed = \wp_parse_url( $_SERVER['REQUEST_URI'] );
$query = \explode( '&', $parsed['query'] );
$utms = [];
$other_args = [];
foreach ( $query as $query_arg ) {
if ( \stripos( $query_arg, 'utm_' ) === 0 ) {
$utms[] = $query_arg;
continue;
}
$other_args[] = $query_arg;
}
if ( empty( $utms ) ) {
return;
}
$other_args_str = '';
if ( \count( $other_args ) > 0 ) {
$other_args_str = '?' . \implode( '&', $other_args );
}
$new_path = $parsed['path'] . $other_args_str . '#' . \implode( '&', $utms );
$message = \sprintf(
/* translators: %1$s: Yoast SEO Premium */
\__( '%1$s: redirect utm variables to #', 'wordpress-seo-premium' ),
'Yoast SEO Premium'
);
\wp_safe_redirect( \trailingslashit( $this->url_helper->recreate_current_url( false ) ) . \ltrim( $new_path, '/' ), 301, $message );
exit;
}
/**
* Removes unneeded query variables from the URL.
*
* @deprecated 20.4
* @codeCoverageIgnore
*
* @return void
*/
public function clean_permalinks() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.4', 'Yoast\WP\SEO\Initializers\Crawl_Cleanup_Permalinks::clean_permalinks()' );
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- We're not processing anything yet...
if ( \is_robots() || \get_query_var( 'sitemap' ) || empty( $_GET ) || \is_user_logged_in() ) {
return;
}
$current_url = $this->url_helper->recreate_current_url();
/**
* Filter: 'Yoast\WP\SEO\allowlist_permalink_vars' - Allows plugins to register their own variables not to clean.
*
* Note: This is a Premium plugin-only hook.
*
* @since 19.2.0
*
* @param array $allowed_extravars The list of the allowed vars (empty by default).
*/
$allowed_extravars = \apply_filters( 'Yoast\WP\SEO\allowlist_permalink_vars', [] );
if ( $this->options_helper->get( 'clean_permalinks_extra_variables' ) !== '' ) {
$allowed_extravars = \array_merge( $allowed_extravars, \explode( ',', $this->options_helper->get( 'clean_permalinks_extra_variables' ) ) );
}
$allowed_query = [];
// @todo parse_str changes spaces in param names into `_`, we should find a better way to support them.
\wp_parse_str( \wp_parse_url( $current_url, \PHP_URL_QUERY ), $query );
if ( ! empty( $allowed_extravars ) ) {
foreach ( $allowed_extravars as $get ) {
$get = \trim( $get );
if ( isset( $query[ $get ] ) ) {
$allowed_query[ $get ] = \rawurlencode_deep( $query[ $get ] );
unset( $query[ $get ] );
}
}
}
// If we had only allowed params, let's just bail out, no further processing needed.
if ( \count( $query ) === 0 ) {
return;
}
global $wp_query;
$proper_url = '';
if ( \is_singular() ) {
global $post;
$proper_url = \get_permalink( $post->ID );
$page = \get_query_var( 'page' );
if ( $page && $page !== 1 ) {
$the_post = \get_post( $post->ID );
$page_count = \substr_count( $the_post->post_content, '<!--nextpage-->' );
$proper_url = \user_trailingslashit( \trailingslashit( $proper_url ) . $page );
if ( $page > ( $page_count + 1 ) ) {
$proper_url = \user_trailingslashit( \trailingslashit( $proper_url ) . ( $page_count + 1 ) );
}
}
// Fix reply to comment links, whoever decided this should be a GET variable?
// phpcs:ignore WordPress.Security -- We know this is scary.
if ( isset( $_SERVER['REQUEST_URI'] ) && \preg_match( '`(\?replytocom=[^&]+)`', \sanitize_text_field( $_SERVER['REQUEST_URI'] ), $matches ) ) {
$proper_url .= \str_replace( '?replytocom=', '#comment-', $matches[0] );
}
unset( $matches );
// Prevent cleaning out posts & page previews for people capable of viewing them.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- We know this is scary.
if ( isset( $_GET['preview'] ) && isset( $_GET['preview_nonce'] ) && \current_user_can( 'edit_post' ) ) {
return;
}
}
elseif ( \is_front_page() ) {
if ( $this->current_page_helper->is_home_posts_page() ) {
$proper_url = \home_url( '/' );
}
elseif ( $this->current_page_helper->is_home_static_page() ) {
$proper_url = \get_permalink( $GLOBALS['post']->ID );
}
}
elseif ( $this->current_page_helper->is_posts_page() ) {
$proper_url = \get_permalink( \get_option( 'page_for_posts' ) );
}
elseif ( \is_category() || \is_tag() || \is_tax() ) {
$term = $wp_query->get_queried_object();
if ( \is_feed() ) {
$proper_url = \get_term_feed_link( $term->term_id, $term->taxonomy );
}
else {
$proper_url = \get_term_link( $term, $term->taxonomy );
}
}
elseif ( \is_search() ) {
$s = \get_search_query();
$proper_url = \get_bloginfo( 'url' ) . '/?s=' . \rawurlencode( $s );
}
elseif ( \is_404() ) {
if ( \is_multisite() && ! \is_subdomain_install() && \is_main_site() ) {
if ( $current_url === \get_bloginfo( 'url' ) . '/blog/' || $current_url === \get_bloginfo( 'url' ) . '/blog' ) {
if ( $this->current_page_helper->is_home_static_page() ) {
$proper_url = \get_permalink( \get_option( 'page_for_posts' ) );
}
else {
$proper_url = \get_bloginfo( 'url' );
}
}
}
}
if ( ! empty( $proper_url ) && $wp_query->query_vars['paged'] !== 0 && $wp_query->post_count !== 0 ) {
if ( \is_search() ) {
$proper_url = \get_bloginfo( 'url' ) . '/page/' . $wp_query->query_vars['paged'] . '/?s=' . \rawurlencode( \get_search_query() );
}
else {
$proper_url = \user_trailingslashit( \trailingslashit( $proper_url ) . 'page/' . $wp_query->query_vars['paged'] );
}
}
$proper_url = \add_query_arg( $allowed_query, $proper_url );
if ( ! empty( $proper_url ) && $current_url !== $proper_url ) {
\header( 'Content-Type: redirect', true );
\header_remove( 'Content-Type' );
\header_remove( 'Last-Modified' );
\header_remove( 'X-Pingback' );
$message = \sprintf(
/* translators: %1$s: Yoast SEO Premium */
\__( '%1$s: unregistered URL parameter removed', 'wordpress-seo-premium' ),
'Yoast SEO Premium'
);
\wp_safe_redirect( $proper_url, 301, $message );
exit;
}
}
}

View File

@@ -0,0 +1,412 @@
<?php
namespace Yoast\WP\SEO\Premium\Integrations\Admin;
use WPSEO_Option;
use WPSEO_Shortlinker;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Presenters\Admin\Alert_Presenter;
use Yoast_Form;
/**
* Crawl_Settings_Integration class
*
* @deprecated 20.4
* @codeCoverageIgnore
*/
class Crawl_Settings_Integration implements Integration_Interface {
/**
* Holds the settings + labels for the head clean up piece.
*
* @var array
*/
private $basic_settings;
/**
* Holds the settings + labels for the feeds clean up.
*
* @var array
*/
private $feed_settings;
/**
* Holds the settings + labels for permalink cleanup settings.
*
* @var array
*/
private $permalink_cleanup_settings;
/**
* Holds the settings + labels for search cleanup settings.
*
* @var array
*/
private $search_cleanup_settings;
/**
* Holds the settings + labels for unused resources settings.
*
* @var array
*/
private $unused_resources_settings;
/**
* The shortlinker.
*
* @var WPSEO_Shortlinker
*/
private $shortlinker;
/**
* The options' helper.
*
* @var Options_Helper
*/
private $options_helper;
/**
* Crawl_Settings_Integration constructor.
*
* @deprecated 20.4
* @codeCoverageIgnore
*
* @param Options_Helper $options_helper The options helper.
* @param WPSEO_Shortlinker $shortlinker The shortlinker.
*/
public function __construct( Options_Helper $options_helper, WPSEO_Shortlinker $shortlinker ) {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.4', 'Yoast\WP\SEO\Integrations\Admin\Crawl_Settings_Integration' );
$this->options_helper = $options_helper;
$this->shortlinker = $shortlinker;
}
/**
* Returns the conditionals based in which this loadable should be active.
*
* In this case: when on an admin page.
*
* @deprecated 20.4
* @codeCoverageIgnore
*/
public static function get_conditionals() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.4', 'Yoast\WP\SEO\Integrations\Admin\Crawl_Settings_Integration::get_conditionals()' );
return [ Admin_Conditional::class ];
}
/**
* Registers an action to add a new tab to the General page.
*
* @deprecated 20.4
* @codeCoverageIgnore
*/
public function register_hooks() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.4', 'Yoast\WP\SEO\Integrations\Admin\Crawl_Settings_Integration::register_hooks()' );
$this->register_setting_labels();
\add_action( 'wpseo_settings_tab_crawl_cleanup_network', [ $this, 'add_crawl_settings_tab_content_network' ] );
}
/**
* Enqueue the workouts app.
*
* @deprecated 20.4
* @codeCoverageIgnore
*/
public function enqueue_assets() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.4', 'Yoast\WP\SEO\Integrations\Admin\Crawl_Settings_Integration::enqueue_assets()' );
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Date is not processed or saved.
if ( ! isset( $_GET['page'] ) || $_GET['page'] !== 'wpseo_dashboard' ) {
return;
}
\wp_enqueue_script( 'wp-seo-premium-crawl-settings' );
}
/**
* Adds content to the Crawl Cleanup tab.
*
* @deprecated 20.4
* @codeCoverageIgnore
*
* @param Yoast_Form $yform The yoast form object.
*/
public function add_crawl_settings_tab_content( $yform ) {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.4' );
$this->add_crawl_settings( $yform, false );
}
/**
* Adds content to the Crawl Cleanup network tab.
*
* @deprecated 20.4
* @codeCoverageIgnore
*
* @param Yoast_Form $yform The yoast form object.
*/
public function add_crawl_settings_tab_content_network( $yform ) {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.4', 'Yoast\WP\SEO\Integrations\Admin\Crawl_Settings_Integration::add_crawl_settings_tab_content_network( $yform )' );
$this->add_crawl_settings( $yform, true );
}
/**
* Connects the settings to their labels.
*
* @return void
*/
private function register_setting_labels() {
$this->feed_settings = [
'remove_feed_global' => \__( 'Global feed', 'wordpress-seo-premium' ),
'remove_feed_global_comments' => \__( 'Global comment feeds', 'wordpress-seo-premium' ),
'remove_feed_post_comments' => \__( 'Post comments feeds', 'wordpress-seo-premium' ),
'remove_feed_authors' => \__( 'Post authors feeds', 'wordpress-seo-premium' ),
'remove_feed_post_types' => \__( 'Post type feeds', 'wordpress-seo-premium' ),
'remove_feed_categories' => \__( 'Category feeds', 'wordpress-seo-premium' ),
'remove_feed_tags' => \__( 'Tag feeds', 'wordpress-seo-premium' ),
'remove_feed_custom_taxonomies' => \__( 'Custom taxonomy feeds', 'wordpress-seo-premium' ),
'remove_feed_search' => \__( 'Search results feeds', 'wordpress-seo-premium' ),
'remove_atom_rdf_feeds' => \__( 'Atom/RDF feeds', 'wordpress-seo-premium' ),
];
$this->basic_settings = [
'remove_shortlinks' => \__( 'Shortlinks', 'wordpress-seo-premium' ),
'remove_rest_api_links' => \__( 'REST API links', 'wordpress-seo-premium' ),
'remove_rsd_wlw_links' => \__( 'RSD / WLW links', 'wordpress-seo-premium' ),
'remove_oembed_links' => \__( 'oEmbed links', 'wordpress-seo-premium' ),
'remove_generator' => \__( 'Generator tag', 'wordpress-seo-premium' ),
'remove_pingback_header' => \__( 'Pingback HTTP header', 'wordpress-seo-premium' ),
'remove_powered_by_header' => \__( 'Powered by HTTP header', 'wordpress-seo-premium' ),
];
$this->permalink_cleanup_settings = [
'clean_campaign_tracking_urls' => \__( 'Campaign tracking URL parameters', 'wordpress-seo-premium' ),
'clean_permalinks' => \__( 'Unregistered URL parameters', 'wordpress-seo-premium' ),
];
$this->search_cleanup_settings = [
'search_cleanup' => \__( 'Filter search terms', 'wordpress-seo-premium' ),
'search_cleanup_emoji' => \__( 'Filter searches with emojis and other special characters', 'wordpress-seo-premium' ),
'search_cleanup_patterns' => \__( 'Filter searches with common spam patterns', 'wordpress-seo-premium' ),
'deny_search_crawling' => \__( 'Prevent search engines from crawling site search URLs', 'wordpress-seo-premium' ),
'redirect_search_pretty_urls' => \__( 'Redirect pretty URLs for search pages to raw format', 'wordpress-seo-premium' ),
];
$this->unused_resources_settings = [
'remove_emoji_scripts' => \__( 'Emoji scripts', 'wordpress-seo-premium' ),
'deny_wp_json_crawling' => \__( 'Prevent search engines from crawling /wp-json/', 'wordpress-seo-premium' ),
];
}
/**
* Print the settings sections.
*
* @param Yoast_Form $yform The Yoast form class.
* @param bool $is_network Whether we're on the network site.
*
* @return void
*/
private function add_crawl_settings( $yform, $is_network ) {
$this->print_toggles( $this->basic_settings, $yform, $is_network, \__( 'Basic crawl settings', 'wordpress-seo-premium' ), \__( 'Remove links added by WordPress to the header and &lt;head&gt;.', 'wordpress-seo-premium' ) );
$this->print_toggles( $this->feed_settings, $yform, $is_network, \__( 'Feed crawl settings', 'wordpress-seo-premium' ), \__( "Remove feed links added by WordPress that aren't needed for this site.", 'wordpress-seo-premium' ) );
$this->print_toggles( $this->unused_resources_settings, $yform, $is_network, \__( 'Remove unused resources', 'wordpress-seo-premium' ), \__( 'WordPress loads lots of resources, some of which your site might not need. If youre not using these, removing them can speed up your pages and save resources.', 'wordpress-seo-premium' ) );
$first_search_setting = \array_slice( $this->search_cleanup_settings, 0, 1 );
$rest_search_settings = \array_slice( $this->search_cleanup_settings, 1 );
$search_settings_toggles = [
'off' => \__( 'Disabled', 'wordpress-seo-premium' ),
'on' => \__( 'Enabled', 'wordpress-seo-premium' ),
];
$this->print_toggles( $first_search_setting, $yform, $is_network, \__( 'Search cleanup settings', 'wordpress-seo-premium' ), \__( 'Clean up and filter searches to prevent search spam.', 'wordpress-seo-premium' ), $search_settings_toggles );
if ( ! $is_network ) {
echo '<div id="search_character_limit_container" class="yoast-crawl-single-setting">';
$yform->number(
'search_character_limit',
\__( 'Max number of characters to allow in searches', 'wordpress-seo-premium' ),
[
'min' => 1,
'max' => 1000,
]
);
echo '</div>';
}
$this->print_toggles( $rest_search_settings, $yform, $is_network, '', '', $search_settings_toggles );
$permalink_warning = \sprintf(
/* Translators: %1$s expands to an opening anchor tag for a link leading to the Yoast SEO page of the Permalink Cleanup features, %2$s expands to a closing anchor tag. */
\esc_html__(
'These are expert features, so make sure you know what you\'re doing before removing the parameters. %1$sRead more about how your site can be affected%2$s.',
'wordpress-seo-premium'
),
'<a href="' . \esc_url( $this->shortlinker->build_shortlink( 'https://yoa.st/permalink-cleanup' ) ) . '" target="_blank" rel="noopener noreferrer">',
'</a>'
);
$this->print_toggles( $this->permalink_cleanup_settings, $yform, $is_network, \__( 'Permalink cleanup settings', 'wordpress-seo-premium' ), \__( 'Remove unwanted URL parameters from your URLs.', 'wordpress-seo-premium' ), [], $permalink_warning );
if ( ! $is_network && ! empty( \get_option( 'permalink_structure' ) ) ) {
echo '<div id="clean_permalinks_extra_variables_container" class="yoast-crawl-single-setting">';
$yform->textinput( 'clean_permalinks_extra_variables', \__( 'Additional URL parameters to allow', 'wordpress-seo-premium' ) );
echo '<p class="desc label yoast-extra-variables-label">';
\esc_html_e( 'Please use a comma to separate multiple URL parameters.', 'wordpress-seo-premium' );
echo '</p>';
echo '</div>';
}
else {
// Also add the original option as hidden, so as not to lose any values if it's disabled and the form is saved.
$yform->hidden( 'clean_permalinks_extra_variables', 'clean_permalinks_extra_variables' );
}
}
/**
* Prints a list of toggles for an array of settings with labels.
*
* @param array $settings The settings being displayed.
* @param Yoast_Form $yform The Yoast form class.
* @param bool $is_network Whether we're on the network site.
* @param string $title Optional title for the settings being displayed.
* @param string $description Optional description of the settings being displayed.
* @param array $toggles Optional naming of the toggle buttons.
* @param string $warning Optional warning to be displayed above the toggles.
*
* @return void
*/
private function print_toggles( array $settings, Yoast_Form $yform, $is_network = false, $title = '', $description = '', $toggles = [], $warning = '' ) {
if ( ! empty( $title ) ) {
echo '<h3 class="yoast-crawl-settings">', \esc_html( $title ), '</h3>';
}
if ( ! $is_network && ! empty( $description ) ) {
echo '<p class="yoast-crawl-settings-explanation">', \esc_html( $description ), '</p>';
}
if ( ! empty( $warning ) ) {
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output escaped in Alert_Presenter.
echo new Alert_Presenter( $warning, 'warning' );
}
if ( empty( $toggles ) ) {
$toggles = [
'off' => \__( 'Keep', 'wordpress-seo-premium' ),
'on' => \__( 'Remove', 'wordpress-seo-premium' ),
];
}
$setting_prefix = '';
if ( $is_network ) {
$setting_prefix = WPSEO_Option::ALLOW_KEY_PREFIX;
$toggles = [
// phpcs:ignore WordPress.WP.I18n.TextDomainMismatch -- Reason: text is originally from Yoast SEO.
'on' => \__( 'Allow Control', 'wordpress-seo' ),
// phpcs:ignore WordPress.WP.I18n.TextDomainMismatch -- Reason: text is originally from Yoast SEO.
'off' => \__( 'Disable', 'wordpress-seo' ),
];
}
foreach ( $settings as $setting => $label ) {
$attr = [];
$variable = $setting_prefix . $setting;
if ( $this->should_feature_be_disabled_permalink( $setting, $is_network ) ) {
$attr = [
'disabled' => true,
];
$variable = $setting_prefix . $setting . '_disabled';
// Also add the original option as hidden, so as not to lose any values if it's disabled and the form is saved.
$yform->hidden( $setting_prefix . $setting, $setting_prefix . $setting );
}
elseif ( $this->should_feature_be_disabled_multisite( $setting ) ) {
$attr = [
'disabled' => true,
'preserve_disabled_value' => false,
];
}
$yform->toggle_switch(
$variable,
$toggles,
$label,
'',
$attr
);
if ( $setting === 'remove_feed_global_comments' && ! $is_network ) {
echo '<p class="yoast-crawl-settings-help">';
echo \esc_html__( 'By removing Global comments feed, Post comments feeds will be removed too.', 'wordpress-seo-premium' );
echo '</p>';
}
if ( $this->should_feature_be_disabled_permalink( $setting, $is_network ) ) {
echo '<p class="yoast-crawl-settings-help">';
if ( \current_user_can( 'manage_options' ) ) {
echo \sprintf(
/* translators: 1: Link start tag to the Permalinks settings page, 2: Link closing tag. */
\esc_html__( 'This feature is disabled when your site is not using %1$spretty permalinks%2$s.', 'wordpress-seo-premium' ),
'<a href="' . \esc_url( \admin_url( 'options-permalink.php' ) ) . '">',
'</a>'
);
}
else {
echo \esc_html__( 'This feature is disabled when your site is not using pretty permalinks.', 'wordpress-seo-premium' );
}
echo '</p>';
}
elseif ( $this->should_feature_be_disabled_multisite( $setting ) ) {
echo '<p>';
\esc_html_e( 'This feature is not available for multisites.', 'wordpress-seo-premium' );
echo '</p>';
}
}
}
/**
* Checks if the feature should be disabled due to non-pretty permalinks.
*
* @param string $setting The setting to be displayed.
* @param bool $is_network Whether we're on the network site.
*
* @return bool
*/
protected function should_feature_be_disabled_permalink( $setting, $is_network ) {
return (
\in_array( $setting, [ 'clean_permalinks', 'clean_campaign_tracking_urls' ], true )
&& ! $is_network
&& empty( \get_option( 'permalink_structure' ) )
&& ! $this->is_control_disabled( $setting )
);
}
/**
* Checks if the feature should be disabled due to the site being a multisite.
*
* @param string $setting The setting to be displayed.
*
* @return bool
*/
protected function should_feature_be_disabled_multisite( $setting ) {
return (
\in_array( $setting, [ 'deny_search_crawling', 'deny_wp_json_crawling' ], true )
&& \is_multisite()
);
}
/**
* Checks whether a given control should be disabled, because of the network admin.
*
* @param string $variable The variable within the option to check whether its control should be disabled.
*
* @return bool True if control should be disabled, false otherwise.
*/
protected function is_control_disabled( $variable ) {
return ! $this->options_helper->get( 'allow_' . $variable, true );
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace Yoast\WP\SEO\Premium\Integrations\Admin;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
/**
* Integrations_Page class
*
* @deprecated 20.7
* @codeCoverageIgnore
*/
class Integrations_Page implements Integration_Interface {
/**
* The options helper.
*
* @var Options_Helper
*/
private $options_helper;
/**
* {@inheritDoc}
*
* @deprecated 20.7
* @codeCoverageIgnore
*/
public static function get_conditionals() {
\deprecated_function( __METHOD__, 'Yoast SEO Premium 20.7' );
return [ Admin_Conditional::class ];
}
/**
* Workouts_Integration constructor.
*
* @deprecated 20.7
* @codeCoverageIgnore
*
* @param Options_Helper $options_helper The options helper.
*/
public function __construct( Options_Helper $options_helper ) {
\deprecated_function( __METHOD__, 'Yoast SEO Premium 20.7' );
$this->options_helper = $options_helper;
}
/**
* {@inheritDoc}
*
* @deprecated 20.7
* @codeCoverageIgnore
*/
public function register_hooks() {
\deprecated_function( __METHOD__, 'Yoast SEO Premium 20.7' );
}
/**
* Enqueue the workouts app.
*
* @deprecated 20.7
* @codeCoverageIgnore
*/
public function enqueue_assets() {
\deprecated_function( __METHOD__, 'Yoast SEO Premium 20.7' );
}
}

View File

@@ -0,0 +1,190 @@
<?php
namespace Yoast\WP\SEO\Premium\Integrations\Admin;
use WPSEO_Admin_Asset_Manager;
use WPSEO_Options;
use WPSEO_Shortlinker;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Helpers\Capability_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Premium\Conditionals\Zapier_Enabled_Conditional;
use Yoast\WP\SEO\Presenters\Admin\Notice_Presenter;
/**
* Shows a notification telling the user that zapier integration will be removed.
*
* @deprecated 20.7
* @codeCoverageIgnore
*/
class Zapier_Notification_Integration implements Integration_Interface {
/**
* Holds the name of the user meta key.
*
* The value of this database field holds whether the user has dismissed this notice or not.
*
* @var string
*/
const USER_META_DISMISSED = 'is_dismissed_zapier_notice';
/**
* The capability helper.
*
* @var Capability_Helper
*/
private $capability_helper;
/**
* The admin asset manager.
*
* @var WPSEO_Admin_Asset_Manager
*/
private $admin_asset_manager;
/**
* The admin asset manager.
*
* @var Zapier_Enabled_Conditional
*/
private $zapier_enable_conditional;
/**
* {@inheritDoc}
*
* @deprecated 20.7
* @codeCoverageIgnore
*/
public static function get_conditionals() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.7' );
return [ Admin_Conditional::class ];
}
/**
* Zapier_Notification_Integration constructor.
*
* @deprecated 20.7
* @codeCoverageIgnore
*
* @param WPSEO_Admin_Asset_Manager $admin_asset_manager The admin asset manager.
* @param Capability_Helper $capability_helper The capability helper.
* @param Zapier_Enabled_Conditional $zapier_enable_conditional The capability helper.
*/
public function __construct(
WPSEO_Admin_Asset_Manager $admin_asset_manager,
Capability_Helper $capability_helper,
Zapier_Enabled_Conditional $zapier_enable_conditional
) {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.7' );
$this->admin_asset_manager = $admin_asset_manager;
$this->capability_helper = $capability_helper;
$this->zapier_enable_conditional = $zapier_enable_conditional;
}
/**
* {@inheritDoc}
*
* @deprecated 20.7
* @codeCoverageIgnore
*/
public function register_hooks() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.7' );
\add_action( 'admin_notices', [ $this, 'zapier_notice' ] );
\add_action( 'wp_ajax_dismiss_zapier_notice', [ $this, 'dismiss_zapier_notice' ] );
}
/**
* Shows a notice if zapier is enabled and it's not being dismissed before.
*
* @deprecated 20.7
* @codeCoverageIgnore
*
* @return void
*/
public function zapier_notice() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.7' );
if ( ! $this->capability_helper->current_user_can( 'wpseo_manage_options' ) ) {
return;
}
if ( $this->is_notice_dismissed() ) {
return;
}
$is_zapier_connected = WPSEO_Options::get( 'zapier_subscription', [] );
if ( $is_zapier_connected ) {
$this->admin_asset_manager->enqueue_style( 'monorepo' );
/* translators: %1$s for Yoast SEO */
$title = \sprintf( \__( 'Zapier integration will be removed from %1$s', 'wordpress-seo-premium' ), 'Yoast SEO' );
$content = \sprintf(
/* translators: %1$s and %2$s expands to the link to https://yoast.com/features/zapier, %3$s for Yoast SEO, %4$s for support email. */
\esc_html__( 'The %1$sZapier integration%2$s (on the Integrations page) will be removed from %3$s in 20.7 (release date May 9th). If you have any questions, please reach out to %4$s.', 'wordpress-seo-premium' ),
'<a href="' . WPSEO_Shortlinker::get( 'http://yoa.st/zapier-removal-notification' ) . '" target="_blank">',
'</a>',
'Yoast SEO',
'support@yoast.com'
);
// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped -- Output of the title escaped in the Notice_Presenter.
echo new Notice_Presenter(
$title,
$content,
null,
null,
true,
'yoast-zapier-notice'
);
// phpcs:enable
// Enable permanently dismissing the notice.
echo '<script>
jQuery( document ).ready( function() {
jQuery( "body" ).on( "click", "#yoast-zapier-notice .notice-dismiss", function() {
const data = { "action": "dismiss_zapier_notice", "nonce": "' . \esc_attr( \wp_create_nonce( 'dismiss_zapier_notice' ) ) . '" };
jQuery.post( ajaxurl, data, function( response ) {
jQuery( this ).parent( "#yoast-zapier-notice" ).hide();
});
} );
} );
</script>';
}
}
/**
* Was the notice dismissed by the user.
*
* @deprecated 20.7
* @codeCoverageIgnore
*
* @return bool
*/
protected function is_notice_dismissed() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.7' );
return \get_user_meta( \get_current_user_id(), self::USER_META_DISMISSED, true ) === '1';
}
/**
* Dismisses the notice.
*
* @deprecated 20.7
* @codeCoverageIgnore
*
* @return bool
*/
public function dismiss_zapier_notice() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.7' );
if ( ! \check_ajax_referer( 'dismiss_zapier_notice', 'nonce', false ) || ! $this->capability_helper->current_user_can( 'wpseo_manage_options' ) ) {
return;
}
\update_user_meta( \get_current_user_id(), self::USER_META_DISMISSED, true );
return WPSEO_Options::set( 'is_dismissed_zapier_notice', true );
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace Yoast\WP\SEO\Integrations\Blocks;
use Yoast\WP\SEO\Conditionals\No_Conditionals;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Schema_Templates\Block_Patterns\Block_Pattern;
/**
* Registers the block patterns needed for the Premium Schema blocks.
*
* @deprecated 20.5
* @codeCoverageIgnore
*/
class Block_Patterns implements Integration_Interface {
use No_Conditionals;
/**
* The block patterns to register.
*
* @var Block_Pattern[]
*/
protected $block_patterns = [];
/**
* Block_Patterns integration constructor.
*
* @deprecated 20.5
* @codeCoverageIgnore
*
* @param Block_Pattern ...$block_patterns The block patterns to register.
*/
public function __construct( Block_Pattern ...$block_patterns ) {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.5' );
$this->block_patterns = $block_patterns;
}
/**
* Initializes the integration.
*
* This is the place to register hooks and filters.
*
* @deprecated 20.5
* @codeCoverageIgnore
*
* @return void
*/
public function register_hooks() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.5' );
}
/**
* Registers the block patterns with WordPress.
*
* @deprecated 20.5
* @codeCoverageIgnore
*
* @return void
*/
public function register_block_patterns() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.5' );
}
/**
* Registers the block pattern category with WordPress.
*
* @deprecated 20.5
* @codeCoverageIgnore
*
* @return void
*/
public function register_block_pattern_category() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.5' );
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace Yoast\WP\SEO\Integrations\Blocks;
use Yoast\WP\SEO\Conditionals\No_Conditionals;
use Yoast\WP\SEO\Helpers\Wordpress_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
/**
* Adds the block categories for the Jobs Posting block.
*
* @deprecated 20.5
* @codeCoverageIgnore
*/
class Job_Posting_Block implements Integration_Interface {
use No_Conditionals;
/**
* Represents the WordPress helper.
*
* @var Wordpress_Helper
*/
protected $wordpress_helper;
/**
* Job_Posting_Block constructor.
*
* @deprecated 20.5
* @codeCoverageIgnore
*
* @param Wordpress_Helper $wordpress_helper The WordPress helper.
*/
public function __construct( Wordpress_Helper $wordpress_helper ) {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.5' );
$this->wordpress_helper = $wordpress_helper;
}
/**
* Registers the hooks.
*
* @deprecated 20.5
* @codeCoverageIgnore
*/
public function register_hooks() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.5' );
}
/**
* Adds Yoast block categories.
*
* @deprecated 20.5
* @codeCoverageIgnore
*
* @param array $categories The categories to filter.
*
* @return array The filtered categories.
*/
public function add_block_categories( $categories ) {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.5' );
return $categories;
}
}

View File

@@ -0,0 +1,70 @@
<?php
namespace Yoast\WP\SEO\Premium\Integrations\Blocks;
use WPSEO_Admin_Asset_Manager;
use Yoast\WP\SEO\Conditionals\No_Conditionals;
use Yoast\WP\SEO\Integrations\Integration_Interface;
/**
* Loads the Premium schema block templates into Gutenberg.
*
* @deprecated 20.5
* @codeCoverageIgnore
*/
class Schema_Blocks implements Integration_Interface {
use No_Conditionals;
/**
* Schema_Blocks constructor.
*
* @deprecated 20.5
* @codeCoverageIgnore
*
* @param WPSEO_Admin_Asset_Manager $asset_manager The asset manager.
*/
public function __construct( WPSEO_Admin_Asset_Manager $asset_manager ) {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.5' );
}
/**
* Initializes the integration.
*
* This is the place to register hooks and filters.
*
* @deprecated 20.5
* @codeCoverageIgnore
*
* @return void
*/
public function register_hooks() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.5' );
}
/**
* Collects the Premium structured data blocks templates.
*
* @deprecated 20.5
* @codeCoverageIgnore
*
* @param array $templates The templates from Yoast SEO.
*
* @return array All the templates that should be loaded.
*/
public function add_premium_templates( $templates ) {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.5' );
return [];
}
/**
* Enqueues the schema blocks css file.
*
* @deprecated 20.5
* @codeCoverageIgnore
*/
public function enqueue_assets() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.5' );
}
}

View File

@@ -0,0 +1,159 @@
<?php
namespace Yoast\WP\SEO\Premium\Integrations\Front_End;
use Yoast\WP\SEO\Conditionals\Front_End_Conditional;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
/**
* Class Crawl_Cleanup_Basic.
*
* @deprecated 20.4
* @codeCoverageIgnore
*/
class Crawl_Cleanup_Basic implements Integration_Interface {
/**
* The options helper.
*
* @var Options_Helper
*/
private $options_helper;
/**
* Crawl Cleanup Basic integration constructor.
*
* @deprecated 20.4
* @codeCoverageIgnore
*
* @param Options_Helper $options_helper The option helper.
*/
public function __construct( Options_Helper $options_helper ) {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.4', 'Yoast\WP\SEO\Integrations\Front_End\Crawl_Cleanup_Basic' );
$this->options_helper = $options_helper;
}
/**
* Initializes the integration.
*
* This is the place to register hooks and filters.
*
* @deprecated 20.4
* @codeCoverageIgnore
*
* @return void
*/
public function register_hooks() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.4', 'Yoast\WP\SEO\Integrations\Front_End\Crawl_Cleanup_Basic::register_hooks()' );
// Remove HTTP headers we don't want.
\add_action( 'wp', [ $this, 'clean_headers' ], 0 );
if ( $this->is_true( 'remove_shortlinks' ) ) {
// Remove shortlinks.
\remove_action( 'wp_head', 'wp_shortlink_wp_head' );
\remove_action( 'template_redirect', 'wp_shortlink_header', 11 );
}
if ( $this->is_true( 'remove_rest_api_links' ) ) {
// Remove REST API links.
\remove_action( 'wp_head', 'rest_output_link_wp_head' );
\remove_action( 'template_redirect', 'rest_output_link_header', 11 );
}
if ( $this->is_true( 'remove_rsd_wlw_links' ) ) {
// Remove RSD and WLW Manifest links.
\remove_action( 'wp_head', 'rsd_link' );
\remove_action( 'xmlrpc_rsd_apis', 'rest_output_rsd' );
\remove_action( 'wp_head', 'wlwmanifest_link' );
}
if ( $this->is_true( 'remove_oembed_links' ) ) {
// Remove JSON+XML oEmbed links.
\remove_action( 'wp_head', 'wp_oembed_add_discovery_links' );
}
if ( $this->is_true( 'remove_generator' ) ) {
\remove_action( 'wp_head', 'wp_generator' );
}
if ( $this->is_true( 'remove_emoji_scripts' ) ) {
// Remove emoji scripts and additional stuff they cause.
\remove_action( 'wp_head', 'print_emoji_detection_script', 7 );
\remove_action( 'wp_print_styles', 'print_emoji_styles' );
\remove_action( 'admin_print_scripts', 'print_emoji_detection_script' );
\remove_action( 'admin_print_styles', 'print_emoji_styles' );
\add_filter( 'wp_resource_hints', [ $this, 'resource_hints_plain_cleanup' ], 1 );
}
}
/**
* Returns the conditionals based in which this loadable should be active.
*
* @deprecated 20.4
* @codeCoverageIgnore
*
* @return array The array of conditionals.
*/
public static function get_conditionals() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.4', 'Yoast\WP\SEO\Integrations\Front_End\Crawl_Cleanup_Basic::get_conditionals()' );
return [ Front_End_Conditional::class ];
}
/**
* Removes X-Pingback and X-Powered-By headers as they're unneeded.
*
* @deprecated 20.4
* @codeCoverageIgnore
*/
public function clean_headers() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.4', 'Yoast\WP\SEO\Integrations\Front_End\Crawl_Cleanup_Basic::clean_headers()' );
if ( \headers_sent() ) {
return;
}
if ( $this->is_true( 'remove_powered_by_header' ) ) {
\header_remove( 'X-Powered-By' );
}
if ( $this->is_true( 'remove_pingback_header' ) ) {
\header_remove( 'X-Pingback' );
}
}
/**
* Remove the core s.w.org hint as it's only used for emoji stuff we don't use.
*
* @deprecated 20.4
* @codeCoverageIgnore
*
* @param array $hints The hints we're adding to.
*
* @return array
*/
public function resource_hints_plain_cleanup( $hints ) {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.4', 'Yoast\WP\SEO\Integrations\Front_End\Crawl_Cleanup_Basic::resource_hints_plain_cleanup( $hints )' );
foreach ( $hints as $key => $hint ) {
if ( \strpos( $hint, '//s.w.org' ) !== false ) {
unset( $hints[ $key ] );
}
}
return $hints;
}
/**
* Checks if the value of an option is set to true.
*
* @param string $option_name The option name.
*
* @return bool
*/
private function is_true( $option_name ) {
return $this->options_helper->get( $option_name ) === true;
}
}

View File

@@ -0,0 +1,223 @@
<?php
namespace Yoast\WP\SEO\Premium\Integrations\Front_End;
use Yoast\WP\SEO\Conditionals\Front_End_Conditional;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
/**
* Adds actions that cleanup unwanted rss feed links.
*
* @deprecated 20.4
* @codeCoverageIgnore
*/
class Crawl_Cleanup_Rss implements Integration_Interface {
/**
* The options helper.
*
* @var Options_Helper
*/
private $options_helper;
/**
* Crawl Cleanup RSS integration constructor.
*
* @deprecated 20.4
* @codeCoverageIgnore
*
* @param Options_Helper $options_helper The option helper.
*/
public function __construct( Options_Helper $options_helper ) {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.4', 'Yoast\WP\SEO\Integrations\Front_End\Crawl_Cleanup_Rss' );
$this->options_helper = $options_helper;
}
/**
* Returns the conditionals based on which this loadable should be active.
*
* @deprecated 20.4
* @codeCoverageIgnore
*
* @return array The conditionals.
*/
public static function get_conditionals() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.4', 'Yoast\WP\SEO\Integrations\Front_End\Crawl_Cleanup_Rss::get_conditionals()' );
return [ Front_End_Conditional::class ];
}
/**
* Register our RSS related hooks.
*
* @deprecated 20.4
* @codeCoverageIgnore
*/
public function register_hooks() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.4', 'Yoast\WP\SEO\Integrations\Front_End\Crawl_Cleanup_Rss::register_hooks()' );
if ( $this->is_true( 'remove_feed_global' ) ) {
\add_action( 'feed_links_show_posts_feed', '__return_false' );
}
if ( $this->is_true( 'remove_feed_global_comments' ) ) {
\add_action( 'feed_links_show_comments_feed', '__return_false' );
}
\add_action( 'wp', [ $this, 'maybe_disable_feeds' ] );
\add_action( 'wp', [ $this, 'maybe_redirect_feeds' ], -10000 );
}
/**
* Disable feeds on selected cases.
*
* @deprecated 20.4
* @codeCoverageIgnore
*/
public function maybe_disable_feeds() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.4', 'Yoast\WP\SEO\Integrations\Front_End\Crawl_Cleanup_Rss::maybe_disable_feeds()' );
if ( \is_singular() && $this->is_true( 'remove_feed_post_comments' )
|| ( \is_author() && $this->is_true( 'remove_feed_authors' ) )
|| ( \is_category() && $this->is_true( 'remove_feed_categories' ) )
|| ( \is_tag() && $this->is_true( 'remove_feed_tags' ) )
|| ( \is_tax() && $this->is_true( 'remove_feed_custom_taxonomies' ) )
|| ( \is_post_type_archive() && $this->is_true( 'remove_feed_post_types' ) )
|| ( \is_search() && $this->is_true( 'remove_feed_search' ) ) ) {
\remove_action( 'wp_head', 'feed_links_extra', 3 );
}
}
/**
* Redirect feeds we don't want away.
*
* @deprecated 20.4
* @codeCoverageIgnore
*/
public function maybe_redirect_feeds() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.4', 'Yoast\WP\SEO\Integrations\Front_End\Crawl_Cleanup_Rss::maybe_redirect_feeds()' );
global $wp_query;
if ( ! \is_feed() ) {
return;
}
if ( \in_array( \get_query_var( 'feed' ), [ 'atom', 'rdf' ], true ) && $this->is_true( 'remove_atom_rdf_feeds' ) ) {
$this->redirect_feed( \home_url(), 'We disable Atom/RDF feeds for performance reasons.' );
}
// Only if we're on the global feed, the query is _just_ `'feed' => 'feed'`, hence this check.
if ( ( $wp_query->query === [ 'feed' => 'feed' ]
|| $wp_query->query === [ 'feed' => 'atom' ]
|| $wp_query->query === [ 'feed' => 'rdf' ] )
&& $this->is_true( 'remove_feed_global' ) ) {
$this->redirect_feed( \home_url(), 'We disable the RSS feed for performance reasons.' );
}
if ( \is_comment_feed() && ! ( \is_singular() || \is_attachment() ) && $this->is_true( 'remove_feed_global_comments' ) ) {
$this->redirect_feed( \home_url(), 'We disable comment feeds for performance reasons.' );
}
elseif ( \is_comment_feed()
&& \is_singular()
&& ( $this->is_true( 'remove_feed_post_comments' ) || $this->is_true( 'remove_feed_global_comments' ) ) ) {
$url = \get_permalink( \get_queried_object() );
$this->redirect_feed( $url, 'We disable post comment feeds for performance reasons.' );
}
if ( \is_author() && $this->is_true( 'remove_feed_authors' ) ) {
$author_id = (int) \get_query_var( 'author' );
$url = \get_author_posts_url( $author_id );
$this->redirect_feed( $url, 'We disable author feeds for performance reasons.' );
}
if ( ( \is_category() && $this->is_true( 'remove_feed_categories' ) )
|| ( \is_tag() && $this->is_true( 'remove_feed_tags' ) )
|| ( \is_tax() && $this->is_true( 'remove_feed_custom_taxonomies' ) ) ) {
$term = \get_queried_object();
$url = \get_term_link( $term, $term->taxonomy );
if ( \is_wp_error( $url ) ) {
$url = \home_url();
}
$this->redirect_feed( $url, 'We disable taxonomy feeds for performance reasons.' );
}
if ( ( \is_post_type_archive() ) && $this->is_true( 'remove_feed_post_types' ) ) {
$url = \get_post_type_archive_link( $this->get_queried_post_type() );
$this->redirect_feed( $url, 'We disable post type feeds for performance reasons.' );
}
if ( \is_search() && $this->is_true( 'remove_feed_search' ) ) {
$url = \trailingslashit( \home_url() ) . '?s=' . \get_search_query();
$this->redirect_feed( $url, 'We disable search RSS feeds for performance reasons.' );
}
}
/**
* Sends a cache control header.
*
* @deprecated 20.4
* @codeCoverageIgnore
*
* @param int $expiration The expiration time.
*/
public function cache_control_header( $expiration ) {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.4', 'Yoast\WP\SEO\Integrations\Front_End\Crawl_Cleanup_Rss::cache_control_header( $expiration )' );
\header_remove( 'Expires' );
// The cacheability of the current request. 'public' allows caching, 'private' would not allow caching by proxies like CloudFlare.
$cacheability = 'public';
$format = '%1$s, max-age=%2$d, s-maxage=%2$d, stale-while-revalidate=120, stale-if-error=14400';
if ( \is_user_logged_in() ) {
$expiration = 0;
$cacheability = 'private';
$format = '%1$s, max-age=%2$d';
}
\header( \sprintf( 'Cache-Control: ' . $format, $cacheability, $expiration ), true );
}
/**
* Redirect a feed result to somewhere else.
*
* @param string $url The location we're redirecting to.
* @param string $reason The reason we're redirecting.
*/
private function redirect_feed( $url, $reason ) {
\header_remove( 'Content-Type' );
\header_remove( 'Last-Modified' );
$this->cache_control_header( 7 * \DAY_IN_SECONDS );
\wp_safe_redirect( $url, 301, 'Yoast SEO: ' . $reason );
exit;
}
/**
* Retrieves the queried post type.
*
* @return string The queried post type.
*/
private function get_queried_post_type() {
$post_type = \get_query_var( 'post_type' );
if ( \is_array( $post_type ) ) {
$post_type = \reset( $post_type );
}
return $post_type;
}
/**
* Checks if the value of an option is set to true.
*
* @param string $option_name The option name.
*
* @return bool
*/
private function is_true( $option_name ) {
return $this->options_helper->get( $option_name ) === true;
}
}

View File

@@ -0,0 +1,232 @@
<?php
namespace Yoast\WP\SEO\Premium\Integrations\Front_End;
use WP_Query;
use Yoast\WP\SEO\Conditionals\Front_End_Conditional;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Redirect_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
/**
* Class Crawl_Cleanup_Searches.
*
* @deprecated 20.4
* @codeCoverageIgnore
*/
class Crawl_Cleanup_Searches implements Integration_Interface {
/**
* Patterns to match against to find spam.
*
* @var array
*/
private $patterns = [
'/[:()【】[]]+/u',
'/(TALK|QQ)\:/iu',
];
/**
* The options helper.
*
* @var Options_Helper
*/
private $options_helper;
/**
* The redirect helper.
*
* @var Redirect_Helper
*/
private $redirect_helper;
/**
* Crawl_Cleanup_Searches integration constructor.
*
* @deprecated 20.4
* @codeCoverageIgnore
*
* @param Options_Helper $options_helper The option helper.
* @param Redirect_Helper $redirect_helper The redirect helper.
*/
public function __construct( Options_Helper $options_helper, Redirect_Helper $redirect_helper ) {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.4', 'Yoast\WP\SEO\Integrations\Front_End\Crawl_Cleanup_Searches' );
$this->options_helper = $options_helper;
$this->redirect_helper = $redirect_helper;
}
/**
* Initializes the integration.
*
* This is the place to register hooks and filters.
*
* @deprecated 20.4
* @codeCoverageIgnore
*
* @return void
*/
public function register_hooks() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.4', 'Yoast\WP\SEO\Integrations\Front_End\Crawl_Cleanup_Searches::register_hooks()' );
if ( $this->options_helper->get( 'search_cleanup' ) ) {
\add_filter( 'pre_get_posts', [ $this, 'validate_search' ] );
}
if ( $this->options_helper->get( 'redirect_search_pretty_urls' ) && ! empty( \get_option( 'permalink_structure' ) ) ) {
\add_action( 'template_redirect', [ $this, 'maybe_redirect_searches' ], 2 );
}
}
/**
* Returns the conditionals based in which this loadable should be active.
*
* @deprecated 20.4
* @codeCoverageIgnore
*
* @return array The array of conditionals.
*/
public static function get_conditionals() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.4', 'Yoast\WP\SEO\Integrations\Front_End\Crawl_Cleanup_Searches::get_conditionals()' );
return [ Front_End_Conditional::class ];
}
/**
* Check if we want to allow this search to happen.
*
* @deprecated 20.4
* @codeCoverageIgnore
*
* @param WP_Query $query The main query.
*
* @return WP_Query
*/
public function validate_search( WP_Query $query ) {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.4', 'Yoast\WP\SEO\Integrations\Front_End\Crawl_Cleanup_Searches::validate_search( WP_Query $query )' );
if ( ! $query->is_search() ) {
return $query;
}
// First check against emoji and patterns we might not want.
$this->check_unwanted_patterns( $query );
// Then limit characters if still needed.
$this->limit_characters();
return $query;
}
/**
* Redirect pretty search URLs to the "raw" equivalent
*
* @deprecated 20.4
* @codeCoverageIgnore
*/
public function maybe_redirect_searches() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.4', 'Yoast\WP\SEO\Integrations\Front_End\Crawl_Cleanup_Searches::maybe_redirect_searches()' );
if ( ! \is_search() ) {
return;
}
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput
if ( isset( $_SERVER['REQUEST_URI'] ) && \stripos( $_SERVER['REQUEST_URI'], '/search/' ) === 0 ) {
$args = [];
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput
$parsed = \wp_parse_url( $_SERVER['REQUEST_URI'] );
if ( ! empty( $parsed['query'] ) ) {
\wp_parse_str( $parsed['query'], $args );
}
$args['s'] = \get_search_query();
$proper_url = \home_url( '/' );
if ( \intval( \get_query_var( 'paged' ) ) > 1 ) {
$proper_url .= \sprintf( 'page/%s/', \get_query_var( 'paged' ) );
unset( $args['paged'] );
}
$proper_url = \add_query_arg( \array_map( 'rawurlencode_deep', $args ), $proper_url );
if ( ! empty( $parsed['fragment'] ) ) {
$proper_url .= '#' . \rawurlencode( $parsed['fragment'] );
}
$this->redirect_away( 'We redirect pretty URLs to the raw format.', $proper_url );
}
}
/**
* Check query against unwanted search patterns.
*
* @param WP_Query $query The main WordPress query.
*
* @return void
*/
private function check_unwanted_patterns( WP_Query $query ) {
$s = \rawurldecode( $query->query_vars['s'] );
if ( $this->options_helper->get( 'search_cleanup_emoji' ) && $this->has_emoji( $s ) ) {
$this->redirect_away( 'We don\'t allow searches with emojis and other special characters.' );
}
if ( ! $this->options_helper->get( 'search_cleanup_patterns' ) ) {
return;
}
foreach ( $this->patterns as $pattern ) {
$outcome = \preg_match( $pattern, $s, $matches );
if ( $outcome && $matches !== [] ) {
$this->redirect_away( 'Your search matched a common spam pattern.' );
}
}
}
/**
* Redirect to the homepage for invalid searches.
*
* @param string $reason The reason for redirecting away.
* @param string $to_url The URL to redirect to.
*
* @return void
*/
private function redirect_away( $reason, $to_url = '' ) {
if ( empty( $to_url ) ) {
$to_url = \get_home_url();
}
$this->redirect_helper->do_safe_redirect( $to_url, 301, 'Yoast Search Filtering: ' . $reason );
}
/**
* Limits the number of characters in the search query.
*
* @return void
*/
private function limit_characters() {
// We retrieve the search term unescaped because we want to count the characters properly. We make sure to escape it afterwards, if we do something with it.
$unescaped_s = \get_search_query( false );
// We then unslash the search term, again because we want to count the characters properly. We make sure to slash it afterwards, if we do something with it.
$raw_s = \wp_unslash( $unescaped_s );
if ( \mb_strlen( $raw_s, 'UTF-8' ) > $this->options_helper->get( 'search_character_limit' ) ) {
$new_s = \mb_substr( $raw_s, 0, $this->options_helper->get( 'search_character_limit' ), 'UTF-8' );
\set_query_var( 's', \wp_slash( \esc_attr( $new_s ) ) );
}
}
/**
* Determines if a text string contains an emoji or not.
*
* @param string $text The text string to detect emoji in.
*
* @return bool
*/
private function has_emoji( $text ) {
$emojis_regex = '/([^-\p{L}\x00-\x7F]+)/u';
\preg_match( $emojis_regex, $text, $matches );
return ! empty( $matches );
}
}

View File

@@ -0,0 +1,122 @@
<?php
namespace Yoast\WP\SEO\Premium\Integrations\Third_Party;
use WP_Post;
use WPSEO_Admin_Utils;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Premium\Conditionals\Zapier_Enabled_Conditional;
use Yoast\WP\SEO\Premium\Helpers\Zapier_Helper;
/**
* Class to manage the Zapier integration in the Classic editor.
*
* @deprecated 20.7
* @codeCoverageIgnore
*/
class Zapier_Classic_Editor implements Integration_Interface {
/**
* The Zapier helper.
*
* @var Zapier_Helper
*/
protected $zapier_helper;
/**
* Zapier constructor.
*
* @deprecated 20.7
* @codeCoverageIgnore
*
* @param Zapier_Helper $zapier_helper The Zapier helper.
*/
public function __construct( Zapier_Helper $zapier_helper ) {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.7' );
$this->zapier_helper = $zapier_helper;
}
/**
* Returns the conditionals based in which this loadable should be active.
*
* @deprecated 20.7
* @codeCoverageIgnore
*
* @return array
*/
public static function get_conditionals() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.7' );
return [ Zapier_Enabled_Conditional::class ];
}
/**
* Initializes the integration.
*
* This is the place to register hooks and filters.
*
* @deprecated 20.7
* @codeCoverageIgnore
*
* @return void
*/
public function register_hooks() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.7' );
\add_action( 'wpseo_publishbox_misc_actions', [ $this, 'add_publishbox_text' ] );
}
/**
* Adds the Zapier text to the Classic Editor publish box.
*
* @deprecated 20.7
* @codeCoverageIgnore
*
* @param WP_Post $post The current post object.
*
* @return void
*/
public function add_publishbox_text( $post ) {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.7' );
if ( ! \is_a( $post, 'WP_Post' ) ) {
return;
}
if ( ! $this->zapier_helper->is_post_type_supported( $post->post_type ) ) {
return;
}
?>
<div class="misc-pub-section yoast yoast-seo-score yoast-zapier-text">
<span class="yoast-logo svg"></span>
<span>
<?php
if ( $this->zapier_helper->is_connected() ) {
\printf(
/* translators: 1: Zapier, 2: Link start tag, 3: Zapier, 4: Link closing tag. */
\esc_html__( 'Youre successfully connected to %1$s. Publishing a post will trigger automated actions based on your Zaps configuration. %2$sManage your Zap in %3$s%4$s.', 'wordpress-seo-premium' ),
'Zapier',
'<a href="' . \esc_url( 'https://zapier.com/app/zaps' ) . '" target="_blank">',
'Zapier',
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- The content is already escaped.
WPSEO_Admin_Utils::get_new_tab_message() . '</a>'
);
}
else {
\printf(
/* translators: 1: Link start tag, 2: Yoast SEO, 3: Zapier, 4: Link closing tag. */
\esc_html__( '%1$sConnect %2$s with %3$s%4$s to instantly share your published posts with 2000+ destinations such as Twitter, Facebook and more.', 'wordpress-seo-premium' ),
'<a href="' . \esc_url( \admin_url( 'admin.php?page=wpseo_integrations' ) ) . '" target="_blank">',
'Yoast SEO',
'Zapier',
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- The content is already escaped.
WPSEO_Admin_Utils::get_new_tab_message() . '</a>'
);
}
?>
</span>
</div>
<?php
}
}

View File

@@ -0,0 +1,149 @@
<?php
namespace Yoast\WP\SEO\Premium\Integrations\Third_Party;
use WP_Error;
use Yoast\WP\SEO\Helpers\Meta_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Models\Indexable;
use Yoast\WP\SEO\Premium\Conditionals\Zapier_Enabled_Conditional;
use Yoast\WP\SEO\Premium\Helpers\Zapier_Helper;
/**
* Class to manage the triggering of the Zapier integration.
*
* @deprecated 20.7
* @codeCoverageIgnore
*/
class Zapier_Trigger implements Integration_Interface {
/**
* The meta helper.
*
* @var Meta_Helper
*/
protected $meta_helper;
/**
* The Zapier helper.
*
* @var Zapier_Helper
*/
protected $zapier_helper;
/**
* Zapier constructor.
*
* @deprecated 20.7
* @codeCoverageIgnore
*
* @param Meta_Helper $meta_helper The meta helper.
* @param Zapier_Helper $zapier_helper The Zapier helper.
*/
public function __construct( Meta_Helper $meta_helper, Zapier_Helper $zapier_helper ) {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.7' );
$this->meta_helper = $meta_helper;
$this->zapier_helper = $zapier_helper;
}
/**
* Returns the conditionals based in which this loadable should be active.
*
* @deprecated 20.7
* @codeCoverageIgnore
*
* @return array
*/
public static function get_conditionals() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.7' );
return [ Zapier_Enabled_Conditional::class ];
}
/**
* Initializes the integration.
*
* This is the place to register hooks and filters.
*
* @deprecated 20.7
* @codeCoverageIgnore
*
* @return void
*/
public function register_hooks() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.7' );
\add_action( 'wpseo_save_indexable', [ $this, 'maybe_call_zapier' ] );
}
/**
* Decides if Zapier should be triggered.
*
* Zapier should be triggered only if:
* - we have a connection established
* - the item is a post (in the Indexable sense, as opposed to taxonomies etc.)
* - the item status is 'publish'
* - we are not serving a REST request (to avoid triggering on the first request by the block editor)
* - if the item hasn't been sent before
* - if the post_date is recent (so we are not just updating a post published before enabling Zapier)
*
* @deprecated 20.7
* @codeCoverageIgnore
*
* @param Indexable $indexable The indexable.
*
* @return void
*/
public function maybe_call_zapier( Indexable $indexable ) {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.7' );
if ( ! $this->zapier_helper->is_connected()
|| $indexable->object_type !== 'post'
|| $indexable->post_status !== 'publish'
|| \defined( 'REST_REQUEST' ) && \REST_REQUEST
|| $this->meta_helper->get_value( 'zapier_trigger_sent', $indexable->object_id ) === '1' ) {
return;
}
// All dates are GMT to prevent failing checks due to timezone differences.
$post = \get_post( $indexable->object_id );
$published_datetime_gmt = \strtotime( $post->post_date_gmt . ' +0000' );
$half_an_hour_ago_datetime_gmt = ( \time() - ( \MINUTE_IN_SECONDS * 30 ) );
if ( ! $this->zapier_helper->is_post_type_supported( $post->post_type )
|| $published_datetime_gmt < $half_an_hour_ago_datetime_gmt ) {
return;
}
$this->call_zapier( $indexable );
}
/**
* Sends a request to the Zapier trigger hook.
*
* @deprecated 20.7
* @codeCoverageIgnore
*
* @param Indexable $indexable The indexable.
*
* @return void
*/
public function call_zapier( Indexable $indexable ) {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.7' );
$trigger_url = $this->zapier_helper->get_trigger_url();
$zapier_data = $this->zapier_helper->get_data_for_zapier( $indexable );
$response = \wp_remote_post(
$trigger_url,
[
'body' => $zapier_data,
]
);
if ( ! $response instanceof WP_Error ) {
// Need to cast the new value to a string as booleans aren't supported.
$this->meta_helper->set_value( 'zapier_trigger_sent', '1', $indexable->object_id );
}
}
}

View File

@@ -0,0 +1,246 @@
<?php
namespace Yoast\WP\SEO\Premium\Integrations\Third_Party;
use WPSEO_Admin_Asset_Manager;
use WPSEO_Admin_Utils;
use Yoast\WP\SEO\Conditionals\Yoast_Admin_And_Dashboard_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Premium\Helpers\Zapier_Helper;
use Yoast\WP\SEO\Presenters\Admin\Alert_Presenter;
use Yoast_Feature_Toggle;
/**
* Zapier integration class for managing the toggle and the connection setup.
*
* @deprecated 20.7
* @codeCoverageIgnore
*/
class Zapier implements Integration_Interface {
/**
* The Zapier dashboard URL.
*
* @var string
*/
const ZAPIER_DASHBOARD_URL = 'https://zapier.com/app/zaps';
/**
* Represents the admin asset manager.
*
* @var WPSEO_Admin_Asset_Manager
*/
protected $asset_manager;
/**
* The Zapier helper.
*
* @var Zapier_Helper
*/
protected $zapier_helper;
/**
* Returns the conditionals based in which this loadable should be active.
*
* @deprecated 20.7
* @codeCoverageIgnore
*
* @return array
*/
public static function get_conditionals() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.7' );
return [ Yoast_Admin_And_Dashboard_Conditional::class ];
}
/**
* Zapier constructor.
*
* @deprecated 20.7
* @codeCoverageIgnore
*
* @param WPSEO_Admin_Asset_Manager $asset_manager The admin asset manager.
* @param Zapier_Helper $zapier_helper The Zapier helper.
*/
public function __construct( WPSEO_Admin_Asset_Manager $asset_manager, Zapier_Helper $zapier_helper ) {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.7' );
$this->asset_manager = $asset_manager;
$this->zapier_helper = $zapier_helper;
}
/**
* Initializes the integration.
*
* This is the place to register hooks and filters.
*
* @deprecated 20.7
* @codeCoverageIgnore
*
* @return void
*/
public function register_hooks() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.7' );
// Add the Zapier toggle to the Integrations tab in the admin.
\add_action( 'Yoast\WP\SEO\admin_integration_after', [ $this, 'toggle_after' ] );
\add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] );
\add_filter( 'wpseo_premium_integrations_page_data', [ $this, 'enhance_integrations_page_data' ] );
}
/**
* Enqueues the required assets.
*
* @deprecated 20.7
* @codeCoverageIgnore
*
* @return void
*/
public function enqueue_assets() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.7' );
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Date is not processed or saved.
if ( ! isset( $_GET['page'] ) || $_GET['page'] !== 'wpseo_integrations' ) {
return;
}
$this->asset_manager->enqueue_style( 'monorepo' );
}
/**
* Returns additional content to be displayed after the Zapier toggle.
*
* @deprecated 20.7
* @codeCoverageIgnore
*
* @param Yoast_Feature_Toggle $integration The integration feature we've shown the toggle for.
*
* @return void
*/
public function toggle_after( $integration ) {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.7' );
if ( $integration->setting !== 'zapier_integration_active' ) {
return;
}
if ( $this->zapier_helper->is_connected() ) {
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output is already escaped in function.
echo $this->get_connected_content();
return;
}
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output is already escaped in function.
echo $this->get_not_connected_content();
}
/**
* Returns additional content to be displayed when Zapier is connected.
*
* @return string The additional content.
*/
private function get_connected_content() {
$alert = new Alert_Presenter(
\sprintf(
/* translators: 1: Yoast SEO, 2: Zapier. */
\esc_html__( '%1$s is successfully connected to %2$s!', 'wordpress-seo-premium' ),
'Yoast SEO',
'Zapier'
),
'success'
);
$output = '<div id="zapier-connection">';
$output .= $alert->present();
$output .= '<p><a href="' . self::ZAPIER_DASHBOARD_URL . '" class="yoast-button yoast-button--primary" type="button" target="_blank">' . \sprintf(
/* translators: %s: Zapier. */
\esc_html__( 'Go to your %s Dashboard', 'wordpress-seo-premium' ),
'Zapier'
) . WPSEO_Admin_Utils::get_new_tab_message() . '</a></p>';
$output .= '<p>' . \sprintf(
/* translators: 1: Zapier, 2: The Zapier API Key. */
\esc_html__( '%1$s uses this API Key: %2$s', 'wordpress-seo-premium' ),
'Zapier',
'<strong>' . $this->zapier_helper->get_or_generate_zapier_api_key() . '</strong>'
) . '</p>';
$output .= '<p><button name="zapier_api_key_reset" value="1" type="submit" class="yoast-button yoast-button--secondary">' . \esc_html__( 'Reset API Key', 'wordpress-seo-premium' ) . '</button></p>';
$output .= '</div>';
return $output;
}
/**
* Returns additional content to be displayed when Zapier is not connected.
*
* @return string The additional content.
*/
private function get_not_connected_content() {
$content = \sprintf(
/* translators: 1: Yoast SEO, 2: Zapier, 3: Emphasis open tag, 4: Emphasis close tag. */
\esc_html__( '%1$s is not connected to %2$s. To set up a connection, make sure you click %3$sSave changes%4$s first, then copy the given API key below and use it to %3$screate%4$s and %3$sturn on%4$s a Zap within your %2$s account.', 'wordpress-seo-premium' ),
'Yoast SEO',
'Zapier',
'<em>',
'</em>'
);
$content .= '<br/><br/>';
$content .= ' ' . \sprintf(
/* translators: 1: Yoast SEO. */
\esc_html__( 'Please note that you can only create 1 Zap with a trigger event from %1$s. Within this Zap you can choose one or more actions.', 'wordpress-seo-premium' ),
'Yoast SEO'
);
$alert = new Alert_Presenter(
$content,
'info'
);
$output = '<div id="zapier-connection">';
$output .= $alert->present();
$output .= '<div class="yoast-field-group">';
$output .= '<div class="yoast-field-group__title yoast-field-group__title--light">';
$output .= '<label for="zapier-api-key">' . \sprintf(
/* translators: %s: Zapier. */
\esc_html__( '%s will ask for an API key. Use this one:', 'wordpress-seo-premium' ),
'Zapier'
) . '</label>';
$output .= '</div>';
$output .= '<div class="yoast-field-group__inline">';
$output .= '<input class="yoast-field-group__inputfield" readonly type="text" id="zapier-api-key" name="wpseo[zapier_api_key]" value="' . $this->zapier_helper->get_or_generate_zapier_api_key() . '">';
$output .= '<button type="button" class="yoast-button yoast-button--secondary" id="copy-zapier-api-key" data-clipboard-target="#zapier-api-key">' . \esc_html__( 'Copy to clipboard', 'wordpress-seo-premium' ) . '</button><br />';
$output .= '</div>';
$output .= '</div>';
$output .= '<p><a href="' . self::ZAPIER_DASHBOARD_URL . '" class="yoast-button yoast-button--primary" type="button" target="_blank">' . \sprintf(
/* translators: %s: Zapier. */
\esc_html__( 'Create a Zap in %s', 'wordpress-seo-premium' ),
'Zapier'
) . WPSEO_Admin_Utils::get_new_tab_message() . '</a></p>';
$output .= '</div>';
return $output;
}
/**
* Enhances the array for the integrations page script with additional data.
*
* @deprecated 20.7
* @codeCoverageIgnore
*
* @param array $data The array to add data to.
*
* @return array The enhances data.
*/
public function enhance_integrations_page_data( $data ) {
\_deprecated_function( __METHOD__, 'WPSEO Premium 20.7' );
if ( ! \is_array( $data ) ) {
$data = [ $data ];
}
$data['zapierKey'] = $this->zapier_helper->get_or_generate_zapier_api_key();
$data['zapierUrl'] = self::ZAPIER_DASHBOARD_URL;
$data['zapierIsConnected'] = $this->zapier_helper->is_connected();
return $data;
}
}

View File

@@ -0,0 +1,81 @@
<?php
namespace Yoast\WP\SEO\Premium\Integrations\Watchers;
use Yoast\WP\SEO\Conditionals\No_Conditionals;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
/**
* Watcher for the wpseo option on Premium.
*
* Represents the option wpseo watcher for Premium.
*
* @deprecated 20.7
* @codeCoverageIgnore
*/
class Premium_Option_Wpseo_Watcher implements Integration_Interface {
use No_Conditionals;
/**
* The options helper.
*
* @var Options_Helper
*/
private $options;
/**
* Watcher constructor.
*
* @deprecated 20.7
* @codeCoverageIgnore
*
* @param Options_Helper $options The options helper.
*/
public function __construct( Options_Helper $options ) {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.7' );
$this->options = $options;
}
/**
* Initializes the integration.
*
* This is the place to register hooks and filters.
*
* @deprecated 20.7
* @codeCoverageIgnore
*
* @return void
*/
public function register_hooks() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.7' );
\add_action( 'update_option_wpseo', [ $this, 'check_zapier_option_disabled' ], 10, 2 );
}
/**
* Checks if the Zapier integration is disabled; if so, deletes the data.
*
* @deprecated 20.7
* @codeCoverageIgnore
*
* @param array $old_value The old value of the option.
* @param array $new_value The new value of the option.
*
* @return bool Whether the Zapier data has been deleted or not.
*/
public function check_zapier_option_disabled( $old_value, $new_value ) {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.7' );
if ( \array_key_exists( 'zapier_integration_active', $new_value )
&& $old_value['zapier_integration_active'] === true
&& $new_value['zapier_integration_active'] === false ) {
$this->options->set( 'zapier_subscription', [] );
$this->options->set( 'zapier_api_key', '' );
return true;
}
return false;
}
}

View File

@@ -0,0 +1,91 @@
<?php
namespace Yoast\WP\SEO\Premium\Integrations\Watchers;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Premium\Conditionals\Zapier_Enabled_Conditional;
/**
* Watcher for resetting the Zapier API key.
*
* Represents the Zapier API key reset watcher for Premium.
*
* @deprecated 20.5
* @codeCoverageIgnore
*
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
*/
class Zapier_APIKey_Reset_Watcher implements Integration_Interface {
/**
* The options helper.
*
* @var Options_Helper
*/
private $options;
/**
* Watcher constructor.
*
* @deprecated 20.5
* @codeCoverageIgnore
*
* @param Options_Helper $options The options helper.
*/
public function __construct( Options_Helper $options ) {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.5' );
$this->options = $options;
}
/**
* Returns the conditionals based in which this loadable should be active.
*
* @deprecated 20.5
* @codeCoverageIgnore
*
* @return array
*/
public static function get_conditionals() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.5' );
return [ Zapier_Enabled_Conditional::class ];
}
/**
* Initializes the integration.
*
* This is the place to register hooks and filters.
*
* @deprecated 20.5
* @codeCoverageIgnore
*
* @return void
*/
public function register_hooks() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.5' );
}
/**
* Checks if the Zapier API key must be reset; if so, deletes the data.
*
* @deprecated 20.5
* @codeCoverageIgnore
*
* @return bool Whether the Zapier data has been deleted or not.
*/
public function zapier_api_key_reset() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.5' );
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- The nonce is already validated.
if ( \current_user_can( 'manage_options' ) && isset( $_POST['zapier_api_key_reset'] ) && $_POST['zapier_api_key_reset'] === '1' ) {
$this->options->set( 'zapier_api_key', '' );
$this->options->set( 'zapier_subscription', [] );
return true;
}
return false;
}
}

View File

@@ -0,0 +1,323 @@
<?php
namespace Yoast\WP\SEO\Premium\Routes;
use WP_REST_Request;
use WP_REST_Response;
use Yoast\WP\SEO\Main;
use Yoast\WP\SEO\Premium\Actions\Zapier_Action;
use Yoast\WP\SEO\Premium\Conditionals\Zapier_Enabled_Conditional;
use Yoast\WP\SEO\Routes\Route_Interface;
/**
* Registers the route for the Zapier integration.
*
* @deprecated 20.7
* @codeCoverageIgnore
*/
class Zapier_Route implements Route_Interface {
/**
* The Zapier route prefix.
*
* @var string
*/
const ROUTE_PREFIX = 'zapier';
/**
* The subscribe route constant.
*
* @var string
*/
const SUBSCRIBE_ROUTE = self::ROUTE_PREFIX . '/subscribe';
/**
* The unsubscribe route constant.
*
* @var string
*/
const UNSUBSCRIBE_ROUTE = self::ROUTE_PREFIX . '/unsubscribe';
/**
* The check route constant.
*
* @var string
*/
const CHECK_API_KEY_ROUTE = self::ROUTE_PREFIX . '/check';
/**
* The perform list route constant.
*
* @var string
*/
const PERFORM_LIST = self::ROUTE_PREFIX . '/list';
/**
* The is_connected route constant.
*
* @var string
*/
const IS_CONNECTED = self::ROUTE_PREFIX . '/is_connected';
/**
* The reset_api_key route constant.
*
* @var string
*/
const RESET_API_KEY = self::ROUTE_PREFIX . '/reset_api_key';
/**
* Instance of the Zapier_Action.
*
* @var Zapier_Action
*/
protected $zapier_action;
/**
* Zapier_Route constructor.
*
* @deprecated 20.7
* @codeCoverageIgnore
*
* @param Zapier_Action $zapier_action The action to handle the requests to the endpoint.
*/
public function __construct( Zapier_Action $zapier_action ) {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.7' );
$this->zapier_action = $zapier_action;
}
/**
* Registers routes with WordPress.
*
* @deprecated 20.7
* @codeCoverageIgnore
*
* @return void
*/
public function register_routes() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.7' );
$subscribe_route_args = [
'methods' => 'POST',
'args' => [
'url' => [
'required' => true,
'type' => 'string',
'description' => 'The callback URL to use.',
],
'api_key' => [
'required' => true,
'type' => 'string',
'description' => 'The API key to validate.',
],
],
'callback' => [ $this, 'subscribe' ],
'permission_callback' => '__return_true',
];
\register_rest_route( Main::API_V1_NAMESPACE, self::SUBSCRIBE_ROUTE, $subscribe_route_args );
$unsubscribe_route_args = [
'methods' => 'DELETE',
'args' => [
'id' => [
'required' => true,
'type' => 'string',
'description' => 'The ID of the subscription to unsubscribe.',
],
],
'callback' => [ $this, 'unsubscribe' ],
'permission_callback' => '__return_true',
];
\register_rest_route( Main::API_V1_NAMESPACE, self::UNSUBSCRIBE_ROUTE, $unsubscribe_route_args );
$check_api_key_route_args = [
'methods' => 'POST',
'args' => [
'api_key' => [
'required' => true,
'type' => 'string',
'description' => 'The API key to validate.',
],
],
'callback' => [ $this, 'check_api_key' ],
'permission_callback' => '__return_true',
];
\register_rest_route( Main::API_V1_NAMESPACE, self::CHECK_API_KEY_ROUTE, $check_api_key_route_args );
$perform_list_route_args = [
'methods' => 'GET',
'args' => [
'api_key' => [
'required' => true,
'type' => 'string',
'description' => 'The API key to validate.',
],
],
'callback' => [ $this, 'perform_list' ],
'permission_callback' => '__return_true',
];
\register_rest_route( Main::API_V1_NAMESPACE, self::PERFORM_LIST, $perform_list_route_args );
$is_connected_route_args = [
'methods' => 'GET',
'args' => [],
'callback' => [ $this, 'is_connected' ],
'permission_callback' => [ $this, 'check_permissions' ],
];
\register_rest_route( Main::API_V1_NAMESPACE, self::IS_CONNECTED, $is_connected_route_args );
$reset_api_key_route_args = [
'methods' => 'POST',
'args' => [
'api_key' => [
'required' => true,
'type' => 'string',
'description' => 'The API key to reset.',
],
],
'callback' => [ $this, 'reset_api_key' ],
'permission_callback' => [ $this, 'check_permissions' ],
];
\register_rest_route( Main::API_V1_NAMESPACE, self::RESET_API_KEY, $reset_api_key_route_args );
}
/**
* Runs the subscribe action.
*
* @deprecated 20.7
* @codeCoverageIgnore
*
* @param WP_REST_Request $request The request object.
*
* @return WP_REST_Response The response of the subscribe action.
*/
public function subscribe( WP_REST_Request $request ) {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.7' );
$subscription = $this->zapier_action->subscribe( $request['url'], $request['api_key'] );
$response = $subscription->data;
if ( empty( $response ) && \property_exists( $subscription, 'message' ) ) {
$response = $subscription->message;
}
return new WP_REST_Response( $response, $subscription->status );
}
/**
* Runs the unsubscribe action.
*
* @deprecated 20.7
* @codeCoverageIgnore
*
* @param WP_REST_Request $request The request object.
*
* @return WP_REST_Response The response of the unsubscribe action.
*/
public function unsubscribe( WP_REST_Request $request ) {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.7' );
$subscription = $this->zapier_action->unsubscribe( $request['id'] );
return new WP_REST_Response( $subscription->message, $subscription->status );
}
/**
* Runs the check_api_key action.
*
* @deprecated 20.7
* @codeCoverageIgnore
*
* @param WP_REST_Request $request The request object.
*
* @return WP_REST_Response The response of the check_api_key action.
*/
public function check_api_key( WP_REST_Request $request ) {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.7' );
$check = $this->zapier_action->check_api_key( $request['api_key'] );
return new WP_REST_Response( $check->message, $check->status );
}
/**
* Runs the perform_list action.
*
* @deprecated 20.7
* @codeCoverageIgnore
*
* @param WP_REST_Request $request The request object.
*
* @return WP_REST_Response The response of the perform_list action.
*/
public function perform_list( WP_REST_Request $request ) {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.7' );
$response = $this->zapier_action->perform_list( $request['api_key'] );
return new WP_REST_Response( $response->data, $response->status );
}
/**
* Runs the is_connected action.
*
* @deprecated 20.7
* @codeCoverageIgnore
*
* @return WP_REST_Response The response of the is_connected action.
*/
public function is_connected() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.7' );
$response = $this->zapier_action->is_connected();
return new WP_REST_Response( [ 'json' => $response->data ] );
}
/**
* Runs the reset_api_key action.
*
* @deprecated 20.7
* @codeCoverageIgnore
*
* @param WP_REST_Request $request The request object.
*
* @return WP_REST_Response The response of the reset_api_key action.
*/
public function reset_api_key( WP_REST_Request $request ) {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.7' );
$result = $this->zapier_action->reset_api_key( $request['api_key'] );
return new WP_REST_Response( [ 'json' => $result->data ] );
}
/**
* Checks if the user is authorised to query the connection status or reset the key.
*
* @deprecated 20.7
* @codeCoverageIgnore Just a wrapper for a WordPress function.
*
* @return bool Whether the user is authorised to query the connection status or reset the key.
*/
public function check_permissions() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.7' );
return \current_user_can( 'wpseo_manage_options' );
}
/**
* Returns the conditionals based in which these routes should be active.
*
* @deprecated 20.7
* @codeCoverageIgnore
*
* @return array The list of conditionals.
*/
public static function get_conditionals() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.7' );
return [ Zapier_Enabled_Conditional::class ];
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Yoast\WP\SEO\Schema_Templates\Block_Patterns;
/**
* Holds the names of the block pattern categories.
*
* @deprecated 20.5
* @codeCoverageIgnore
*/
class Block_Pattern_Categories {
const YOAST_JOB_POSTING = 'yoast_job_posting';
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Yoast\WP\SEO\Schema_Templates\Block_Patterns;
/**
* Holds the names of the block pattern keywords.
*
* @deprecated 20.5
* @codeCoverageIgnore
*/
class Block_Pattern_Keywords {
const YOAST_JOB_POSTING = [ 'yoast', 'job', 'posting', 'vacancy' ];
}

View File

@@ -0,0 +1,80 @@
<?php
namespace Yoast\WP\SEO\Schema_Templates\Block_Patterns;
/**
* A Gutenberg block pattern.
*
* @deprecated 20.5
* @codeCoverageIgnore
*/
abstract class Block_Pattern {
/**
* Returns the block pattern configuration.
*
* @deprecated 20.5
* @codeCoverageIgnore
*
* @return string[] The configuration.
*/
public function get_configuration() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.5' );
return [
'title' => '',
'content' => '',
'categories' => [],
'keywords' => [],
];
}
/**
* Gets the name of this block pattern.
*
* @deprecated 20.5
* @codeCoverageIgnore
*
* @return string The name of this block pattern.
*/
abstract public function get_name();
/**
* Gets the title of this block pattern.
*
* @deprecated 20.5
* @codeCoverageIgnore
*
* @return string The title of this block pattern.
*/
abstract public function get_title();
/**
* Gets the contents of this block pattern.
*
* @deprecated 20.5
* @codeCoverageIgnore
*
* @return string The contents of this block pattern.
*/
abstract public function get_content();
/**
* Gets the categories of this block pattern.
*
* @deprecated 20.5
* @codeCoverageIgnore
*
* @return string[] The categories of this block pattern.
*/
abstract public function get_categories();
/**
* Gets the keywords of this block pattern.
*
* @deprecated 20.5
* @codeCoverageIgnore
*
* @return string[] The keywords of this block pattern.
*/
abstract public function get_keywords();
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Yoast\WP\SEO\Schema_Templates\Block_Patterns;
/**
* A minimal job posting, containing required blocks only.
*
* @deprecated 20.5
* @codeCoverageIgnore
*
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
*/
abstract class Job_Posting_Base_Pattern extends Block_Pattern {
/**
* Includes this Job Posting block pattern in the Yoast Job Posting block pattern category.
*
* @deprecated 20.5
* @codeCoverageIgnore
*
* @return array The categories under which this block pattern should be shown.
*/
public function get_categories() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.5' );
return [];
}
/**
* Gets the keywords of this block pattern.
*
* @deprecated 20.5
* @codeCoverageIgnore
*
* @return array The keywords that help users discover the pattern while searching.
*/
public function get_keywords() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.5' );
return [];
}
}

View File

@@ -0,0 +1,141 @@
<?php
namespace Yoast\WP\SEO\Schema_Templates\Block_Patterns;
/**
* A job posting containing all the required and recommended blocks, shown in one column (with a three-column header).
*
* @deprecated 20.5
* @codeCoverageIgnore
*
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
*/
class Job_Posting_One_Column extends Job_Posting_Base_Pattern {
/**
* Gets the name of this block pattern.
*
* @deprecated 20.5
* @codeCoverageIgnore
*
* @return string The name of this block pattern.
*/
public function get_name() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.5' );
return 'yoast/job-posting/one-column';
}
/**
* Gets the title of this block pattern.
*
* @deprecated 20.5
* @codeCoverageIgnore
*
* @return string The title of this block pattern.
*/
public function get_title() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.5' );
return 'Three-column header and one centered column of text';
}
/**
* Gets the contents of this block pattern.
*
* @deprecated 20.5
* @codeCoverageIgnore
*
* @return string The contents of this block pattern.
*/
public function get_content() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.5' );
return '<!-- wp:columns {"align":"wide"} -->
<div class="wp-block-columns alignwide"><!-- wp:column -->
<div class="wp-block-column"><!-- wp:html -->
<strong>Employment</strong>
<!-- /wp:html -->
<!-- wp:yoast/job-employment-type {"employmentType":"FULL_TIME"} -->
<div class="yoast-job-block__employment "><div><span data-id="employmentType" data-value="FULL_TIME">Full time</span></div></div>
<!-- /wp:yoast/job-employment-type --></div>
<!-- /wp:column -->
<!-- wp:column -->
<div class="wp-block-column"><!-- wp:html -->
<strong>Base salary</strong>
<!-- /wp:html -->
<!-- wp:yoast/job-salary -->
<div class=""><!-- wp:yoast/job-base-salary {"currency":"USD","value":"4000","unit":"MONTH"} -->
<div class="yoast-job-block__salary "><div class="yoast-schema-flex"><span data-id="currency" data-value="USD">USD</span> 4000 / <span data-id="unit" data-value="MONTH">month</span></div></div>
<!-- /wp:yoast/job-base-salary --></div>
<!-- /wp:yoast/job-salary --></div>
<!-- /wp:column --></div>
<!-- /wp:columns -->
<!-- wp:separator {"align":"wide"} -->
<hr class="wp-block-separator alignwide"/>
<!-- /wp:separator -->
<!-- wp:columns {"align":"wide"} -->
<div class="wp-block-columns alignwide"><!-- wp:column {"width":"66.66%"} -->
<div class="wp-block-column" style="flex-basis:66.66%"><!-- wp:paragraph {"style":{"typography":{"fontSize":24}}} -->
<p style="font-size:24px">Our company is growing! And were searching for an ambitious employee! Do you believe that a hard work is fundamental for your business? If you do, were probably looking for you!</p>
<!-- /wp:paragraph -->
<!-- wp:heading -->
<h2 id="h-about-the-job">About the job</h2>
<!-- /wp:heading -->
<!-- wp:yoast/job-description -->
<div class="yoast-job-block__description "><p data-id="description">Youll be part of an interdisciplinary team and together youll work on challenging, varied projects. Youll get the freedom and responsibility to reach your full potential!</p></div>
<!-- /wp:yoast/job-description -->
<!-- wp:heading -->
<h2 id="h-about-you">About you</h2>
<!-- /wp:heading -->
<!-- wp:paragraph -->
<p>Do you you have a passion for your job? Are you aware of current trends in your field? Do you love diving into details?</p>
<!-- /wp:paragraph -->
<!-- wp:paragraph -->
<p>Were looking for someone who is proactive, patient and smart, has an eye for details, and is a great communicator and motivator. If you also have a sense of humor and an enthusiasm for participating in discussions, youre probably a fit!</p>
<!-- /wp:paragraph -->
<!-- wp:yoast/job-requirements {"title_level":2} -->
<h2 data-id="title">To summarize</h2><div class="yoast-job-block__requirements "><ul data-id="requirements"><li>You enjoy working in a fast-paced team environment.</li><li>You dont ever think “good enough” is good enough.</li><li>You are available for 40 hours per week.</li><li>You speak and write English fluently (preferably with at least proficiency level C1).</li></ul></div>
<!-- /wp:yoast/job-requirements -->
<!-- wp:yoast/job-benefits {"title_level":2,"className":"yoast-job-block__benefits"} -->
<h2 data-id="title">What were offering</h2><div class="yoast-job-block__benefits yoast-job-block__benefits"><ul data-id="benefits"><li>A challenging job in a fast-growing, dynamic, ambitious and international atmosphere.</li><li>25 vacation days (on the base of 40 hours).</li><li>Youll be able to spend 10% of your salary on education.</li><li>We have a really fun company culture with lots of team building activities.</li><li>Are you interested? Then please send your application to this@emailaddress.com before January 1, 2022. Do you have any questions? Well be happy to answer them.</li></ul></div>
<!-- /wp:yoast/job-benefits -->
<!-- wp:yoast/job-application-closing-date {"closingDate":"2022-01-01"} -->
<div class="yoast-job-block__application-closing-date "><span data-id="title">Apply before</span> <time datetime="2022-01-01">January 1, 2022</time></div>
<!-- /wp:yoast/job-application-closing-date --></div>
<!-- /wp:column -->
<!-- wp:column {"width":"33.33%"} -->
<div class="wp-block-column" style="flex-basis:33.33%"><!-- wp:yoast/job-location -->
<div class=""><!-- wp:yoast/office-location -->
<div class="yoast-job-block__location "><!-- wp:yoast/job-location-address -->
<div class="yoast-job-block__location__address "><span data-id="address">350 5th Avenue</span></div>
<!-- /wp:yoast/job-location-address -->
<!-- wp:yoast/job-location-city -->
<div class="yoast-job-block__location__city "><span data-id="city">New York</span></div>
<!-- /wp:yoast/job-location-city -->
<!-- wp:yoast/job-location-postal-code -->
<div class="yoast-job-block__location__postal-code "><span data-id="postal-code">NY 10118</span></div>
<!-- /wp:yoast/job-location-postal-code -->
<!-- wp:yoast/job-location-country -->
<div class="yoast-job-block__location__country "><span data-id="country">United States of America</span></div>
<!-- /wp:yoast/job-location-country --></div>
<!-- /wp:yoast/office-location --></div>
<!-- /wp:yoast/job-location --></div>
<!-- /wp:column --></div>
<!-- /wp:columns -->';
}
}

View File

@@ -0,0 +1,157 @@
<?php
namespace Yoast\WP\SEO\Schema_Templates\Block_Patterns;
/**
* A job posting containing all the required and recommended blocks, shown in two columns (with a two-column header).
*
* @deprecated 20.5
* @codeCoverageIgnore
*
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
*/
class Job_Posting_Two_Columns extends Job_Posting_Base_Pattern {
/**
* Gets the name of this block pattern.
*
* @deprecated 20.5
* @codeCoverageIgnore
*
* @return string The name of this block pattern.
*/
public function get_name() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.5' );
return 'yoast/job-posting/two-columns';
}
/**
* Gets the title of this block pattern.
*
* @deprecated 20.5
* @codeCoverageIgnore
*
* @return string The title of this block pattern.
*/
public function get_title() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.5' );
return 'Two-column header and two columns of text';
}
/**
* Gets the contents of this block pattern.
*
* @deprecated 20.5
* @codeCoverageIgnore
*
* @return string The contents of this block pattern.
*/
public function get_content() {
\_deprecated_function( __METHOD__, 'Yoast SEO Premium 20.5' );
return '<!-- wp:columns {"align":"wide"} -->
<div class="wp-block-columns alignwide"><!-- wp:column -->
<div class="wp-block-column"><!-- wp:html -->
<strong>Employment</strong>
<!-- /wp:html -->
<!-- wp:yoast/job-employment-type {"employmentType":"FULL_TIME"} -->
<div class="yoast-job-block__employment "><div><span data-id="employmentType" data-value="FULL_TIME">Full time</span></div></div>
<!-- /wp:yoast/job-employment-type --></div>
<!-- /wp:column -->
<!-- wp:column -->
<div class="wp-block-column"><!-- wp:html -->
<strong>Salary range</strong>
<!-- /wp:html -->
<!-- wp:yoast/job-salary -->
<div class=""><!-- wp:yoast/job-salary-range {"currency":"USD","minValue":"1000","maxValue":"2000","unit":"MONTH"} -->
<div class="yoast-job-block__salary "><div class="yoast-schema-flex"><span data-id="currency" data-value="USD">USD</span> 1000 - 2000 / <span data-id="unit" data-value="MONTH">month</span></div></div>
<!-- /wp:yoast/job-salary-range --></div>
<!-- /wp:yoast/job-salary --></div>
<!-- /wp:column -->
<!-- wp:column -->
<div class="wp-block-column"><!-- wp:html -->
<strong>Location</strong>
<!-- /wp:html -->
<!-- wp:yoast/job-location -->
<div class=""><!-- wp:yoast/office-location -->
<div class="yoast-job-block__location "><!-- wp:yoast/job-location-address -->
<div class="yoast-job-block__location__address "><span data-id="address">350 5th Avenue</span></div>
<!-- /wp:yoast/job-location-address -->
<!-- wp:yoast/job-location-postal-code -->
<div class="yoast-job-block__location__postal-code "><span data-id="postal-code">10118</span></div>
<!-- /wp:yoast/job-location-postal-code -->
<!-- wp:yoast/job-location-city -->
<div class="yoast-job-block__location__city "><span data-id="city">New York</span></div>
<!-- /wp:yoast/job-location-city -->
<!-- wp:yoast/job-location-region -->
<div class="yoast-job-block__location__region "><span data-id="region">NY</span></div>
<!-- /wp:yoast/job-location-region -->
<!-- wp:yoast/job-location-country -->
<div class="yoast-job-block__location__country "><span data-id="country">United States of America</span></div>
<!-- /wp:yoast/job-location-country --></div>
<!-- /wp:yoast/office-location --></div>
<!-- /wp:yoast/job-location --></div>
<!-- /wp:column --></div>
<!-- /wp:columns -->
<!-- wp:separator {"align":"wide"} -->
<hr class="wp-block-separator alignwide"/>
<!-- /wp:separator -->
<!-- wp:paragraph {"style":{"typography":{"fontSize":24}}} -->
<p style="font-size:24px">Our company is growing! And were searching for an ambitious employee! Do you believe that a hard work is fundamental for your business? If you do, were probably looking for you!</p>
<!-- /wp:paragraph -->
<!-- wp:heading -->
<h2 id="h-about-the-job">About the job</h2>
<!-- /wp:heading -->
<!-- wp:yoast/job-description -->
<div class="yoast-job-block__description "><p data-id="description">Youll be part of a interdisciplinary team and together youll work on challenging, varied projects. Youll get the freedom and responsibility to reach your full potential!</p></div>
<!-- /wp:yoast/job-description -->
<!-- wp:heading -->
<h2 id="h-about-you">About you</h2>
<!-- /wp:heading -->
<!-- wp:paragraph -->
<p>Do you you have a passion for your job? Are you aware of current trends in your field? Do you love diving into details?</p>
<!-- /wp:paragraph -->
<!-- wp:paragraph -->
<p>Were looking for someone who is proactive, patient and smart, has an eye for details, and is a great communicator and motivator. If you also have a sense of humor and an enthusiasm for participating in discussions, youre probably a fit!</p>
<!-- /wp:paragraph -->
<!-- wp:yoast/job-requirements {"title_level":2} -->
<h2 data-id="title">Requirements</h2><div class="yoast-job-block__requirements "><ul data-id="requirements"><li>You enjoy working in a fast-paced team environment.</li><li>You dont ever think “good enough” is good enough.</li><li>You are available for 40 hours per week.</li><li>You speak and write English fluently (preferably with at least proficiency level C1).</li></ul></div>
<!-- /wp:yoast/job-requirements -->
<!-- wp:yoast/job-benefits {"title_level":2} -->
<h2 data-id="title">Benefits</h2><div class="yoast-job-block__benefits "><ul data-id="benefits"><li>A challenging job in a fast-growing, dynamic, ambitious and international atmosphere.</li><li>25 vacation days (on the base of 40 hours).</li><li>Youll be able to spend 10% of your salary on education.</li><li>We have a really fun company culture with lots of team building activities.</li></ul></div>
<!-- /wp:yoast/job-benefits -->
<!-- wp:paragraph -->
<p>Are you interested? Then please send your application to this@emailaddress.com before January 1, 2022. Do you have any questions? Well be happy to answer them.</p>
<!-- /wp:paragraph -->
<!-- wp:yoast/job-application-closing-date {"closingDate":"2022-01-01"} -->
<div class="yoast-job-block__application-closing-date "><span data-id="title">Apply before</span> <time datetime="2022-01-01">January 1, 2022</time></div>
<!-- /wp:yoast/job-application-closing-date -->
<!-- wp:separator {"align":"wide"} -->
<hr class="wp-block-separator alignwide"/>
<!-- /wp:separator -->
<!-- wp:paragraph -->
<p></p>
<!-- /wp:paragraph -->';
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace Yoast\WP\SEO\Premium\Exceptions;
use RuntimeException;
/**
* Exception for attempting a mutation on properties that are made readonly through magic getters and setters.
*/
class Forbidden_Property_Mutation_Exception extends RuntimeException {
/**
* Creates a Forbidden_Property_Mutation_Exception exception when an attempt is made
* to assign a value to an immutable property.
*
* @param string $property_name The name of the immutable property.
*
* @return Forbidden_Property_Mutation_Exception The exception.
*/
public static function cannot_set_because_property_is_immutable( $property_name ) {
return new self( \sprintf( 'Setting property $%s is not supported.', $property_name ) );
}
/**
* Creates a Forbidden_Property_Mutation_Exception exception when an attempt is made to unset an immutable property.
*
* @param string $property_name The name of the immutable property.
*
* @return Forbidden_Property_Mutation_Exception The exception.
*/
public static function cannot_unset_because_property_is_immutable( $property_name ) {
return new self( \sprintf( 'Unsetting property $%s is not supported.', $property_name ) );
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Yoast\WP\SEO\Premium\Exceptions\Remote_Request;
/**
* Class to manage a 400 - Bad request response.
*/
class Bad_Request_Exception extends Remote_Request_Exception {
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Yoast\WP\SEO\Premium\Exceptions\Remote_Request;
/**
* Class to manage a 403 - Forbidden response.
*/
class Forbidden_Exception extends Remote_Request_Exception {
}

View File

@@ -0,0 +1,12 @@
<?php
namespace Yoast\WP\SEO\Premium\Exceptions\Remote_Request;
/**
* Class to manage a 500 - Internal server error response.
*
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
*/
class Internal_Server_Error_Exception extends Remote_Request_Exception {
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Yoast\WP\SEO\Premium\Exceptions\Remote_Request;
/**
* Class to manage a 404 - not found response.
*/
class Not_Found_Exception extends Remote_Request_Exception {
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Yoast\WP\SEO\Premium\Exceptions\Remote_Request;
/**
* Class to manage a 402 - payment required response.
*/
class Payment_Required_Exception extends Remote_Request_Exception {
}

View File

@@ -0,0 +1,12 @@
<?php
namespace Yoast\WP\SEO\Premium\Exceptions\Remote_Request;
use Exception;
/**
* Class Remote_Request_Exception
*/
abstract class Remote_Request_Exception extends Exception {
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Yoast\WP\SEO\Premium\Exceptions\Remote_Request;
/**
* Class to manage a 408 - request timeout exception
*/
class Request_Timeout_Exception extends Remote_Request_Exception {
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Yoast\WP\SEO\Premium\Exceptions\Remote_Request;
/**
* Class to manage a 503 - service unavailable response.
*/
class Service_Unavailable_Exception extends Remote_Request_Exception {
}

View File

@@ -0,0 +1,12 @@
<?php
namespace Yoast\WP\SEO\Premium\Exceptions\Remote_Request;
/**
* Class to manage a 429 - Too many requests response.
*
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
*/
class Too_Many_Requests_Exception extends Remote_Request_Exception {
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Yoast\WP\SEO\Premium\Exceptions\Remote_Request;
/**
* Class to manage a 401 - unauthorized response.
*/
class Unauthorized_Exception extends Remote_Request_Exception {
}

View File

@@ -5,12 +5,7 @@
* @package Yoast\WP\SEO\Premium
*/
if ( ! defined( 'WPSEO_PREMIUM_VERSION' ) ) {
header( 'Status: 403 Forbidden' );
header( 'HTTP/1.1 403 Forbidden' );
exit();
}
use Yoast\WP\SEO\Premium\Addon_Installer;
use Yoast\WP\SEO\Premium\Main;
/**
@@ -25,7 +20,8 @@ function YoastSEOPremium() {
static $main;
if ( did_action( 'wpseo_loaded' ) ) {
if ( $main === null ) {
$should_load = Addon_Installer::is_yoast_seo_up_to_date();
if ( $main === null && $should_load ) {
// Ensure free is loaded as loading premium will fail without it.
YoastSEO();
$main = new Main();

View File

@@ -0,0 +1,352 @@
<?php
namespace Yoast\WP\SEO\Premium\Helpers;
use RuntimeException;
use WP_User;
use WPSEO_Utils;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\User_Helper;
use Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Bad_Request_Exception;
use Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Forbidden_Exception;
use Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Internal_Server_Error_Exception;
use Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Not_Found_Exception;
use Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Payment_Required_Exception;
use Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Request_Timeout_Exception;
use Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Service_Unavailable_Exception;
use Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Too_Many_Requests_Exception;
use Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Unauthorized_Exception;
/**
* Class AI_Generator_Helper
*
* @package Yoast\WP\SEO\Helpers
*/
class AI_Generator_Helper {
/**
* The API base URL.
*
* @var string
*/
protected $base_url = 'https://ai.yoa.st/api/v1';
/**
* The options helper.
*
* @var Options_Helper
*/
protected $options_helper;
/**
* The User helper.
*
* @var User_Helper
*/
protected $user_helper;
/**
* AI_Generator_Helper constructor.
*
* @codeCoverageIgnore It only sets dependencies.
*
* @param Options_Helper $options The options helper.
* @param User_Helper $user_helper The User helper.
*/
public function __construct( Options_Helper $options, User_Helper $user_helper ) {
$this->options_helper = $options;
$this->user_helper = $user_helper;
}
/**
* Generates a random code verifier for a user. The code verifier is used in communication with the Yoast AI API
* to ensure that the callback that is sent for both the token and refresh request are handled by the same site that requested the tokens.
* Each code verifier should only be used once.
* This all helps with preventing access tokens from one site to be sent to another and it makes a mitm attack more difficult to execute.
*
* @param \WP_User $user The WP user.
*
* @return string The code verifier.
*/
public function generate_code_verifier( WP_User $user ) {
$random_string = \substr( \str_shuffle( '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' ), 1, 10 );
return \hash( 'sha256', $user->user_email . $random_string );
}
/**
* Temporarily stores the code verifier. We expect the callback that consumes this verifier to reach us within a couple of seconds.
* So, we throw away the code after 5 minutes: when we know the callback isn't coming.
*
* @param int $user_id The user ID.
* @param string $code_verifier The code verifier.
*
* @return void
*/
public function set_code_verifier( int $user_id, string $code_verifier ): void {
$user_id_string = (string) $user_id;
\set_transient( "yoast_wpseo_ai_generator_code_verifier_$user_id_string", $code_verifier, ( \MINUTE_IN_SECONDS * 5 ) );
}
/**
* Retrieves the code verifier.
*
* @param int $user_id The user ID.
*
* @throws \RuntimeException Unable to retrieve the code verifier.
*
* @return string The code verifier.
*/
public function get_code_verifier( int $user_id ): string {
$user_id_string = (string) $user_id;
$code_verifier = \get_transient( "yoast_wpseo_ai_generator_code_verifier_$user_id_string" );
if ( ! \is_string( $code_verifier ) || $code_verifier === '' ) {
throw new RuntimeException( 'Unable to retrieve the code verifier.' );
}
return $code_verifier;
}
/**
* Deletes the code verifier.
*
* @param int $user_id The user ID.
*
* @return void
*/
public function delete_code_verifier( int $user_id ): void {
$user_id_string = (string) $user_id;
\delete_transient( "yoast_wpseo_ai_generator_code_verifier_$user_id_string" );
}
/**
* Gets the licence URL.
*
* @return string The licence URL.
*/
public function get_license_url() {
return WPSEO_Utils::get_home_url();
}
/**
* Gets the callback URL to be used by the API to send back the access token, refresh token and code challenge.
*
* @return array The callbacks URLs.
*/
public function get_callback_url() {
return \get_rest_url( null, 'yoast/v1/ai_generator/callback' );
}
/**
* Gets the callback URL to be used by the API to send back the refreshed JWTs once they expire.
*
* @return array The callbacks URLs.
*/
public function get_refresh_callback_url() {
return \get_rest_url( null, 'yoast/v1/ai_generator/refresh_callback' );
}
/**
* Performs the request using WordPress internals.
*
* @param string $action_path The path to the desired action.
* @param array $request_body The request body.
* @param array $request_headers The request headers.
*
* @throws Bad_Request_Exception When the request fails for any other reason.
* @throws Forbidden_Exception When the response code is 403.
* @throws Internal_Server_Error_Exception When the response code is 500.
* @throws Not_Found_Exception When the response code is 404.
* @throws Payment_Required_Exception When the response code is 402.
* @throws Request_Timeout_Exception When the response code is 408.
* @throws Service_Unavailable_Exception When the response code is 503.
* @throws Too_Many_Requests_Exception When the response code is 429.
* @throws Unauthorized_Exception When the response code is 401.
*
* @return object The response object.
*/
public function request( $action_path, $request_body = [], $request_headers = [] ) {
// Our API expects JSON.
// The request times out after 30 seconds.
$request_headers = \array_merge( $request_headers, [ 'Content-Type' => 'application/json' ] );
$request_arguments = [
'timeout' => 30,
// phpcs:ignore Yoast.Yoast.AlternativeFunctions.json_encode_wp_json_encode -- Reason: We don't want the debug/pretty possibility.
'body' => \wp_json_encode( $request_body ),
'headers' => $request_headers,
];
/**
* Filter: 'Yoast\WP\SEO\ai_api_url' - Replaces the default URL for the AI API with a custom one.
*
* Note: This is a Premium plugin-only hook.
*
* @since 21.0
* @internal
*
* @param string $url The default URL for the AI API.
*/
$api_url = \apply_filters( 'Yoast\WP\SEO\ai_api_url', $this->base_url );
$response = \wp_remote_post( $api_url . $action_path, $request_arguments );
if ( \is_wp_error( $response ) ) {
throw new Bad_Request_Exception( $response->get_error_message(), $response->get_error_code() );
}
[ $response_code, $response_message ] = $this->parse_response( $response );
switch ( $response_code ) {
case 200:
return (object) $response;
case 401:
throw new Unauthorized_Exception( $response_message, $response_code );
case 402:
throw new Payment_Required_Exception( $response_message, $response_code );
case 403:
throw new Forbidden_Exception( $response_message, $response_code );
case 404:
throw new Not_Found_Exception( $response_message, $response_code );
case 408:
throw new Request_Timeout_Exception( $response_message, $response_code );
case 429:
throw new Too_Many_Requests_Exception( $response_message, $response_code );
case 500:
throw new Internal_Server_Error_Exception( $response_message, $response_code );
case 503:
throw new Service_Unavailable_Exception( $response_message, $response_code );
default:
throw new Bad_Request_Exception( $response_message, $response_code );
}
}
/**
* Generates the list of 5 suggestions to return.
*
* @param object $response The response from the API.
*
* @return array The array of suggestions.
*/
public function build_suggestions_array( $response ): array {
$suggestions = [];
$json = \json_decode( $response->body );
if ( $json === null || ! isset( $json->choices ) ) {
return $suggestions;
}
foreach ( $json->choices as $suggestion ) {
$suggestions[] = $suggestion->text;
}
return $suggestions;
}
/**
* Parses the response from the API.
*
* @param array|\WP_Error $response The response from the API.
*
* @return array The response code and message.
*/
public function parse_response( $response ) {
$response_code = ( \wp_remote_retrieve_response_code( $response ) !== '' ) ? \wp_remote_retrieve_response_code( $response ) : 0;
$response_message = \esc_html( \wp_remote_retrieve_response_message( $response ) );
if ( $response_code !== 200 && $response_code !== 0 ) {
$json_body = \json_decode( \wp_remote_retrieve_body( $response ) );
if ( $json_body !== null ) {
$response_message = isset( $json_body->error_code ) ? $json_body->error_code : $this->map_message_to_code( $json_body->message );
}
}
return [ $response_code, $response_message ];
}
/**
* Checks whether the token has expired.
*
* @param string $jwt The JWT.
*
* @return bool Whether the token has expired.
*/
public function has_token_expired( string $jwt ): bool {
$parts = \explode( '.', $jwt );
if ( \count( $parts ) !== 3 ) {
// Headers, payload and signature parts are not detected.
return true;
}
// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode -- Reason: Decoding the payload of the JWT.
$payload = \base64_decode( $parts[1] );
$json = \json_decode( $payload );
if ( $json === null || ! isset( $json->exp ) ) {
return true;
}
return $json->exp < time();
}
/**
* Retrieves the access JWT.
*
* @param string $user_id The user ID.
*
* @throws \RuntimeException Unable to retrieve the access token.
*
* @return string The access JWT.
*/
public function get_access_token( string $user_id ): string {
$access_jwt = $this->user_helper->get_meta( $user_id, '_yoast_wpseo_ai_generator_access_jwt', true );
if ( ! \is_string( $access_jwt ) || $access_jwt === '' ) {
throw new RuntimeException( 'Unable to retrieve the access token.' );
}
return $access_jwt;
}
/**
* Retrieves the refresh JWT.
*
* @param string $user_id The user ID.
*
* @throws \RuntimeException Unable to retrieve the refresh token.
*
* @return string The access JWT.
*/
public function get_refresh_token( $user_id ) {
$refresh_jwt = $this->user_helper->get_meta( $user_id, '_yoast_wpseo_ai_generator_refresh_jwt', true );
if ( ! \is_string( $refresh_jwt ) || $refresh_jwt === '' ) {
throw new RuntimeException( 'Unable to retrieve the refresh token.' );
}
return $refresh_jwt;
}
/**
* Checks if the AI Generator feature is active.
*
* @return bool Whether the feature is active.
*/
public function is_ai_generator_enabled() {
return $this->options_helper->get( 'enable_ai_generator', false );
}
/**
* Maps the message to a code.
*
* @param string $message The message.
*
* @return string The code.
*/
private function map_message_to_code( $message ) {
if ( \strpos( $message, 'must NOT have fewer than 1 characters' ) !== false ) {
return 'NOT_ENOUGH_CONTENT';
}
if ( \strpos( $message, 'Client timeout' ) !== false ) {
return 'CLIENT_TIMEOUT';
}
if ( \strpos( $message, 'Server timeout' ) !== false ) {
return 'SERVER_TIMEOUT';
}
return $message;
}
}

View File

@@ -0,0 +1,108 @@
<?php
namespace Yoast\WP\SEO\Premium\Helpers;
/**
* Class Current_Page_Helper.
*/
class Current_Page_Helper {
/**
* Determine whether the current page is the homepage and shows posts.
*
* @return bool
*/
public function is_home_posts_page() {
return ( \is_home() && \get_option( 'show_on_front' ) !== 'page' );
}
/**
* Determine whether the current page is a static homepage.
*
* @return bool
*/
public function is_home_static_page() {
return ( \is_front_page() && \get_option( 'show_on_front' ) === 'page' && \is_page( \get_option( 'page_on_front' ) ) );
}
/**
* Determine whether this is the posts page, regardless of whether it's the frontpage or not.
*
* @return bool
*/
public function is_posts_page() {
return ( \is_home() && ! \is_front_page() );
}
/**
* Retrieves the current post id.
* Returns 0 if no post id is found.
*
* @return int The post id.
*/
public function get_current_post_id() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are not processing form information, We are casting to an integer.
if ( isset( $_GET['post'] ) && \is_string( $_GET['post'] ) && (int) \wp_unslash( $_GET['post'] ) > 0 ) {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are not processing form information, We are casting to an integer, also this is a helper function.
return (int) \wp_unslash( $_GET['post'] );
}
return 0;
}
/**
* Retrieves the current post type.
*
* @return string The post type.
*/
public function get_current_post_type() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information.
if ( isset( $_GET['post_type'] ) && \is_string( $_GET['post_type'] ) ) {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information.
return \sanitize_text_field( \wp_unslash( $_GET['post_type'] ) );
}
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Reason: should be done outside the helper function.
if ( isset( $_POST['post_type'] ) && \is_string( $_POST['post_type'] ) ) {
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Reason: should be done outside the helper function.
return \sanitize_text_field( \wp_unslash( $_POST['post_type'] ) );
}
$post_id = $this->get_current_post_id();
if ( $post_id ) {
return \get_post_type( $post_id );
}
return 'post';
}
/**
* Retrieves the current taxonomy.
*
* @return string The taxonomy.
*/
public function get_current_taxonomy() {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- doing a strict in_array check should be sufficient.
if ( ! isset( $_SERVER['REQUEST_METHOD'] ) || ! \in_array( $_SERVER['REQUEST_METHOD'], [ 'GET', 'POST' ], true ) ) {
return '';
}
// phpcs:ignore WordPress.Security.NonceVerification -- Reason: We are not processing form information.
if ( $_SERVER['REQUEST_METHOD'] === 'POST' ) {
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Reason: should be done outside the helper function.
if ( isset( $_POST['taxonomy'] ) && \is_string( $_POST['taxonomy'] ) ) {
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Reason: should be done outside the helper function.
return \sanitize_text_field( \wp_unslash( $_POST['taxonomy'] ) );
}
return '';
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information.
if ( isset( $_GET['taxonomy'] ) && \is_string( $_GET['taxonomy'] ) ) {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information.
return \sanitize_text_field( \wp_unslash( $_GET['taxonomy'] ) );
}
return '';
}
}

View File

@@ -1,11 +1,11 @@
<?php
namespace Yoast\WP\SEO\Helpers;
namespace Yoast\WP\SEO\Premium\Helpers;
use Yoast\WP\SEO\Helpers\Options_Helper;
/**
* Class Prominent_Words_Helper.
*
* @package Yoast\WP\SEO\Helpers
*/
class Prominent_Words_Helper {

View File

@@ -0,0 +1,30 @@
<?php
namespace Yoast\WP\SEO\Premium\Helpers;
use Yoast\WP\SEO\Premium\Addon_Installer;
/**
* Helper class to check the status of the Free and Premium versions.
*/
class Version_Helper {
/**
* Checks whether Free is active and set to a version later than the minimum required.
*
* @return bool
*/
public function is_free_upgraded() {
return ( \defined( 'WPSEO_VERSION' ) && \version_compare( \WPSEO_VERSION, Addon_Installer::MINIMUM_YOAST_SEO_VERSION . '-RC0', '>' ) );
}
/**
* Checks whether a new update is available for Premium.
*
* @return bool
*/
public function is_premium_update_available() {
$plugin_updates = \get_plugin_updates();
return isset( $plugin_updates[ WPSEO_PREMIUM_BASENAME ] );
}
}

View File

@@ -0,0 +1,123 @@
<?php
namespace Yoast\WP\SEO\Premium\Initializers;
use Yoast\WP\SEO\Conditionals\No_Conditionals;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Initializers\Initializer_Interface;
/**
* Index_Now_Key class
*/
class Index_Now_Key implements Initializer_Interface {
use No_Conditionals;
/**
* The options helper.
*
* @var Options_Helper
*/
private $options_helper;
/**
* Holds the IndexNow key.
*
* @var string
*/
private $key;
/**
* Index_Now_Key initializer constructor.
*
* @param Options_Helper $options_helper The option helper.
*/
public function __construct( Options_Helper $options_helper ) {
$this->options_helper = $options_helper;
}
/**
* Initializes the integration.
*
* This is the place to register hooks and filters.
*
* @return void
*/
public function initialize() {
\add_action( 'init', [ $this, 'add_rewrite_rule' ], 1 );
\add_action( 'plugins_loaded', [ $this, 'load' ], 15 );
}
/**
* Loads the integration.
*
* @return void
*/
public function load() {
if ( $this->options_helper->get( 'enable_index_now' ) === false ) {
return;
}
$this->key = $this->options_helper->get( 'index_now_key' );
if ( $this->key === '' ) {
$this->generate_key();
}
\add_action( 'wp', [ $this, 'output_key' ], 0 );
}
/**
* Adds the rewrite rule for the IndexNow key txt file.
*
* @return void
*/
public function add_rewrite_rule() {
if ( $this->options_helper->get( 'enable_index_now' ) !== true ) {
return;
}
global $wp;
$wp->add_query_var( 'yoast_index_now_key' );
\add_rewrite_rule( '^yoast-index-now-([a-zA-Z0-9-]+)\.txt$', 'index.php?yoast_index_now_key=$matches[1]', 'top' );
}
/**
* Outputs the key when it matches the key in the database.
*
* @return void
*/
public function output_key() {
$key_in_url = \get_query_var( 'yoast_index_now_key' );
if ( empty( $key_in_url ) ) {
return;
}
if ( $key_in_url === $this->key ) {
// Remove all headers.
\header_remove();
// Only send plain text header.
\header( 'Content-Type: text/plain;charset=UTF-8' );
echo \esc_html( $this->key );
die;
}
// Trying keys? Good luck.
global $wp_query;
$wp_query->set_404();
}
/**
* Generates an IndexNow key.
*
* Adapted from wp_generate_password to include dash (-) and not be filtered.
*
* @return void
*/
private function generate_key() {
$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-';
for ( $i = 0; $i < 100; $i++ ) {
$this->key .= \substr( $chars, \wp_rand( 0, ( \strlen( $chars ) - 1 ) ), 1 );
}
$this->options_helper->set( 'index_now_key', $this->key );
}
}

View File

@@ -0,0 +1,96 @@
<?php
namespace Yoast\WP\SEO\Premium\Initializers;
use Yoast\WP\SEO\Conditionals\Admin\Yoast_Admin_Conditional;
use Yoast\WP\SEO\Helpers\Current_Page_Helper;
use Yoast\WP\SEO\Initializers\Initializer_Interface;
use Yoast\WP\SEO\Introductions\Application\Current_Page_Trait;
use Yoast\WP\SEO\Introductions\Domain\Introduction_Interface;
/**
* Initializes Premium introductions.
*/
class Introductions_Initializer implements Initializer_Interface {
const SCRIPT_HANDLE = 'wp-seo-premium-introductions';
use Current_Page_Trait;
/**
* Holds the current page helper.
*
* @var Current_Page_Helper
*/
private $current_page_helper;
/**
* Holds the introductions.
*
* @var Introduction_Interface
*/
private $introductions;
/**
* Constructs the new features integration.
*
* @param Current_Page_Helper $current_page_helper The current page helper.
* @param Introduction_Interface ...$introductions The introductions.
*/
public function __construct( Current_Page_Helper $current_page_helper, Introduction_Interface ...$introductions ) {
$this->current_page_helper = $current_page_helper;
$this->introductions = $introductions;
}
/**
* Returns the conditionals based in which this loadable should be active.
*
* In this case: when on an admin page.
*/
public static function get_conditionals() {
return [ Yoast_Admin_Conditional::class ];
}
/**
* Registers the action to enqueue the needed script(s).
*
* @return void
*/
public function initialize() {
if ( $this->is_on_installation_page() ) {
return;
}
\add_filter( 'wpseo_introductions', [ $this, 'add_introductions' ] );
\add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] );
}
/**
* Adds the Premium introductions.
*
* @param Introduction_Interface[] $introductions The introductions.
*
* @return array The merged introductions.
*/
public function add_introductions( $introductions ) {
// Safety check and bail.
if ( ! \is_array( $introductions ) ) {
return $introductions;
}
return \array_merge( $introductions, $this->introductions );
}
/**
* Enqueue the workouts app.
*/
public function enqueue_assets() {
\wp_enqueue_script( self::SCRIPT_HANDLE );
\wp_localize_script(
self::SCRIPT_HANDLE,
'wpseoPremiumIntroductions',
[
'pluginUrl' => \plugins_url( '', \WPSEO_PREMIUM_FILE ),
]
);
}
}

View File

@@ -61,7 +61,8 @@ class Plugin implements Initializer_Interface {
public function wpseo_premium_deactivate() {
\do_action( 'wpseo_register_capabilities_premium' );
WPSEO_Capability_Manager_Factory::get( 'premium' )->remove();
$this->options_helper->set( 'tracking', false );
if ( $this->options_helper->get( 'toggled_tracking' ) !== true ) {
$this->options_helper->set( 'tracking', false );
}
}
}

View File

@@ -1,11 +1,13 @@
<?php
namespace Yoast\WP\SEO\Initializers;
namespace Yoast\WP\SEO\Premium\Initializers;
use WP_Query;
use WPSEO_Premium_Redirect_Option;
use WPSEO_Redirect_Option;
use WPSEO_Redirect_Util;
use Yoast\WP\SEO\Conditionals\Front_End_Conditional;
use Yoast\WP\SEO\Initializers\Initializer_Interface;
/**
* Class Redirect_Handler.
@@ -33,20 +35,6 @@ class Redirect_Handler implements Initializer_Interface {
*/
protected $is_redirected = false;
/**
* The options where the URL redirects are stored.
*
* @var string
*/
private $normal_option_name = 'wpseo-premium-redirects-export-plain';
/**
* The option name where the regex redirects are stored.
*
* @var string
*/
private $regex_option_name = 'wpseo-premium-redirects-export-regex';
/**
* The URL that is called at the moment.
*
@@ -81,15 +69,15 @@ class Redirect_Handler implements Initializer_Interface {
return;
}
// Set the requested URL.
$this->set_request_url();
// Check the normal redirects.
$this->handle_normal_redirects( $this->request_url );
// Check the regex redirects.
if ( $this->is_redirected() === false ) {
$this->handle_regex_redirects();
if ( ! \function_exists( 'is_plugin_active_for_network' ) ) {
require_once \ABSPATH . 'wp-admin/includes/plugin.php';
}
// If the plugin is network activated, we wait for the plugins to be loaded before initializing.
if ( \is_plugin_active_for_network( \WPSEO_PREMIUM_BASENAME ) ) {
\add_action( 'plugins_loaded', [ $this, 'handle_redirects' ], 16 );
}
else {
$this->handle_redirects();
}
}
@@ -174,7 +162,7 @@ class Redirect_Handler implements Initializer_Interface {
*/
protected function handle_normal_redirects( $request_url ) {
// Setting the redirects.
$redirects = $this->get_redirects( $this->normal_option_name );
$redirects = $this->get_redirects( WPSEO_Redirect_Option::OPTION_PLAIN );
$this->redirects = $this->normalize_redirects( $redirects );
$request_url = $this->normalize_url( $request_url );
@@ -218,7 +206,7 @@ class Redirect_Handler implements Initializer_Interface {
*/
protected function handle_regex_redirects() {
// Setting the redirects.
$this->redirects = $this->get_redirects( $this->regex_option_name );
$this->redirects = $this->get_redirects( WPSEO_Redirect_Option::OPTION_REGEX );
foreach ( $this->redirects as $regex => $redirect ) {
// Check if the URL matches the $regex.
@@ -270,7 +258,11 @@ class Redirect_Handler implements Initializer_Interface {
* @return array Returns the redirects for the given option.
*/
protected function get_redirects( $option ) {
$redirects = $this->get_redirects_from_options();
static $redirects;
if ( ! isset( $redirects[ $option ] ) ) {
$redirects[ $option ] = \get_option( $option, false );
}
if ( ! empty( $redirects[ $option ] ) ) {
return $redirects[ $option ];
@@ -356,23 +348,19 @@ class Redirect_Handler implements Initializer_Interface {
}
/**
* Gets the request URI, with fallback for super global.
* Gets the request URI.
*
* @return string
*/
protected function get_request_uri() {
$options = [ 'options' => [ 'default' => '' ] ];
$request_uri = \filter_input( \INPUT_SERVER, 'REQUEST_URI', \FILTER_SANITIZE_URL, $options );
$request_uri = '';
// Because there isn't an usable value, try the fallback.
if ( empty( $request_uri ) && isset( $_SERVER['REQUEST_URI'] ) ) {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- this value is compared. I don't want to change the behavior.
$request_uri = \filter_var( $_SERVER['REQUEST_URI'], \FILTER_SANITIZE_URL, $options );
if ( isset( $_SERVER['REQUEST_URI'] ) ) {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- We sanitize after decoding.
$request_uri = \sanitize_text_field( \rawurldecode( \wp_unslash( $_SERVER['REQUEST_URI'] ) ) );
}
$request_uri = $this->strip_subdirectory( $request_uri );
return \rawurldecode( $request_uri );
return $this->strip_subdirectory( $request_uri );
}
/**
@@ -564,33 +552,6 @@ class Redirect_Handler implements Initializer_Interface {
return \home_url();
}
/**
* Returns the redirects from the option table in the database.
*
* @return array The stored redirects.
*/
protected function get_redirects_from_options() {
global $wpdb;
static $redirects;
if ( $redirects !== null ) {
return $redirects;
}
// The code below is needed because we used to not autoload our redirect options. This fixes that.
$all_options = \wp_cache_get( 'alloptions', 'options' );
foreach ( [ $this->normal_option_name, $this->regex_option_name ] as $option ) {
$redirects[ $option ] = isset( $all_options[ $option ] ) ? \maybe_unserialize( $all_options[ $option ] ) : false;
if ( $redirects[ $option ] === false ) {
$redirects[ $option ] = \get_option( $option, false );
// phpcs:ignore WordPress.DB.DirectDatabaseQuery -- Normal methods only work if the option value has changed.
$wpdb->update( $wpdb->options, [ 'autoload' => 'yes' ], [ 'option_name' => $option ] );
}
}
return $redirects;
}
/**
* Sets the hook for setting the template include. This is the file that we want to show.
*
@@ -691,4 +652,22 @@ class Redirect_Handler implements Initializer_Interface {
protected function get_query_template( $filename ) {
return \get_query_template( $filename );
}
/**
* Actually handles redirects.
*
* @return void
*/
public function handle_redirects() {
// Set the requested URL.
$this->set_request_url();
// Check the normal redirects.
$this->handle_normal_redirects( $this->request_url );
// Check the regex redirects.
if ( $this->is_redirected() === false ) {
$this->handle_regex_redirects();
}
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Yoast\WP\SEO\Premium\Initializers;
use Automattic\WooCommerce\Utilities\FeaturesUtil;
use Yoast\WP\SEO\Conditionals\No_Conditionals;
use Yoast\WP\SEO\Initializers\Initializer_Interface;
/**
* Declares compatibility with the WooCommerce HPOS feature.
*/
class Woocommerce implements Initializer_Interface {
use No_Conditionals;
/**
* Hooks into WooCommerce.
*/
public function initialize() {
\add_action( 'before_woocommerce_init', [ $this, 'declare_custom_order_tables_compatibility' ] );
}
/**
* Declares compatibility with the WooCommerce HPOS feature.
*/
public function declare_custom_order_tables_compatibility() {
if ( \class_exists( FeaturesUtil::class ) ) {
FeaturesUtil::declare_compatibility( 'custom_order_tables', \WPSEO_PREMIUM_FILE, true );
}
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace Yoast\WP\SEO\Premium\Initializers;
use WP_CLI;
use Yoast\WP\SEO\Conditionals\No_Conditionals;
use Yoast\WP\SEO\Initializers\Initializer_Interface;
/**
* Wp_Cli_Initializer class
*/
class Wp_Cli_Initializer implements Initializer_Interface {
use No_Conditionals;
/**
* Initializes the integration.
*
* This is the place to register hooks and filters.
*
* @return void
*/
public function initialize() {
if ( \defined( 'WP_CLI' ) && \WP_CLI ) {
\add_action( 'plugins_loaded', [ $this, 'wpseo_cli_init' ], 20 );
}
}
/**
* Initialize the WP-CLI integration.
*
* The WP-CLI integration needs PHP 5.3 support, which should be automatically
* enforced by the check for the WP_CLI constant. As WP-CLI itself only runs
* on PHP 5.3+, the constant should only be set when requirements are met.
*/
public function wpseo_cli_init() {
WP_CLI::add_command(
'yoast redirect list',
'WPSEO_CLI_Redirect_List_Command',
[ 'before_invoke' => 'WPSEO_CLI_Premium_Requirement::enforce' ]
);
WP_CLI::add_command(
'yoast redirect create',
'WPSEO_CLI_Redirect_Create_Command',
[ 'before_invoke' => 'WPSEO_CLI_Premium_Requirement::enforce' ]
);
WP_CLI::add_command(
'yoast redirect update',
'WPSEO_CLI_Redirect_Update_Command',
[ 'before_invoke' => 'WPSEO_CLI_Premium_Requirement::enforce' ]
);
WP_CLI::add_command(
'yoast redirect delete',
'WPSEO_CLI_Redirect_Delete_Command',
[ 'before_invoke' => 'WPSEO_CLI_Premium_Requirement::enforce' ]
);
WP_CLI::add_command(
'yoast redirect has',
'WPSEO_CLI_Redirect_Has_Command',
[ 'before_invoke' => 'WPSEO_CLI_Premium_Requirement::enforce' ]
);
WP_CLI::add_command(
'yoast redirect follow',
'WPSEO_CLI_Redirect_Follow_Command',
[ 'before_invoke' => 'WPSEO_CLI_Premium_Requirement::enforce' ]
);
}
}

View File

@@ -2,9 +2,9 @@
namespace Yoast\WP\SEO\Premium\Integrations;
use Yoast\WP\SEO\Conditionals\Open_Graph_Conditional;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Premium\Conditionals\Social_Templates_Conditional;
/**
* Class Abstract_OpenGraph_Integration.
@@ -61,7 +61,7 @@ abstract class Abstract_OpenGraph_Integration implements Integration_Interface {
* @return array
*/
public static function get_conditionals() {
return [ Social_Templates_Conditional::class ];
return [ Open_Graph_Conditional::class ];
}
/**

View File

@@ -0,0 +1,134 @@
<?php
namespace Yoast\WP\SEO\Premium\Integrations\Admin;
use WPSEO_Addon_Manager;
use WPSEO_Admin_Asset_Manager;
use Yoast\WP\SEO\Conditionals\User_Profile_Conditional;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\User_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Introductions\Infrastructure\Wistia_Embed_Permission_Repository;
/**
* Ai_Consent_Integration class.
*/
class Ai_Consent_Integration implements Integration_Interface {
/**
* Represents the admin asset manager.
*
* @var WPSEO_Admin_Asset_Manager
*/
private $asset_manager;
/**
* Represents the add-on manager.
*
* @var WPSEO_Addon_Manager
*/
private $addon_manager;
/**
* Represents the options manager.
*
* @var Options_Helper
*/
private $options_helper;
/**
* Represents the user helper.
*
* @var User_Helper
*/
private $user_helper;
/**
* Represents the wistia embed permission repository.
*
* @var Wistia_Embed_Permission_Repository
*/
private $wistia_embed_permission_repository;
/**
* Returns the conditionals based in which this loadable should be active.
*
* @return array
*/
public static function get_conditionals() {
return [ User_Profile_Conditional::class ];
}
/**
* Constructs the class.
*
* @param WPSEO_Admin_Asset_Manager $asset_manager The admin asset manager.
* @param WPSEO_Addon_Manager $addon_manager The addon manager.
* @param Options_Helper $options_helper The options helper.
* @param User_Helper $user_helper The user helper.
* @param Wistia_Embed_Permission_Repository $wistia_embed_permission_repository The wistia embed permission
* repository.
*/
public function __construct(
WPSEO_Admin_Asset_Manager $asset_manager,
WPSEO_Addon_Manager $addon_manager,
Options_Helper $options_helper,
User_Helper $user_helper,
Wistia_Embed_Permission_Repository $wistia_embed_permission_repository
) {
$this->asset_manager = $asset_manager;
$this->addon_manager = $addon_manager;
$this->options_helper = $options_helper;
$this->user_helper = $user_helper;
$this->wistia_embed_permission_repository = $wistia_embed_permission_repository;
}
/**
* Initializes the integration.
*
* This is the place to register hooks and filters.
*
* @return void
*/
public function register_hooks() {
// Hide AI feature option in user profile if the user is not allowed to use it.
if ( current_user_can( 'edit_posts' ) ) {
\add_action( 'wpseo_user_profile_additions', [ $this, 'render_user_profile' ], 12 );
}
\add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ], 11 );
}
/**
* Enqueues the required assets.
*
* @return void
*/
public function enqueue_assets() {
$this->asset_manager->enqueue_style( 'premium-ai-generator' );
\wp_enqueue_script( 'wp-seo-premium-manage-ai-consent-button' );
$user_id = $this->user_helper->get_current_user_id();
\wp_localize_script(
'wp-seo-premium-manage-ai-consent-button',
'wpseoPremiumManageAiConsentButton',
[
'hasConsent' => $this->user_helper->get_meta( $user_id, '_yoast_wpseo_ai_consent', true ),
// Note: this is passing the Free plugin URL! As the image is located in there.
'pluginUrl' => \plugins_url( '', \WPSEO_FILE ),
'wistiaEmbedPermission' => $this->wistia_embed_permission_repository->get_value_for_user( $user_id ),
]
);
}
/**
* Renders the AI consent button for the user profile.
*
* @return void
*/
public function render_user_profile() {
echo '<label for="ai-generator-consent-button">',
esc_html__( 'AI features', 'wordpress-seo-premium' ),
'</label>',
'<div id="ai-generator-consent" style="display:inline-block; margin-top: 28px; padding-left:5px;"></div>';
}
}

View File

@@ -0,0 +1,111 @@
<?php
namespace Yoast\WP\SEO\Premium\Integrations\Admin;
use WPSEO_Addon_Manager;
use WPSEO_Admin_Asset_Manager;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\User_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Premium\Conditionals\Ai_Editor_Conditional;
/**
* Ai_Generator_Integration class.
*/
class Ai_Generator_Integration implements Integration_Interface {
/**
* Represents the admin asset manager.
*
* @var WPSEO_Admin_Asset_Manager
*/
private $asset_manager;
/**
* Represents the add-on manager.
*
* @var WPSEO_Addon_Manager
*/
private $addon_manager;
/**
* Represents the options manager.
*
* @var Options_Helper
*/
private $options_helper;
/**
* Represents the user helper.
*
* @var User_Helper
*/
private $user_helper;
/**
* Returns the conditionals based in which this loadable should be active.
*
* @return array
*/
public static function get_conditionals() {
return [ Ai_Editor_Conditional::class ];
}
/**
* Constructs the class.
*
* @param WPSEO_Admin_Asset_Manager $asset_manager The admin asset manager.
* @param WPSEO_Addon_Manager $addon_manager The addon manager.
* @param Options_Helper $options_helper The options helper.
* @param User_Helper $user_helper The user helper.
*/
public function __construct(
WPSEO_Admin_Asset_Manager $asset_manager,
WPSEO_Addon_Manager $addon_manager,
Options_Helper $options_helper,
User_Helper $user_helper
) {
$this->asset_manager = $asset_manager;
$this->addon_manager = $addon_manager;
$this->options_helper = $options_helper;
$this->user_helper = $user_helper;
}
/**
* Initializes the integration.
*
* This is the place to register hooks and filters.
*
* @return void
*/
public function register_hooks() {
if ( ! $this->options_helper->get( 'enable_ai_generator', false ) ) {
return;
}
\add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] );
// Enqueue after Elementor_Premium integration, which re-registers the assets.
\add_action( 'elementor/editor/before_enqueue_scripts', [ $this, 'enqueue_assets' ], 11 );
}
/**
* Enqueues the required assets.
*
* @return void
*/
public function enqueue_assets() {
\wp_enqueue_script( 'wp-seo-premium-ai-generator' );
\wp_localize_script(
'wp-seo-premium-ai-generator',
'wpseoPremiumAiGenerator',
[
'adminUrl' => \admin_url( 'admin.php' ),
'hasConsent' => $this->user_helper->get_meta( $this->user_helper->get_current_user_id(), '_yoast_wpseo_ai_consent', true ),
'hasValidSubscription' => $this->addon_manager->has_valid_subscription( WPSEO_Addon_Manager::PREMIUM_SLUG ),
'pluginUrl' => \plugins_url( '', \WPSEO_PREMIUM_FILE ),
'postType' => \get_post_type(),
]
);
$this->asset_manager->enqueue_style( 'premium-ai-generator' );
}
}

View File

@@ -0,0 +1,226 @@
<?php
namespace Yoast\WP\SEO\Premium\Integrations\Admin;
use WP_Query;
use wpdb;
use WPSEO_Admin_Asset_Manager;
use Yoast\WP\Lib\Model;
use Yoast\WP\SEO\Conditionals\Admin\Posts_Overview_Or_Ajax_Conditional;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Helpers\Post_Type_Helper;
use Yoast\WP\SEO\Integrations\Admin\Admin_Columns_Cache_Integration;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Premium\Conditionals\Cornerstone_Enabled_Conditional;
use Yoast\WP\SEO\Premium\Presenters\Icons\Checkmark_Icon_Presenter;
use Yoast\WP\SEO\Premium\Presenters\Icons\Cross_Icon_Presenter;
/**
* Cornerstone_Column_Integration class.
*/
class Cornerstone_Column_Integration implements Integration_Interface {
/**
* Name of the column.
*
* @var string
*/
const CORNERSTONE_COLUMN_NAME = 'wpseo-cornerstone';
/**
* The post type helper.
*
* @var Post_Type_Helper
*/
protected $post_type_helper;
/**
* The database object.
*
* @var wpdb
*/
protected $wpdb;
/**
* The admin columns cache.
*
* @var Admin_Columns_Cache_Integration
*/
protected $admin_columns_cache;
/**
* {@inheritDoc}
*/
public static function get_conditionals() {
return [
Admin_Conditional::class,
Posts_Overview_Or_Ajax_Conditional::class,
Cornerstone_Enabled_Conditional::class,
];
}
/**
* Cornerstone_Column_Integration constructor
*
* @codeCoverageIgnore
*
* @param Post_Type_Helper $post_type_helper The post type helper.
* @param wpdb $wpdb The wpdb object.
* @param Admin_Columns_Cache_Integration $admin_columns_cache The admin columns cache.
*/
public function __construct(
Post_Type_Helper $post_type_helper,
wpdb $wpdb,
Admin_Columns_Cache_Integration $admin_columns_cache
) {
$this->post_type_helper = $post_type_helper;
$this->wpdb = $wpdb;
$this->admin_columns_cache = $admin_columns_cache;
}
/**
* {@inheritDoc}
*/
public function register_hooks() {
\add_filter( 'posts_clauses', [ $this, 'order_by_cornerstone' ], 1, 2 );
\add_action( 'admin_init', [ $this, 'register_init_hooks' ] );
// Adds a filter to exclude the attachments from the cornerstone column.
\add_filter( 'wpseo_cornerstone_column_post_types', [ 'WPSEO_Post_Type', 'filter_attachment_post_type' ] );
\add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] );
}
/**
* Register hooks that need to be registered after `init` due to all post types not yet being registered.
*/
public function register_init_hooks() {
$public_post_types = \apply_filters( 'wpseo_cornerstone_column_post_types', $this->post_type_helper->get_accessible_post_types() );
if ( ! \is_array( $public_post_types ) || empty( $public_post_types ) ) {
return;
}
foreach ( $public_post_types as $post_type ) {
\add_filter( 'manage_' . $post_type . '_posts_columns', [ $this, 'add_cornerstone_column' ] );
\add_action( 'manage_' . $post_type . '_posts_custom_column', [ $this, 'column_content' ], 10, 2 );
\add_filter( 'manage_edit-' . $post_type . '_sortable_columns', [ $this, 'column_sort' ] );
}
}
/**
* Enqueues the assets needed for the integration to work.
*
* @return void
*/
public function enqueue_assets() {
\wp_enqueue_style( WPSEO_Admin_Asset_Manager::PREFIX . 'premium-post-overview' );
}
/**
* Adds the columns for the post overview.
*
* @param array $columns Array with columns.
*
* @return array The extended array with columns.
*/
public function add_cornerstone_column( $columns ) {
if ( ! \is_array( $columns ) ) {
return $columns;
}
$columns[ self::CORNERSTONE_COLUMN_NAME ] = \sprintf(
'<span class="yoast-column-cornerstone yoast-column-header-has-tooltip" data-tooltip-text="%1$s"><span class="screen-reader-text">%2$s</span></span>',
\esc_attr__( 'Is this cornerstone content?', 'wordpress-seo-premium' ),
/* translators: Hidden accessibility text. */
\esc_html__( 'Cornerstone content', 'wordpress-seo-premium' )
);
return $columns;
}
/**
* Modifies the query pieces to allow ordering column by cornerstone.
*
* @param array $pieces Array of Query pieces.
* @param WP_Query $query The Query on which to apply.
*
* @return array
*/
public function order_by_cornerstone( $pieces, $query ) {
if ( $query->get( 'orderby' ) !== self::CORNERSTONE_COLUMN_NAME ) {
return $pieces;
}
return $this->build_sort_query_pieces( $pieces, $query );
}
/**
* Builds the pieces for a sorting query.
*
* @param array $pieces Array of Query pieces.
* @param WP_Query $query The Query on which to apply.
*
* @return array Modified Query pieces.
*/
protected function build_sort_query_pieces( $pieces, $query ) {
// We only want our code to run in the main WP query.
if ( ! $query->is_main_query() ) {
return $pieces;
}
// Get the order query variable - ASC or DESC.
$order = \strtoupper( $query->get( 'order' ) );
// Make sure the order setting qualifies. If not, set default as ASC.
if ( ! \in_array( $order, [ 'ASC', 'DESC' ], true ) ) {
$order = 'ASC';
}
$table = Model::get_table_name( 'Indexable' );
$pieces['join'] .= " LEFT JOIN $table AS yoast_indexable ON yoast_indexable.object_id = {$this->wpdb->posts}.ID AND yoast_indexable.object_type = 'post' ";
$pieces['orderby'] = "yoast_indexable.is_cornerstone $order, FIELD( {$this->wpdb->posts}.post_status, 'publish' ) $order, {$pieces['orderby']}";
return $pieces;
}
/**
* Displays the column content for the given column.
*
* @param string $column_name Column to display the content for.
* @param int $post_id Post to display the column content for.
*/
public function column_content( $column_name, $post_id ) {
$indexable = $this->admin_columns_cache->get_indexable( $post_id );
// Nothing to output if we don't have the value.
if ( empty( $indexable ) ) {
return;
}
// phpcs:disable WordPress.Security.EscapeOutput -- Reason: The Icons contains safe svg.
if ( $column_name === self::CORNERSTONE_COLUMN_NAME ) {
if ( $indexable->is_cornerstone === true ) {
echo new Checkmark_Icon_Presenter( 20 );
return;
}
echo new Cross_Icon_Presenter( 20 );
}
// phpcs:enable
}
/**
* Sets the sortable columns.
*
* @param array $columns Array with sortable columns.
*
* @return array The extended array with sortable columns.
*/
public function column_sort( $columns ) {
$columns[ self::CORNERSTONE_COLUMN_NAME ] = self::CORNERSTONE_COLUMN_NAME;
return $columns;
}
}

View File

@@ -0,0 +1,139 @@
<?php
namespace Yoast\WP\SEO\Premium\Integrations\Admin;
use WPSEO_Admin_Asset_Manager;
use WPSEO_Taxonomy_Meta;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Premium\Conditionals\Cornerstone_Enabled_Conditional;
use Yoast\WP\SEO\Premium\Presenters\Icons\Checkmark_Icon_Presenter;
use Yoast\WP\SEO\Premium\Presenters\Icons\Cross_Icon_Presenter;
use Yoast\WP\SEO\Premium\Conditionals\Term_Overview_Or_Ajax_Conditional;
use Yoast\WP\SEO\Premium\Helpers\Current_Page_Helper;
/**
* Cornerstone_Taxonomy_Column_Integration class.
*
* phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
*/
class Cornerstone_Taxonomy_Column_Integration implements Integration_Interface {
/**
* Name of the column.
*
* @var string
*/
const CORNERSTONE_COLUMN_NAME = 'wpseo-cornerstone';
/**
* Holds the Current_Page_Helper instance.
*
* @var Current_Page_Helper
*/
private $current_page_helper;
/**
* Returns the posted/get taxonomy value if it is set.
*
* @param Current_Page_Helper $current_page_helper The Current_Page_Helper.
*/
public function __construct( Current_Page_Helper $current_page_helper ) {
$this->current_page_helper = $current_page_helper;
}
/**
* {@inheritDoc}
*/
public static function get_conditionals() {
return [
Admin_Conditional::class,
Term_Overview_Or_Ajax_Conditional::class,
Cornerstone_Enabled_Conditional::class,
];
}
/**
* {@inheritDoc}
*/
public function register_hooks() {
\add_action( 'admin_init', [ $this, 'register_init_hooks' ] );
\add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] );
}
/**
* Register hooks that need to be registered after `init` due to all post types not yet being registered.
*/
public function register_init_hooks() {
$taxonomy = $this->current_page_helper->get_current_taxonomy();
$is_product = $this->current_page_helper->get_current_post_type() === 'product';
$is_product_cat = $taxonomy === 'product_cat';
$is_product_tag = $taxonomy === 'product_tag';
if ( ( $is_product && ( $is_product_cat || $is_product_tag ) ) || ( ! $is_product && $taxonomy ) ) {
\add_filter( 'manage_edit-' . $taxonomy . '_columns', [ $this, 'add_cornerstone_column' ] );
\add_filter( 'manage_' . $taxonomy . '_custom_column', [ $this, 'column_content' ], 10, 3 );
}
}
/**
* Enqueues the assets needed for the integration to work.
*
* @return void
*/
public function enqueue_assets() {
\wp_enqueue_style( WPSEO_Admin_Asset_Manager::PREFIX . 'premium-post-overview' );
}
/**
* Adds the cornerstone column for the term overview.
*
* @param array $columns Array with columns.
*
* @return array The extended array with columns.
*/
public function add_cornerstone_column( $columns ) {
if ( ! \is_array( $columns ) ) {
return $columns;
}
$columns[ self::CORNERSTONE_COLUMN_NAME ] = \sprintf(
'<span class="yoast-tooltip yoast-tooltip-n yoast-tooltip-alt" data-label="%1$s">
<span class="yoast-column-cornerstone yoast-column-header-has-tooltip">
<span class="screen-reader-text">%2$s</span>
</span>
</span>',
\esc_attr__( 'Is this cornerstone content?', 'wordpress-seo-premium' ),
/* translators: Hidden accessibility text. */
\esc_html__( 'Cornerstone content', 'wordpress-seo-premium' )
);
return $columns;
}
/**
* Displays the column content for the given column.
*
* @param string $content The current content of the column.
* @param string $column_name The name of the column.
* @param int $term_id ID of requested taxonomy.
*
* @return string
*/
public function column_content( $content, $column_name, $term_id ) {
$is_cornerstone = (int) WPSEO_Taxonomy_Meta::get_term_meta( $term_id, $this->current_page_helper->get_current_taxonomy(), 'is_cornerstone' );
if ( $column_name === self::CORNERSTONE_COLUMN_NAME ) {
if ( $is_cornerstone ) {
// phpcs:disable WordPress.Security.EscapeOutput -- Reason: The Icons contains safe svg.
echo new Checkmark_Icon_Presenter( 20 );
return;
}
echo new Cross_Icon_Presenter( 20 );
}
return $content;
}
}

View File

@@ -0,0 +1,229 @@
<?php
namespace Yoast\WP\SEO\Premium\Integrations\Admin;
use WP_Query;
use wpdb;
use WPSEO_Admin_Asset_Manager;
use Yoast\WP\Lib\Model;
use Yoast\WP\SEO\Conditionals\Admin\Posts_Overview_Or_Ajax_Conditional;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Helpers\Post_Type_Helper;
use Yoast\WP\SEO\Helpers\Score_Icon_Helper;
use Yoast\WP\SEO\Integrations\Admin\Admin_Columns_Cache_Integration;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Premium\Conditionals\Inclusive_Language_Enabled_Conditional;
/**
* Inclusive_Language_Column_Integration class.
*
* phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
*/
class Inclusive_Language_Column_Integration implements Integration_Interface {
/**
* Name of the column.
*
* @var string
*/
const INCLUSIVE_LANGUAGE_COLUMN_NAME = 'wpseo-inclusive-language';
/**
* The post type helper.
*
* @var Post_Type_Helper
*/
protected $post_type_helper;
/**
* The score icon helper.
*
* @var Score_Icon_Helper
*/
protected $score_icon_helper;
/**
* The database object.
*
* @var wpdb
*/
protected $wpdb;
/**
* The admin columns cache.
*
* @var Admin_Columns_Cache_Integration
*/
protected $admin_columns_cache;
/**
* {@inheritDoc}
*/
public static function get_conditionals() {
return [
Admin_Conditional::class,
Posts_Overview_Or_Ajax_Conditional::class,
Inclusive_Language_Enabled_Conditional::class,
];
}
/**
* Inclusive_Language_Column_Integration constructor
*
* @codeCoverageIgnore
*
* @param Post_Type_Helper $post_type_helper The post type helper.
* @param Score_Icon_Helper $score_icon_helper The score icon helper.
* @param wpdb $wpdb The wpdb object.
* @param Admin_Columns_Cache_Integration $admin_columns_cache The admin columns cache.
*/
public function __construct(
Post_Type_Helper $post_type_helper,
Score_Icon_Helper $score_icon_helper,
wpdb $wpdb,
Admin_Columns_Cache_Integration $admin_columns_cache
) {
$this->post_type_helper = $post_type_helper;
$this->score_icon_helper = $score_icon_helper;
$this->wpdb = $wpdb;
$this->admin_columns_cache = $admin_columns_cache;
}
/**
* {@inheritDoc}
*/
public function register_hooks() {
\add_filter( 'posts_clauses', [ $this, 'order_by_inclusive_language_score' ], 1, 2 );
\add_action( 'admin_init', [ $this, 'register_init_hooks' ] );
// Adds a filter to exclude the attachments from the inclusive language column.
\add_filter( 'wpseo_inclusive_language_column_post_types', [ 'WPSEO_Post_Type', 'filter_attachment_post_type' ] );
\add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] );
}
/**
* Register hooks that need to be registered after `init` due to all post types not yet being registered.
*/
public function register_init_hooks() {
$public_post_types = \apply_filters( 'wpseo_inclusive_language_column_post_types', $this->post_type_helper->get_accessible_post_types() );
if ( ! \is_array( $public_post_types ) || empty( $public_post_types ) ) {
return;
}
foreach ( $public_post_types as $post_type ) {
\add_filter( 'manage_' . $post_type . '_posts_columns', [ $this, 'add_inclusive_language_column' ] );
\add_action( 'manage_' . $post_type . '_posts_custom_column', [ $this, 'column_content' ], 10, 2 );
\add_filter( 'manage_edit-' . $post_type . '_sortable_columns', [ $this, 'column_sort' ] );
}
}
/**
* Enqueues the assets needed for the integration to work.
*
* @return void
*/
public function enqueue_assets() {
\wp_enqueue_style( WPSEO_Admin_Asset_Manager::PREFIX . 'premium-post-overview' );
}
/**
* Adds the inclusive language column for the post overview.
*
* @param array $columns Array with columns.
*
* @return array The extended array with columns.
*/
public function add_inclusive_language_column( $columns ) {
if ( ! \is_array( $columns ) ) {
return $columns;
}
$columns[ self::INCLUSIVE_LANGUAGE_COLUMN_NAME ] = \sprintf(
'<span class="yoast-column-inclusive-language yoast-column-header-has-tooltip" data-tooltip-text="%1$s"><span class="screen-reader-text">%2$s</span></span>',
\esc_attr__( 'Inclusive language score', 'wordpress-seo-premium' ),
\esc_html__( 'Inclusive language score', 'wordpress-seo-premium' )
);
return $columns;
}
/**
* Modifies the query pieces to allow ordering column by inclusive language score.
*
* @param array $pieces Array of Query pieces.
* @param WP_Query $query The Query on which to apply.
*
* @return array
*/
public function order_by_inclusive_language_score( $pieces, $query ) {
if ( $query->get( 'orderby' ) !== self::INCLUSIVE_LANGUAGE_COLUMN_NAME ) {
return $pieces;
}
return $this->build_sort_query_pieces( $pieces, $query );
}
/**
* Builds the pieces for a sorting query.
*
* @param array $pieces Array of Query pieces.
* @param WP_Query $query The Query on which to apply.
*
* @return array Modified Query pieces.
*/
protected function build_sort_query_pieces( $pieces, $query ) {
// We only want our code to run in the main WP query.
if ( ! $query->is_main_query() ) {
return $pieces;
}
// Get the order query variable - ASC or DESC.
$order = \strtoupper( $query->get( 'order' ) );
// Make sure the order setting qualifies. If not, set default as ASC.
if ( ! \in_array( $order, [ 'ASC', 'DESC' ], true ) ) {
$order = 'ASC';
}
$table = Model::get_table_name( 'Indexable' );
$pieces['join'] .= " LEFT JOIN $table AS yoast_indexable ON yoast_indexable.object_id = {$this->wpdb->posts}.ID AND yoast_indexable.object_type = 'post' ";
$pieces['orderby'] = "yoast_indexable.inclusive_language_score $order, {$pieces['orderby']}";
return $pieces;
}
/**
* Displays the column content for the given column.
*
* @param string $column_name Column to display the content for.
* @param int $post_id Post to display the column content for.
*/
public function column_content( $column_name, $post_id ) {
$indexable = $this->admin_columns_cache->get_indexable( $post_id );
// Nothing to output if we don't have the value.
if ( empty( $indexable ) ) {
return;
}
if ( $column_name === self::INCLUSIVE_LANGUAGE_COLUMN_NAME ) {
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Correctly escaped through the Score_Icon_Helper.
echo $this->score_icon_helper->for_inclusive_language( $indexable->inclusive_language_score );
}
}
/**
* Sets the sortable columns.
*
* @param array $columns Array with sortable columns.
*
* @return array The extended array with sortable columns.
*/
public function column_sort( $columns ) {
$columns[ self::INCLUSIVE_LANGUAGE_COLUMN_NAME ] = self::INCLUSIVE_LANGUAGE_COLUMN_NAME;
return $columns;
}
}

View File

@@ -0,0 +1,165 @@
<?php
namespace Yoast\WP\SEO\Premium\Integrations\Admin;
use WPSEO_Meta;
use WPSEO_Rank;
use Yoast\WP\SEO\Conditionals\Admin\Posts_Overview_Or_Ajax_Conditional;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Premium\Conditionals\Inclusive_Language_Enabled_Conditional;
/**
* Inclusive_Language_Filter_Integration class.
*
* phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
*/
class Inclusive_Language_Filter_Integration implements Integration_Interface {
/**
* {@inheritDoc}
*/
public static function get_conditionals() {
return [
Admin_Conditional::class,
Posts_Overview_Or_Ajax_Conditional::class,
Inclusive_Language_Enabled_Conditional::class,
];
}
/**
* {@inheritDoc}
*/
public function register_hooks() {
// Creates the inclusive language score filter dropdown -- with priority 20 to fire after the SEO/readability filter.
\add_action( 'restrict_manage_posts', [ $this, 'posts_filter_dropdown_inclusive_language' ], 20 );
// Adds the inclusive language score filter to the list of active filters -- if selected for filtering.
\add_filter( 'wpseo_change_applicable_filters', [ $this, 'add_inclusive_language_filter' ] );
// Adds the inclusive language score meta column to the order by part of the query -- if selected for ordering.
\add_filter( 'wpseo_change_order_by', [ $this, 'add_inclusive_language_order_by' ] );
}
/**
* Adds a dropdown that allows filtering on inclusive language score.
*
* @return void
*/
public function posts_filter_dropdown_inclusive_language() {
$ranks = WPSEO_Rank::get_all_inclusive_language_ranks();
echo '<label class="screen-reader-text" for="wpseo-inclusive-language-filter">'
/* translators: Hidden accessibility text. */
. \esc_html__( 'Filter by Inclusive Language Score', 'wordpress-seo-premium' )
. '</label>';
echo '<select name="inclusive_language_filter" id="wpseo-inclusive-language-filter">';
// phpcs:ignore WordPress.Security.EscapeOutput -- Output is correctly escaped in the generate_option() method.
echo $this->generate_option( '', __( 'All Inclusive Language Scores', 'wordpress-seo-premium' ) );
foreach ( $ranks as $rank ) {
$selected = \selected( $this->get_current_inclusive_language_filter(), $rank->get_rank(), false );
// phpcs:ignore WordPress.Security.EscapeOutput -- Output is correctly escaped in the generate_option() method.
echo $this->generate_option( $rank->get_rank(), $rank->get_drop_down_inclusive_language_labels(), $selected );
}
echo '</select>';
}
/**
* Generates an <option> element.
*
* @param string $value The option's value.
* @param string $label The option's label.
* @param string $selected HTML selected attribute for an option.
*
* @return string The generated <option> element.
*/
protected function generate_option( $value, $label, $selected = '' ) {
return '<option ' . $selected . ' value="' . esc_attr( $value ) . '">' . esc_html( $label ) . '</option>';
}
/**
* Retrieves the current inclusive language score filter value from the $_GET variable.
*
* @return string|null The sanitized inclusive language score filter value or null when the variable is not set in $_GET.
*/
public function get_current_inclusive_language_filter() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information.
if ( isset( $_GET['inclusive_language_filter'] ) && \is_string( $_GET['inclusive_language_filter'] ) ) {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information.
return \sanitize_text_field( \wp_unslash( $_GET['inclusive_language_filter'] ) );
}
return null;
}
/**
* Determines the inclusive language score filter to the meta query, based on the passed inclusive language filter.
*
* @param string $inclusive_language_filter The inclusive language filter to use to determine what further filter to apply.
*
* @return array The inclusive language score filter.
*/
public function determine_inclusive_language_filters( $inclusive_language_filter ) {
$rank = new WPSEO_Rank( $inclusive_language_filter );
return $this->create_inclusive_language_score_filter( $rank->get_starting_score(), $rank->get_end_score() );
}
/**
* Creates an inclusive language score filter.
*
* @param number $low The lower boundary of the score.
* @param number $high The higher boundary of the score.
*
* @return array The inclusive language score filter.
*/
protected function create_inclusive_language_score_filter( $low, $high ) {
return [
[
'key' => WPSEO_Meta::$meta_prefix . 'inclusive_language_score',
'value' => [ $low, $high ],
'type' => 'numeric',
'compare' => 'BETWEEN',
],
];
}
/**
* Adds the inclusive language filter to the list of active filters -- if it has been used for filtering.
*
* @param array $active_filters The currently active filters.
* @return array The active filters, including the inclusive language filter -- if it has been used for filtering.
*/
public function add_inclusive_language_filter( $active_filters ) {
$inclusive_language_filter = $this->get_current_inclusive_language_filter();
if ( \is_string( $inclusive_language_filter ) && $inclusive_language_filter !== '' ) {
$active_filters = \array_merge(
$active_filters,
$this->determine_inclusive_language_filters( $inclusive_language_filter )
);
}
return $active_filters;
}
/**
* Adds the inclusive language score field to the order by part of the query -- if it has been selected during filtering.
*
* @param array $order_by The current order by statement.
* @param string $order_by_column The column to use for ordering.
* @return array The order by.
*/
public function add_inclusive_language_order_by( $order_by, $order_by_column = '' ) {
if ( $order_by === [] && $order_by_column === Inclusive_Language_Column_Integration::INCLUSIVE_LANGUAGE_COLUMN_NAME ) {
return [
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Reason: Only used when user requests sorting.
'meta_key' => WPSEO_Meta::$meta_prefix . 'inclusive_language_score',
'orderby' => 'meta_value_num',
];
}
return $order_by;
}
}

View File

@@ -0,0 +1,143 @@
<?php
namespace Yoast\WP\SEO\Premium\Integrations\Admin;
use WPSEO_Admin_Asset_Manager;
use WPSEO_Taxonomy_Meta;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Helpers\Score_Icon_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Premium\Conditionals\Inclusive_Language_Enabled_Conditional;
use Yoast\WP\SEO\Premium\Conditionals\Term_Overview_Or_Ajax_Conditional;
use Yoast\WP\SEO\Premium\Helpers\Current_Page_Helper;
/**
* Inclusive_Language_Column_Integration class.
*
* phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
*/
class Inclusive_Language_Taxonomy_Column_Integration implements Integration_Interface {
/**
* Name of the column.
*
* @var string
*/
const INCLUSIVE_LANGUAGE_COLUMN_NAME = 'wpseo-inclusive-language';
/**
* The score icon helper.
*
* @var Score_Icon_Helper
*/
protected $score_icon_helper;
/**
* Holds the Current_Page_Helper instance.
*
* @var Current_Page_Helper
*/
private $current_page_helper;
/**
* {@inheritDoc}
*/
public static function get_conditionals() {
return [
Admin_Conditional::class,
Term_Overview_Or_Ajax_Conditional::class,
Inclusive_Language_Enabled_Conditional::class,
];
}
/**
* Inclusive_Language_Column_Integration constructor
*
* @param Score_Icon_Helper $score_icon_helper The score icon helper.
* @param Current_Page_Helper $current_page_helper The Current_Page_Helper.
*/
public function __construct(
Score_Icon_Helper $score_icon_helper,
Current_Page_Helper $current_page_helper
) {
$this->score_icon_helper = $score_icon_helper;
$this->current_page_helper = $current_page_helper;
}
/**
* {@inheritDoc}
*/
public function register_hooks() {
\add_action( 'admin_init', [ $this, 'register_init_hooks' ] );
\add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] );
}
/**
* Register hooks that need to be registered after `init` due to all post types not yet being registered.
*/
public function register_init_hooks() {
$taxonomy = $this->current_page_helper->get_current_taxonomy();
$is_product = $this->current_page_helper->get_current_post_type() === 'product';
$is_product_cat = $taxonomy === 'product_cat';
$is_product_tag = $taxonomy === 'product_tag';
if ( ( $is_product && ( $is_product_cat || $is_product_tag ) ) || ( ! $is_product && $taxonomy ) ) {
\add_filter( 'manage_edit-' . $taxonomy . '_columns', [ $this, 'add_inclusive_language_column' ] );
\add_filter( 'manage_' . $taxonomy . '_custom_column', [ $this, 'column_content' ], 10, 3 );
}
}
/**
* Enqueues the assets needed for the integration to work.
*
* @return void
*/
public function enqueue_assets() {
\wp_enqueue_style( WPSEO_Admin_Asset_Manager::PREFIX . 'premium-post-overview' );
}
/**
* Adds the inclusive language column for the term overview.
*
* @param array $columns Array with columns.
*
* @return array The extended array with columns.
*/
public function add_inclusive_language_column( $columns ) {
if ( ! \is_array( $columns ) ) {
return $columns;
}
$columns[ self::INCLUSIVE_LANGUAGE_COLUMN_NAME ] = \sprintf(
'<span class="yoast-tooltip yoast-tooltip-n yoast-tooltip-alt" data-label="%1$s">
<span class="yoast-column-inclusive-language yoast-column-header-has-tooltip">
<span class="screen-reader-text">%2$s</span>
</span>
</span>',
\esc_attr__( 'Inclusive language score', 'wordpress-seo-premium' ),
\esc_html__( 'Inclusive language score', 'wordpress-seo-premium' )
);
return $columns;
}
/**
* Displays the column content for the given column.
*
* @param string $content The current content of the column.
* @param string $column_name The name of the column.
* @param int $term_id ID of requested taxonomy.
*
* @return string
*/
public function column_content( $content, $column_name, $term_id ) {
$score = (int) WPSEO_Taxonomy_Meta::get_term_meta( $term_id, $this->current_page_helper->get_current_taxonomy(), 'inclusive_language_score' );
if ( $column_name === self::INCLUSIVE_LANGUAGE_COLUMN_NAME ) {
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Correctly escaped through the Score_Icon_Helper.
return $this->score_icon_helper->for_inclusive_language( $score );
}
return $content;
}
}

View File

@@ -0,0 +1,89 @@
<?php
namespace Yoast\WP\SEO\Premium\Integrations\Admin;
use WP_Query;
use WPSEO_Meta;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;
/**
* Keyword_Integration class
*/
class Keyword_Integration implements Integration_Interface {
/**
* {@inheritDoc}
*/
public static function get_conditionals() {
return [ Admin_Conditional::class ];
}
/**
* {@inheritDoc}
*/
public function register_hooks() {
\add_filter( 'wpseo_posts_for_focus_keyword', [ $this, 'add_posts_for_focus_keyword' ], 10, 3 );
\add_filter( 'wpseo_posts_for_related_keywords', [ $this, 'add_posts_for_related_keywords' ], 10, 2 );
}
/**
* Enhances the array of posts that share their focus keywords with the post's related keywords by adding posts' ids with the same related keywords.
*
* @param array $usage The array of posts' ids that share their focus keywords with the post.
* @param int $post_id The id of the post we're finding the usage of related keywords for.
*
* @return array The filtered array of posts' ids.
*/
public function add_posts_for_related_keywords( $usage, $post_id ) {
$additional_keywords = \json_decode( WPSEO_Meta::get_value( 'focuskeywords', $post_id ), true );
if ( empty( $additional_keywords ) ) {
return $usage;
}
foreach ( $additional_keywords as $additional_keyword ) {
if ( isset( $additional_keyword['keyword'] ) ) {
$keyword = $additional_keyword['keyword'];
$usage[ $keyword ] = WPSEO_Meta::keyword_usage( $keyword, $post_id );
}
}
return $usage;
}
/**
* Enhances the array of posts that share their focus keywords with the post's focus keywords by adding posts' ids with the same related keywords.
*
* @param array $post_ids The array of posts' ids that share their related keywords with the post.
* @param string $keyword The keyword to search for.
* @param int $post_id The id of the post the keyword is associated to.
*
* @return array The filtered array of posts' ids.
*/
public function add_posts_for_focus_keyword( $post_ids, $keyword, $post_id ) {
$query = [
'meta_query' => [
[
'key' => '_yoast_wpseo_focuskeywords',
'value' => \sprintf( '"keyword":"%s"', $keyword ),
'compare' => 'LIKE',
],
],
'post__not_in' => [ $post_id ],
'fields' => 'ids',
'post_type' => 'any',
/*
* We only need to return zero, one or two results:
* - Zero: keyword hasn't been used before
* - One: Keyword has been used once before
* - Two or more: Keyword has been used twice or more before
*/
'posts_per_page' => 2,
];
$get_posts = new WP_Query( $query );
return \array_merge( $post_ids, $get_posts->posts );
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace Yoast\WP\SEO\Premium\Integrations\Admin;
use WPSEO_Options;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;
/**
* Metabox_Formatter_Integration class
*/
class Metabox_Formatter_Integration implements Integration_Interface {
/**
* {@inheritDoc}
*/
public static function get_conditionals() {
return [ Admin_Conditional::class ];
}
/**
* {@inheritDoc}
*/
public function register_hooks() {
\add_filter( 'wpseo_social_template_post_type', [ $this, 'get_template_for_post_type' ], 10, 3 );
\add_filter( 'wpseo_social_template_taxonomy', [ $this, 'get_template_for_taxonomy' ], 10, 3 );
}
/**
* Retrieves a template for a post type.
*
* @param string $template The default template.
* @param string $template_option_name The subname of the option in which the template you want to get is saved.
* @param string $post_type The name of the post type.
*
* @return string
*/
public function get_template_for_post_type( $template, $template_option_name, $post_type ) {
$needed_option = \sprintf( 'social-%s-%s', $template_option_name, $post_type );
if ( WPSEO_Options::get( $needed_option, '' ) !== '' ) {
return WPSEO_Options::get( $needed_option );
}
return $template;
}
/**
* Retrieves a template for a taxonomy.
*
* @param string $template The default template.
* @param string $template_option_name The subname of the option in which the template you want to get is saved.
* @param string $taxonomy The name of the taxonomy.
*
* @return string
*/
public function get_template_for_taxonomy( $template, $template_option_name, $taxonomy ) {
$needed_option = \sprintf( 'social-%s-tax-%s', $template_option_name, $taxonomy );
return WPSEO_Options::get( $needed_option, $template );
}
}

View File

@@ -1,24 +1,24 @@
<?php
namespace Yoast\WP\SEO\Integrations\Admin\Prominent_Words;
namespace Yoast\WP\SEO\Premium\Integrations\Admin\Prominent_Words;
use WPSEO_Language_Utils;
use Yoast\WP\SEO\Actions\Indexing\Indexable_General_Indexation_Action;
use Yoast\WP\SEO\Actions\Indexing\Indexable_Post_Indexation_Action;
use Yoast\WP\SEO\Actions\Indexing\Indexable_Post_Type_Archive_Indexation_Action;
use Yoast\WP\SEO\Actions\Indexing\Indexable_Term_Indexation_Action;
use Yoast\WP\SEO\Actions\Indexing\Indexation_Action_Interface;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Conditionals\Migrations_Conditional;
use Yoast\WP\SEO\Helpers\Language_Helper;
use Yoast\WP\SEO\Helpers\Prominent_Words_Helper;
use Yoast\WP\SEO\Helpers\Url_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Premium\Actions\Prominent_Words\Content_Action;
use Yoast\WP\SEO\Routes\Prominent_Words_Route;
use Yoast\WP\SEO\Premium\Helpers\Prominent_Words_Helper;
use Yoast\WP\SEO\Premium\Routes\Prominent_Words_Route;
/**
* Class Indexing_Integration.
*
* @package Yoast\WP\SEO\Integrations\Admin\Prominent_Words
*/
class Indexing_Integration implements Integration_Interface {
@@ -39,39 +39,11 @@ class Indexing_Integration implements Integration_Interface {
const PER_INDEXABLE_LIMIT_NO_FUNCTION_WORD_SUPPORT = 30;
/**
* Holds the content action.
* All indexing actions.
*
* @var Content_Action
* @var Indexation_Action_Interface[]
*/
protected $content_indexation_action;
/**
* The post indexing action.
*
* @var Indexable_Post_Indexation_Action
*/
protected $post_indexation_action;
/**
* The term indexing action.
*
* @var Indexable_Term_Indexation_Action
*/
protected $term_indexation_action;
/**
* The post type archive indexing action.
*
* @var Indexable_Post_Type_Archive_Indexation_Action
*/
protected $post_type_archive_indexation_action;
/**
* Represents the general indexing action.
*
* @var Indexable_General_Indexation_Action
*/
protected $general_indexation_action;
protected $indexing_actions;
/**
* Represents the language helper.
@@ -80,6 +52,13 @@ class Indexing_Integration implements Integration_Interface {
*/
protected $language_helper;
/**
* Represents the url helper.
*
* @var Url_Helper
*/
protected $url_helper;
/**
* Represents the prominent words helper.
*
@@ -103,6 +82,7 @@ class Indexing_Integration implements Integration_Interface {
* @param Indexable_General_Indexation_Action $general_indexation_action The general indexing action.
* @param Indexable_Post_Type_Archive_Indexation_Action $post_type_archive_indexation_action The post type archive indexing action.
* @param Language_Helper $language_helper The language helper.
* @param Url_Helper $url_helper The url helper.
* @param Prominent_Words_Helper $prominent_words_helper The prominent words helper.
*/
public function __construct(
@@ -112,15 +92,26 @@ class Indexing_Integration implements Integration_Interface {
Indexable_General_Indexation_Action $general_indexation_action,
Indexable_Post_Type_Archive_Indexation_Action $post_type_archive_indexation_action,
Language_Helper $language_helper,
Url_Helper $url_helper,
Prominent_Words_Helper $prominent_words_helper
) {
$this->content_indexation_action = $content_indexation_action;
$this->post_indexation_action = $post_indexation_action;
$this->term_indexation_action = $term_indexation_action;
$this->general_indexation_action = $general_indexation_action;
$this->post_type_archive_indexation_action = $post_type_archive_indexation_action;
$this->language_helper = $language_helper;
$this->prominent_words_helper = $prominent_words_helper;
$this->language_helper = $language_helper;
$this->url_helper = $url_helper;
$this->prominent_words_helper = $prominent_words_helper;
// Indexation actions are used to calculate the number of unindexed objects.
$this->indexing_actions = [
// Get the number of indexables that haven't had their prominent words indexed yet.
$content_indexation_action,
// Take posts and terms into account that do not have indexables yet.
// These need to be counted again here (in addition to being counted in Free) because them being unindexed
// means that the above prominent words unindexed count couldn't detect these posts/terms for prominent words indexing.
$post_indexation_action,
$term_indexation_action,
$general_indexation_action,
$post_type_archive_indexation_action,
];
}
/**
@@ -135,6 +126,7 @@ class Indexing_Integration implements Integration_Interface {
\add_filter( 'wpseo_indexing_data', [ $this, 'adapt_indexing_data' ] );
\add_filter( 'wpseo_indexing_get_unindexed_count', [ $this, 'get_unindexed_count' ] );
\add_filter( 'wpseo_indexing_get_limited_unindexed_count', [ $this, 'get_limited_unindexed_count' ], 10, 2 );
\add_filter( 'wpseo_indexing_endpoints', [ $this, 'add_endpoints' ] );
}
@@ -202,7 +194,10 @@ class Indexing_Integration implements Integration_Interface {
* @return void
*/
public function enqueue_scripts() {
if ( ! isset( $_GET['page'] ) || $_GET['page'] !== 'wpseo_tools' || ( $_GET['page'] === 'wpseo_tools' && isset( $_GET['tool'] ) ) ) {
if ( ! isset( $_GET['page'] )
|| ( $_GET['page'] !== 'wpseo_tools' && $_GET['page'] !== 'wpseo_workouts' && $_GET['page'] !== 'wpseo_dashboard' )
|| ( $_GET['page'] === 'wpseo_tools' && isset( $_GET['tool'] ) )
) {
return;
}
@@ -210,6 +205,7 @@ class Indexing_Integration implements Integration_Interface {
$this->prominent_words_helper->set_indexing_completed( $is_completed );
\wp_enqueue_script( 'yoast-premium-prominent-words-indexation' );
\wp_localize_script( 'yoast-premium-prominent-words-indexation', 'wpseoPremiumIndexationData', [ 'licensedURL' => $this->url_helper->network_safe_home_url() ] );
}
/**
@@ -220,14 +216,27 @@ class Indexing_Integration implements Integration_Interface {
* @return int The total number of indexables to recalculate.
*/
public function get_unindexed_count( $unindexed_count ) {
// Get the number of indexables that haven't had their prominent words indexed yet.
$unindexed_count += $this->content_indexation_action->get_total_unindexed();
foreach ( $this->indexing_actions as $indexing_action ) {
$unindexed_count += $indexing_action->get_total_unindexed();
}
return $unindexed_count;
}
// Take posts and terms into account that do not have indexables yet.
$unindexed_count += $this->post_indexation_action->get_total_unindexed();
$unindexed_count += $this->term_indexation_action->get_total_unindexed();
$unindexed_count += $this->general_indexation_action->get_total_unindexed();
$unindexed_count += $this->post_type_archive_indexation_action->get_total_unindexed();
/**
* Returns a limited number of unindexed objects.
*
* @param int $unindexed_count The unindexed count.
* @param int $limit Limit the number of unindexed objects that are counted.
*
* @return int The total number of unindexed objects.
*/
public function get_limited_unindexed_count( $unindexed_count, $limit ) {
foreach ( $this->indexing_actions as $indexing_action ) {
$unindexed_count += $indexing_action->get_limited_unindexed_count( $limit - $unindexed_count + 1 );
if ( $unindexed_count > $limit ) {
return $unindexed_count;
}
}
return $unindexed_count;
}

View File

@@ -1,6 +1,6 @@
<?php
namespace Yoast\WP\SEO\Integrations\Admin\Prominent_Words;
namespace Yoast\WP\SEO\Premium\Integrations\Admin\Prominent_Words;
use Yoast\WP\SEO\Conditionals\No_Conditionals;
use Yoast\WP\SEO\Integrations\Integration_Interface;
@@ -9,8 +9,6 @@ use Yoast\WP\SEO\Premium\Actions\Prominent_Words\Save_Action;
/**
* Adds a hidden field to the metabox for storing the calculated words and also
* handles the value of it after posting.
*
* @package Yoast\WP\SEO\Integrations\Admin\Prominent_Words
*/
class Metabox_Integration implements Integration_Interface {
@@ -40,7 +38,7 @@ class Metabox_Integration implements Integration_Interface {
\add_filter( 'update_post_metadata', [ $this, 'save_prominent_words_for_post' ], 10, 4 );
\add_filter( 'wpseo_taxonomy_content_fields', [ $this, 'add_words_for_linking_hidden_field' ] );
\add_filter( 'edit_term', [ $this, 'save_prominent_words_for_term' ] );
\add_action( 'edit_term', [ $this, 'save_prominent_words_for_term' ] );
}
/**

View File

@@ -0,0 +1,66 @@
<?php
namespace Yoast\WP\SEO\Premium\Integrations\Admin;
use WPSEO_Meta;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;
/**
* Take related keyphrases into account when filtering posts on keyphrase.
*
* phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
*/
class Related_Keyphrase_Filter_Integration implements Integration_Interface {
/**
* The conditionals that should be met for this integration to be active.
*
* @return string[] A list of fully qualified class names of the `Conditional`s that should be met.
*/
public static function get_conditionals() {
return [ Admin_Conditional::class ];
}
/**
* Register the WordPress hooks needed for this integration to work.
*
* @return void
*/
public function register_hooks() {
\add_filter( 'wpseo_change_keyphrase_filter_in_request', [ $this, 'add_related_keyphrase_filter' ], 10, 2 );
}
/**
* Adapts the keyphrase filter to also take related keyphrases into account.
*
* @param array $keyphrase_filter The current keyphrase filter.
* @param string $keyphrase The keyphrase.
*
* @return array The new keyphrase filter,
*/
public function add_related_keyphrase_filter( $keyphrase_filter, $keyphrase ) {
return [
'relation' => 'OR',
$keyphrase_filter,
$this->get_related_keyphrase_filter( $keyphrase ),
];
}
/**
* Returns the filter to use within the WP Meta Query to filter
* on related keyphrase.
*
* @param string $focus_keyphrase The focus keyphrase to filter on.
*
* @return array The filter.
*/
private function get_related_keyphrase_filter( $focus_keyphrase ) {
return [
'post_type' => \get_query_var( 'post_type', 'post' ),
'key' => WPSEO_Meta::$meta_prefix . 'focuskeywords',
'value' => '"keyword":"' . \sanitize_text_field( $focus_keyphrase ) . '"',
'compare' => 'LIKE',
];
}
}

View File

@@ -0,0 +1,128 @@
<?php
namespace Yoast\WP\SEO\Premium\Integrations\Admin;
use WPSEO_Admin_Asset_Manager;
use WPSEO_Metabox;
use WPSEO_Options;
use WPSEO_Post_Type;
use WPSEO_Taxonomy;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Premium\Helpers\Current_Page_Helper;
/**
* Replacement_Variables_Integration class
*/
class Replacement_Variables_Integration implements Integration_Interface {
/**
* {@inheritDoc}
*/
public static function get_conditionals() {
return [ Admin_Conditional::class ];
}
/**
* {@inheritDoc}
*/
public function register_hooks() {
\add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] );
}
/**
* Enqueue the replacement variables styles and component.
*/
public function enqueue_assets() {
/**
* Filter: 'wpseo_premium_load_emoji_picker' - Allow changing whether the emoji picker is loaded for the meta description and SEO title fields.
*
* Note: This is a Premium plugin-only hook.
*
* @since 19.0
*
* @param bool $load Whether to load the emoji picker.
*/
if ( ! \apply_filters( 'wpseo_premium_load_emoji_picker', true ) ) {
return;
}
$is_elementor_action = false;
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information.
if ( isset( $_GET['action'] ) && \is_string( $_GET['action'] ) ) {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are not processing form information, We are only strictly comparing.
$is_elementor_action = ( \wp_unslash( $_GET['action'] ) === 'elementor' );
}
$is_settings_page = false;
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information.
if ( isset( $_GET['page'] ) && \is_string( $_GET['page'] ) ) {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are not processing form information, We are only strictly comparing.
$is_settings_page = ( \wp_unslash( $_GET['page'] ) === 'wpseo_page_settings' );
}
if ( ! $is_settings_page && ! $is_elementor_action && ! $this->load_metabox( $this->get_current_page() ) ) {
return;
}
\wp_enqueue_script( 'yoast-seo-premium-draft-js-plugins' );
\wp_enqueue_style( 'yoast-seo-premium-draft-js-plugins' );
$draft_js_external_script_location = 'https://yoast.com/shared-assets/scripts/wp-seo-premium-draft-js-plugins-source-2.0.0.min.js';
if ( \file_exists( \WPSEO_PREMIUM_PATH . 'assets/js/external/draft-js-emoji-picker.min.js' ) ) {
$draft_js_external_script_location = \plugins_url( 'wordpress-seo-premium/assets/js/external/draft-js-emoji-picker.min.js' );
}
\wp_enqueue_script(
'yoast-seo-premium-draft-js-plugins-external',
$draft_js_external_script_location,
[
'yoast-seo-premium-commons',
WPSEO_Admin_Asset_Manager::PREFIX . 'search-metadata-previews',
],
\WPSEO_PREMIUM_VERSION,
false
);
}
/**
* Checks whether or not the metabox related scripts should be loaded.
*
* @codeCoverageIgnore
*
* @param string $current_page The page we are on.
*
* @return bool True when it should be loaded.
*/
protected function load_metabox( $current_page ) {
$page_helper = new Current_Page_Helper();
// When the current page is a term related one.
if ( WPSEO_Taxonomy::is_term_edit( $current_page ) || WPSEO_Taxonomy::is_term_overview( $current_page ) ) {
return WPSEO_Options::get( 'display-metabox-tax-' . $page_helper->get_current_taxonomy() );
}
// When the current page isn't a post related one.
if ( WPSEO_Metabox::is_post_edit( $current_page ) || WPSEO_Metabox::is_post_overview( $current_page ) ) {
return WPSEO_Post_Type::has_metabox_enabled( $page_helper->get_current_post_type() );
}
// Make sure ajax integrations are loaded.
return \wp_doing_ajax();
}
/**
* Retrieves the value of the pagenow variable.
*
* @codeCoverageIgnore
*
* @return string The value of pagenow.
*/
private function get_current_page() {
global $pagenow;
return $pagenow;
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace Yoast\WP\SEO\Premium\Integrations\Admin;
use WPSEO_Admin_Asset_Manager;
use Yoast\WP\SEO\Conditionals\Settings_Conditional;
use Yoast\WP\SEO\Helpers\Current_Page_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
/**
* Class Settings_Integration.
*/
class Settings_Integration implements Integration_Interface {
/**
* Holds the WPSEO_Admin_Asset_Manager.
*
* @var WPSEO_Admin_Asset_Manager
*/
protected $asset_manager;
/**
* Holds the Current_Page_Helper.
*
* @var Current_Page_Helper
*/
protected $current_page_helper;
/**
* Constructs Settings_Integration.
*
* @param WPSEO_Admin_Asset_Manager $asset_manager The WPSEO_Admin_Asset_Manager.
* @param Current_Page_Helper $current_page_helper The Current_Page_Helper.
*/
public function __construct( WPSEO_Admin_Asset_Manager $asset_manager, Current_Page_Helper $current_page_helper ) {
$this->asset_manager = $asset_manager;
$this->current_page_helper = $current_page_helper;
}
/**
* Returns the conditionals based on which this loadable should be active.
*
* @return array
*/
public static function get_conditionals() {
return [ Settings_Conditional::class ];
}
/**
* Initializes the integration.
*
* This is the place to register hooks and filters.
*
* @return void
*/
public function register_hooks() {
// Are we on the settings page?
if ( $this->current_page_helper->get_current_yoast_seo_page() === 'wpseo_page_settings' ) {
\add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] );
}
}
/**
* Enqueues the assets.
*
* @return void
*/
public function enqueue_assets() {
$this->asset_manager->enqueue_style( 'premium-settings' );
}
}

View File

@@ -0,0 +1,110 @@
<?php
namespace Yoast\WP\SEO\Premium\Integrations\Admin;
use WPSEO_Admin_Asset_Manager;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
/**
* Thank_You_Page_Integration class
*
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
*/
class Thank_You_Page_Integration implements Integration_Interface {
// phpcs:enable
/**
* The options helper.
*
* @var Options_Helper
*/
protected $options_helper;
/**
* {@inheritDoc}
*/
public static function get_conditionals() {
return [ Admin_Conditional::class ];
}
/**
* Thank_You_Page_Integration constructor.
*
* @param Options_Helper $options_helper The options helper.
*/
public function __construct( Options_Helper $options_helper ) {
$this->options_helper = $options_helper;
}
/**
* {@inheritDoc}
*/
public function register_hooks() {
\add_filter( 'admin_menu', [ $this, 'add_submenu_page' ], 9 );
\add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] );
\add_action( 'admin_init', [ $this, 'maybe_redirect' ] );
}
/**
* Redirects to the installation success page if an installation has just occured.
*
* @return void
*/
public function maybe_redirect() {
if ( ! $this->options_helper->get( 'should_redirect_after_install' ) ) {
return;
}
$this->options_helper->set( 'should_redirect_after_install', false );
if ( ! empty( $this->options_helper->get( 'activation_redirect_timestamp' ) ) ) {
return;
}
$this->options_helper->set( 'activation_redirect_timestamp', \time() );
\wp_safe_redirect( \admin_url( 'admin.php?page=wpseo_installation_successful' ), 302, 'Yoast SEO Premium' );
exit;
}
/**
* Adds the workouts submenu page.
*
* @param array $submenu_pages The Yoast SEO submenu pages.
*
* @return array the filtered submenu pages.
*/
public function add_submenu_page( $submenu_pages ) {
\add_submenu_page(
'',
\__( 'Installation Successful', 'wordpress-seo-premium' ),
'',
'manage_options',
'wpseo_installation_successful',
[ $this, 'render_page' ]
);
return $submenu_pages;
}
/**
* Enqueue assets on the Thank you page.
*/
public function enqueue_assets() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Date is not processed or saved.
if ( ! isset( $_GET['page'] ) || $_GET['page'] !== 'wpseo_installation_successful' ) {
return;
}
$asset_manager = new WPSEO_Admin_Asset_Manager();
$asset_manager->enqueue_style( 'monorepo' );
\wp_enqueue_style( 'yoast-seo-premium-thank-you' );
}
/**
* Renders the thank you page.
*/
public function render_page() {
require \WPSEO_PREMIUM_PATH . 'classes/views/thank-you.php';
}
}

View File

@@ -0,0 +1,177 @@
<?php
namespace Yoast\WP\SEO\Premium\Integrations\Admin;
use WPSEO_Admin_Asset_Manager;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Helpers\Capability_Helper;
use Yoast\WP\SEO\Helpers\Current_Page_Helper;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Premium\Helpers\Version_Helper;
use Yoast\WP\SEO\Presenters\Admin\Notice_Presenter;
/**
* Integration to display a notification urging to update Premium when a new version is available.
*/
class Update_Premium_Notification implements Integration_Interface {
/**
* The options' helper.
*
* @var Options_Helper
*/
private $options_helper;
/**
* The version helper.
*
* @var Version_Helper
*/
private $version_helper;
/**
* The capability helper.
*
* @var Capability_Helper
*/
private $capability_helper;
/**
* The admin asset manager.
*
* @var WPSEO_Admin_Asset_Manager
*/
private $admin_asset_manager;
/**
* The Current_Page_Helper.
*
* @var Current_Page_Helper
*/
private $current_page_helper;
/**
* {@inheritDoc}
*/
public static function get_conditionals() {
return [ Admin_Conditional::class ];
}
/**
* Update_Premium_Notification constructor.
*
* @param Options_Helper $options_helper The options helper.
* @param Version_Helper $version_helper The version helper.
* @param Capability_Helper $capability_helper The capability helper.
* @param WPSEO_Admin_Asset_Manager $admin_asset_manager The admin asset manager.
* @param Current_Page_Helper $current_page_helper The Current_Page_Helper.
*/
public function __construct(
Options_Helper $options_helper,
Version_Helper $version_helper,
Capability_Helper $capability_helper,
WPSEO_Admin_Asset_Manager $admin_asset_manager,
Current_Page_Helper $current_page_helper
) {
$this->options_helper = $options_helper;
$this->version_helper = $version_helper;
$this->capability_helper = $capability_helper;
$this->admin_asset_manager = $admin_asset_manager;
$this->current_page_helper = $current_page_helper;
}
/**
* {@inheritDoc}
*/
public function register_hooks() {
\add_action( 'admin_notices', [ $this, 'maybe_display_notification' ] );
\add_action( 'wp_ajax_dismiss_update_premium_notification', [ $this, 'dismiss_update_premium_notification' ] );
}
/**
* Shows a notice if Free is newer than the minimum required version and Premium has an update available.
*
* @return void
*/
public function maybe_display_notification() {
if ( $this->current_page_helper->get_current_admin_page() === 'update.php' ) {
return;
}
if ( $this->notice_was_dismissed_on_current_premium_version() ) {
return;
}
if ( ! $this->capability_helper->current_user_can( 'wpseo_manage_options' ) ) {
return;
}
// Check whether Free is set to a version later than the minimum required and a Premium update is a available.
if ( $this->version_helper->is_free_upgraded() && $this->version_helper->is_premium_update_available() ) {
$this->admin_asset_manager->enqueue_style( 'monorepo' );
$is_plugins_page = $this->current_page_helper->get_current_admin_page() === 'plugins.php';
$content = \sprintf(
/* translators: 1: Yoast SEO Premium, 2 and 3: opening and closing anchor tag. */
\esc_html__( 'Please %2$supdate %1$s to the latest version%3$s to ensure you can fully use all Premium settings and features.', 'wordpress-seo-premium' ),
'Yoast SEO Premium',
( $is_plugins_page ) ? '' : '<a href="' . \esc_url( \self_admin_url( 'plugins.php' ) ) . '">',
( $is_plugins_page ) ? '' : '</a>'
);
// phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped -- Output of the title escaped in the Notice_Presenter.
echo new Notice_Presenter(
/* translators: 1: Yoast SEO Premium */
\sprintf( \__( 'Update to the latest version of %1$s!', 'wordpress-seo-premium' ), 'Yoast SEO Premium' ),
$content,
null,
null,
true,
'yoast-update-premium-notification'
);
// phpcs:enable
// Enable permanently dismissing the notice.
echo "<script>
function dismiss_update_premium_notification(){
var data = {
'action': 'dismiss_update_premium_notification',
};
jQuery.post( ajaxurl, data, function( response ) {
jQuery( '#yoast-update-premium-notification' ).hide();
});
}
jQuery( document ).ready( function() {
jQuery( 'body' ).on( 'click', '#yoast-update-premium-notification .notice-dismiss', function() {
dismiss_update_premium_notification();
} );
} );
</script>";
}
}
/**
* Dismisses the old premium notice.
*
* @return bool
*/
public function dismiss_update_premium_notification() {
return $this->options_helper->set( 'dismiss_update_premium_notification', WPSEO_PREMIUM_VERSION );
}
/**
* Returns whether the notification was dismissed in the current Premium version.
*
* @return bool Whether the notification was dismissed in the current Premium version.
*/
protected function notice_was_dismissed_on_current_premium_version() {
$dismissed_notification_version = $this->options_helper->get( 'dismiss_update_premium_notification', '' );
if ( ! empty( $dismissed_notification_version ) ) {
return \version_compare( $dismissed_notification_version, WPSEO_PREMIUM_VERSION, '>=' );
}
return false;
}
}

View File

@@ -174,8 +174,8 @@ class User_Profile_Integration implements Integration_Interface {
* @param int $user_id User ID of the updated user.
*/
public function process_user_option_update( $user_id ) {
$nonce_value = \filter_input( \INPUT_POST, self::NONCE_FIELD_NAME, \FILTER_SANITIZE_STRING );
if ( empty( $nonce_value ) ) {
// I'm keeping this to conform to the original logic.
if ( ! isset( $_POST[ self::NONCE_FIELD_NAME ] ) || ! \is_string( $_POST[ self::NONCE_FIELD_NAME ] ) ) {
return;
}
@@ -184,23 +184,6 @@ class User_Profile_Integration implements Integration_Interface {
\update_user_meta( $user_id, 'wpseo_user_schema', $this->get_posted_user_fields() );
}
/**
* Builds the arguments for filter_var_array which makes sure we only get the fields that we've defined above.
*
* @return array Filter arguments.
*/
private function build_filter_args() {
$args = [];
foreach ( $this->fields as $key => $field ) {
if ( $field['type'] === 'group' ) {
continue;
}
$args[ $key ] = \FILTER_SANITIZE_STRING;
}
return $args;
}
/**
* Gets the posted user fields and sanitizes them.
*
@@ -209,14 +192,12 @@ class User_Profile_Integration implements Integration_Interface {
* @return array The posted user fields, restricted to allowed fields.
*/
private function get_posted_user_fields() {
$args = [
'wpseo_user_schema' => [
'filter' => \FILTER_SANITIZE_STRING,
'flags' => \FILTER_FORCE_ARRAY,
],
];
$user_schema = \filter_input_array( \INPUT_POST, $args )['wpseo_user_schema'];
$user_schema = \filter_var_array( $user_schema, $this->build_filter_args(), false );
$user_schema = [];
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce is verified in process_user_option_update.
if ( isset( $_POST['wpseo_user_schema'] ) && \is_array( $_POST['wpseo_user_schema'] ) ) {
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce is verified in process_user_option_update.
$user_schema = \array_map( 'sanitize_text_field', \wp_unslash( $_POST['wpseo_user_schema'] ) );
}
foreach ( $this->fields as $key => $object ) {
switch ( $object['type'] ) {

View File

@@ -0,0 +1,180 @@
<?php
namespace Yoast\WP\SEO\Premium\Integrations\Admin;
use WPSEO_Premium_Asset_JS_L10n;
use WPSEO_Shortlinker;
use Yoast\WP\SEO\Conditionals\Admin_Conditional;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Post_Type_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Premium\Helpers\Prominent_Words_Helper;
use Yoast\WP\SEO\Repositories\Indexable_Repository;
/**
* WorkoutsIntegration class
*/
class Workouts_Integration implements Integration_Interface {
/**
* The indexable repository.
*
* @var Indexable_Repository The indexable repository.
*/
private $indexable_repository;
/**
* The shortlinker.
*
* @var WPSEO_Shortlinker
*/
private $shortlinker;
/**
* The options helper.
*
* @var Options_Helper
*/
private $options_helper;
/**
* The prominent words helper.
*
* @var Prominent_Words_Helper
*/
private $prominent_words_helper;
/**
* The post type helper.
*
* @var Post_Type_Helper
*/
private $post_type_helper;
/**
* {@inheritDoc}
*/
public static function get_conditionals() {
return [ Admin_Conditional::class ];
}
/**
* Workouts_Integration constructor.
*
* @param Indexable_Repository $indexable_repository The indexables repository.
* @param WPSEO_Shortlinker $shortlinker The shortlinker.
* @param Options_Helper $options_helper The options helper.
* @param Prominent_Words_Helper $prominent_words_helper The prominent words helper.
* @param Post_Type_Helper $post_type_helper The post type helper.
*/
public function __construct(
Indexable_Repository $indexable_repository,
WPSEO_Shortlinker $shortlinker,
Options_Helper $options_helper,
Prominent_Words_Helper $prominent_words_helper,
Post_Type_Helper $post_type_helper
) {
$this->indexable_repository = $indexable_repository;
$this->shortlinker = $shortlinker;
$this->options_helper = $options_helper;
$this->prominent_words_helper = $prominent_words_helper;
$this->post_type_helper = $post_type_helper;
}
/**
* {@inheritDoc}
*/
public function register_hooks() {
\add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] );
}
/**
* Enqueue the workouts app.
*/
public function enqueue_assets() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Date is not processed or saved.
if ( ! isset( $_GET['page'] ) || $_GET['page'] !== 'wpseo_workouts' ) {
return;
}
$workouts_option = $this->options_helper->get( 'workouts' );
$indexable_ids_in_workouts = [ 0 ];
if ( isset( $workouts_option['orphaned']['indexablesByStep'] )
&& \is_array( $workouts_option['orphaned']['indexablesByStep'] )
&& isset( $workouts_option['cornerstone']['indexablesByStep'] )
&& \is_array( $workouts_option['cornerstone']['indexablesByStep'] )
) {
foreach ( [ 'orphaned', 'cornerstone' ] as $workout ) {
foreach ( $workouts_option[ $workout ]['indexablesByStep'] as $step => $indexables ) {
if ( $step === 'removed' ) {
continue;
}
foreach ( $indexables as $indexable_id ) {
$indexable_ids_in_workouts[] = $indexable_id;
}
}
}
}
$orphaned = $this->get_orphaned( $indexable_ids_in_workouts );
$premium_localization = new WPSEO_Premium_Asset_JS_L10n();
$premium_localization->localize_script( 'yoast-seo-premium-workouts' );
\wp_enqueue_script( 'yoast-seo-premium-workouts' );
\wp_localize_script(
'yoast-seo-premium-workouts',
'wpseoPremiumWorkoutsData',
[
'cornerstoneGuide' => $this->shortlinker->build_shortlink( 'https://yoa.st/4el' ),
'orphanedGuide' => $this->shortlinker->build_shortlink( 'https://yoa.st/4fa' ),
'orphanedUpdateContent' => $this->shortlinker->build_shortlink( 'https://yoa.st/4h9' ),
'cornerstoneOn' => $this->options_helper->get( 'enable_cornerstone_content' ),
'seoDataOptimizationNeeded' => ! $this->prominent_words_helper->is_indexing_completed(),
'orphaned' => $orphaned,
]
);
}
/**
* Retrieves the public indexable sub types.
*
* @return array The sub types.
*/
protected function get_public_sub_types() {
$object_sub_types = \array_values(
\array_merge(
$this->post_type_helper->get_public_post_types(),
\get_taxonomies( [ 'public' => true ] )
)
);
$excluded_post_types = \apply_filters( 'wpseo_indexable_excluded_post_types', [ 'attachment' ] );
$object_sub_types = \array_diff( $object_sub_types, $excluded_post_types );
return $object_sub_types;
}
/**
* Gets the orphaned indexables.
*
* @param array $indexable_ids_in_orphaned_workout The orphaned indexable ids.
* @param int $limit The limit.
*
* @return array The orphaned indexables.
*/
protected function get_orphaned( array $indexable_ids_in_orphaned_workout, $limit = 10 ) {
$orphaned = $this->indexable_repository->query()
->where_raw( '( incoming_link_count is NULL OR incoming_link_count < 3 )' )
->where_raw( '( post_status = \'publish\' OR post_status IS NULL )' )
->where_raw( '( is_robots_noindex = FALSE OR is_robots_noindex IS NULL )' )
->where_raw( 'NOT ( object_sub_type = \'page\' AND permalink = %s )', [ \home_url( '/' ) ] )
->where_in( 'object_sub_type', $this->get_public_sub_types() )
->where_in( 'object_type', [ 'post' ] )
->where_not_in( 'id', $indexable_ids_in_orphaned_workout )
->order_by_asc( 'created_at' )
->limit( $limit )
->find_many();
$orphaned = \array_map( [ $this->indexable_repository, 'ensure_permalink' ], $orphaned );
return $orphaned;
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Yoast\WP\SEO\Premium\Integrations\Alerts;
use Yoast\WP\SEO\Integrations\Alerts\Abstract_Dismissable_Alert;
/**
* Registers a dismissible alert.
*
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
*/
class Ai_Generator_Tip_Notification extends Abstract_Dismissable_Alert {
/**
* Holds the alert identifier.
*
* @var string
*/
protected $alert_identifier = 'wpseo_premium_ai_generator';
}

View File

@@ -1,9 +1,13 @@
<?php
namespace Yoast\WP\SEO\Integrations\Blocks;
namespace Yoast\WP\SEO\Premium\Integrations\Blocks;
use Yoast\WP\SEO\Integrations\Blocks\Dynamic_Block;
/**
* Estimated_Reading_Time_Block class.
*
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
*/
class Estimated_Reading_Time_Block extends Dynamic_Block {
@@ -74,6 +78,13 @@ class Estimated_Reading_Time_Block extends Dynamic_Block {
* @return string The block output.
*/
public function present( $attributes, $content = '' ) {
$content = \preg_replace(
'/<span class="yoast-reading-time__time-unit">.*<\/span>/',
'<span class="yoast-reading-time__time-unit"> ' . \sprintf( \_n( 'minute', 'minutes', $attributes['estimatedReadingTime'], 'wordpress-seo-premium' ), $attributes['estimatedReadingTime'] ) . '</span>',
$content,
1
);
if ( $attributes['showIcon'] ) {
// Replace 15.7 icon placeholder.
$content = \preg_replace(

View File

@@ -1,6 +1,6 @@
<?php
namespace Yoast\WP\SEO\Integrations\Blocks;
namespace Yoast\WP\SEO\Premium\Integrations\Blocks;
use Yoast\WP\SEO\Conditionals\No_Conditionals;
use Yoast\WP\SEO\Integrations\Integration_Interface;

View File

@@ -0,0 +1,255 @@
<?php
namespace Yoast\WP\SEO\Premium\Integrations;
use wpdb;
use Yoast\WP\Lib\Model;
use Yoast\WP\SEO\Analytics\Domain\To_Be_Cleaned_Indexable_Bucket;
use Yoast\WP\SEO\Analytics\Domain\To_Be_Cleaned_Indexable_Count;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Repositories\Indexable_Cleanup_Repository;
/**
* Adds cleanup hooks.
*/
class Cleanup_Integration implements Integration_Interface {
/**
* The indexable cleanup repository.
*
* @var Indexable_Cleanup_Repository
*/
private $indexable_cleanup_repository;
/**
* The constructor.
*
* @param Indexable_Cleanup_Repository $indexable_cleanup_repository The indexable cleanup repository.
*/
public function __construct( Indexable_Cleanup_Repository $indexable_cleanup_repository ) {
$this->indexable_cleanup_repository = $indexable_cleanup_repository;
}
/**
* Returns the conditionals based in which this loadable should be active.
*
* @return array The array of conditionals.
*/
public static function get_conditionals() {
return [];
}
/**
* Initializes the integration.
*
* This is the place to register hooks and filters.
*
* @return void
*/
public function register_hooks() {
\add_filter( 'wpseo_cleanup_tasks', [ $this, 'add_cleanup_tasks' ] );
\add_action( 'wpseo_add_cleanup_counts_to_indexable_bucket', [ $this, 'add_cleanup_counts' ] );
}
/**
* Adds cleanup tasks for the cleanup integration.
*
* @param array $tasks Array of tasks to be added.
*
* @return array An associative array of tasks to be added to the cleanup integration.
*/
public function add_cleanup_tasks( $tasks ) {
return \array_merge(
$tasks,
[
'clean_orphaned_indexables_prominent_words' => function ( $limit ) {
return $this->cleanup_orphaned_from_table( 'Prominent_Words', 'indexable_id', $limit );
},
'clean_old_prominent_word_entries' => function ( $limit ) {
return $this->cleanup_old_prominent_words( $limit );
},
'clean_old_prominent_word_version_numbers' => function ( $limit ) {
return $this->cleanup_old_prominent_word_version_numbers( $limit );
},
]
);
}
/**
* Adds cleanup counts to the data bucket object.
*
* @param To_Be_Cleaned_Indexable_Bucket $to_be_cleaned_indexable_bucket The bucket with current indexable count data.
*
* @return void
*/
public function add_cleanup_counts( To_Be_Cleaned_Indexable_Bucket $to_be_cleaned_indexable_bucket ): void {
$to_be_cleaned_indexable_bucket->add_to_be_cleaned_indexable_count( new To_Be_Cleaned_Indexable_Count( 'orphaned_indexables_prominent_words', $this->indexable_cleanup_repository->count_orphaned_from_table( 'Prominent_Words', 'indexable_id' ) ) );
$to_be_cleaned_indexable_bucket->add_to_be_cleaned_indexable_count( new To_Be_Cleaned_Indexable_Count( 'orphaned_prominent_word_entries', $this->count_old_prominent_words() ) );
$to_be_cleaned_indexable_bucket->add_to_be_cleaned_indexable_count( new To_Be_Cleaned_Indexable_Count( 'orphaned_prominent_word_version_numbers', $this->count_old_prominent_word_version_numbers() ) );
}
/**
* Cleans orphaned rows from a yoast table.
*
* @param string $table The table to cleanup.
* @param string $column The table column the cleanup will rely on.
* @param int $limit The limit we'll apply to the queries.
*
* @return int The number of deleted rows.
*/
public function cleanup_orphaned_from_table( $table, $column, $limit ) {
global $wpdb;
$table = Model::get_table_name( $table );
$indexable_table = Model::get_table_name( 'Indexable' );
// Warning: If this query is changed, make sure to update the query in cleanup_orphaned_from_table in Free as well.
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: There is no unescaped user input.
$query = $wpdb->prepare(
"
SELECT table_to_clean.{$column}
FROM {$table} table_to_clean
LEFT JOIN {$indexable_table} AS indexable_table
ON table_to_clean.{$column} = indexable_table.id
WHERE indexable_table.id IS NULL
AND table_to_clean.{$column} IS NOT NULL
LIMIT %d",
$limit
);
// phpcs:enable
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: Already prepared.
$orphans = $wpdb->get_col( $query );
if ( empty( $orphans ) ) {
return 0;
}
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: Already prepared.
return \intval( $wpdb->query( "DELETE FROM $table WHERE {$column} IN( " . \implode( ',', $orphans ) . ' ) ' ) );
}
/**
* Cleans up old style prominent words from the database.
*
* @param int $limit The maximum amount of old prominent words to clean up in one go. Defaults to 1000.
*
* @return int The number of deleted rows.
*/
public function cleanup_old_prominent_words( $limit = 1000 ) {
global $wpdb;
$taxonomy_ids = $this->retrieve_prominent_word_taxonomies( $wpdb, $limit );
if ( \count( $taxonomy_ids ) === 0 ) {
return 0;
}
$nr_of_deleted_rows = $this->delete_prominent_word_taxonomies_and_terms( $wpdb, $taxonomy_ids );
if ( $nr_of_deleted_rows === false ) {
// Failed query.
return 0;
}
return $nr_of_deleted_rows;
}
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
/**
* Count up old style prominent words from the database.
*
* @return int The number of old prominent word rows.
*/
public function count_old_prominent_words() {
global $wpdb;
$query = $wpdb->prepare(
"SELECT count(term_taxonomy_id) FROM {$wpdb->term_taxonomy} WHERE taxonomy = %s",
[ 'yst_prominent_words' ]
);
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: Already prepared.
return $wpdb->get_col( $query )[0];
}
/**
* Retrieve a list of prominent word taxonomy IDs.
*
* @param wpdb $wpdb The WordPress database object.
* @param int $limit The maximum amount of prominent word taxonomies to retrieve.
*
* @return string[] A list of prominent word taxonomy IDs (of size 'limit').
*/
protected function retrieve_prominent_word_taxonomies( $wpdb, $limit ) {
return $wpdb->get_col(
$wpdb->prepare(
"SELECT term_taxonomy_id FROM {$wpdb->term_taxonomy} WHERE taxonomy = %s LIMIT %d",
[ 'yst_prominent_words', $limit ]
)
);
}
/**
* Deletes the given list of taxonomies and their terms.
*
* @param wpdb $wpdb The WordPress database object.
* @param string[] $taxonomy_ids The IDs of the taxonomies to remove and their corresponding terms.
*
* @return bool|int `false` if the query failed, the amount of rows deleted otherwise.
*/
protected function delete_prominent_word_taxonomies_and_terms( $wpdb, $taxonomy_ids ) {
return $wpdb->query(
$wpdb->prepare(
"DELETE t, tr, tt FROM {$wpdb->term_taxonomy} tt
LEFT JOIN {$wpdb->terms} t ON tt.term_id = t.term_id
LEFT JOIN {$wpdb->term_relationships} tr ON tt.term_taxonomy_id = tr.term_taxonomy_id
WHERE tt.term_taxonomy_id IN ( " . \implode( ', ', \array_fill( 0, \count( $taxonomy_ids ), '%s' ) ) . ' )',
$taxonomy_ids
)
);
}
/**
* Cleans up the old prominent word versions from the postmeta table in the database.
*
* @param int $limit The maximum number of prominent word version numbers to clean in one go.
*
* @return bool|int The number of cleaned up prominent word version numbers, or `false` if the query failed.
*/
protected function cleanup_old_prominent_word_version_numbers( $limit ) {
global $wpdb;
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: There is no unescaped user input.
$query = $wpdb->prepare(
"DELETE FROM {$wpdb->postmeta} WHERE meta_key = %s LIMIT %d",
[ '_yst_prominent_words_version', $limit ]
);
// phpcs:enable
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: Already prepared.
return $wpdb->query( $query );
}
/**
* Counts up the old prominent word versions from the postmeta table in the database.
*
* @return bool|int The number of prominent word version numbers.
*/
protected function count_old_prominent_word_version_numbers() {
global $wpdb;
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: There is no unescaped user input.
$query = $wpdb->prepare(
"SELECT count(*) FROM {$wpdb->postmeta} WHERE meta_key = %s",
[ '_yst_prominent_words_version' ]
);
// phpcs:enable
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: Already prepared.
return $wpdb->get_col( $query )[0];
}
// phpcs:enable
}

View File

@@ -0,0 +1,95 @@
<?php
namespace Yoast\WP\SEO\Premium\Integrations\Front_End;
use Yoast\WP\SEO\Conditionals\Robots_Txt_Conditional;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Robots_Txt_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
/**
* Handles adding the rules to `robots.txt`.
*/
class Robots_Txt_Integration implements Integration_Interface {
/**
* Holds the options helper.
*
* @var Options_Helper
*/
protected $options_helper;
/**
* Instantiates the `robots.txt` integration.
*
* @param Options_Helper $options_helper Options helper.
*/
public function __construct( Options_Helper $options_helper ) {
$this->options_helper = $options_helper;
}
/**
* Returns the conditionals based in which this loadable should be active.
*
* @return array
*/
public static function get_conditionals() {
return [ Robots_Txt_Conditional::class ];
}
/**
* Initializes the integration.
*
* This is the place to register hooks and filters.
*
* @return void
*/
public function register_hooks() {
if ( \is_multisite() ) {
return;
}
if ( $this->options_helper->get( 'deny_ccbot_crawling' ) ) {
\add_action( 'Yoast\WP\SEO\register_robots_rules', [ $this, 'add_disallow_ccbot' ], 10, 1 );
}
if ( $this->options_helper->get( 'deny_google_extended_crawling' ) ) {
\add_action( 'Yoast\WP\SEO\register_robots_rules', [ $this, 'add_disallow_google_extended_bot' ], 10, 1 );
}
if ( $this->options_helper->get( 'deny_gptbot_crawling' ) ) {
\add_action( 'Yoast\WP\SEO\register_robots_rules', [ $this, 'add_disallow_gptbot' ], 10, 1 );
}
}
/**
* Add a disallow rule for Common Crawl CCBot agents to `robots.txt`.
*
* @param Robots_Txt_Helper $robots_txt_helper The Robots_Txt_Helper.
*
* @return void
*/
public function add_disallow_ccbot( Robots_Txt_Helper $robots_txt_helper ) {
$robots_txt_helper->add_disallow( 'CCBot', '/' );
}
/**
* Add a disallow rule for Google-Extended agents to `robots.txt`.
*
* @param Robots_Txt_Helper $robots_txt_helper The Robots_Txt_Helper.
*
* @return void
*/
public function add_disallow_google_extended_bot( Robots_Txt_Helper $robots_txt_helper ) {
$robots_txt_helper->add_disallow( 'Google-Extended', '/' );
}
/**
* Add a disallow rule for OpenAI GPTBot agents to `robots.txt`.
*
* @param Robots_Txt_Helper $robots_txt_helper The Robots_Txt_Helper.
*
* @return void
*/
public function add_disallow_gptbot( Robots_Txt_Helper $robots_txt_helper ) {
$robots_txt_helper->add_disallow( 'GPTBot', '/' );
}
}

View File

@@ -0,0 +1,151 @@
<?php
namespace Yoast\WP\SEO\Premium\Integrations;
use WP_Admin_Bar;
use WPSEO_Metabox_Analysis_Readability;
use WPSEO_Metabox_Analysis_SEO;
use WPSEO_Options;
use Yoast\WP\SEO\Conditionals\Front_End_Conditional;
use Yoast\WP\SEO\Helpers\Robots_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
/**
* Frontend_Inspector class
*/
class Frontend_Inspector implements Integration_Interface {
/**
* The identifier used for the frontend inspector submenu.
*
* @var string
*/
const FRONTEND_INSPECTOR_SUBMENU_IDENTIFIER = 'wpseo-frontend-inspector';
/**
* Holds the Robots_Helper.
*
* @var Robots_Helper
*/
protected $robots_helper;
/**
* Constructs a Frontend_Inspector.
*
* @param Robots_Helper $robots_helper The Robots_Helper.
*/
public function __construct( Robots_Helper $robots_helper ) {
$this->robots_helper = $robots_helper;
}
/**
* {@inheritDoc}
*/
public static function get_conditionals() {
return [ Front_End_Conditional::class ];
}
/**
* {@inheritDoc}
*/
public function register_hooks() {
\add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_assets' ], 11 );
\add_action( 'wpseo_add_adminbar_submenu', [ $this, 'add_frontend_inspector_submenu' ], 10, 2 );
}
/**
* Adds the frontend inspector submenu.
*
* @param WP_Admin_Bar $wp_admin_bar The admin bar.
* @param string $menu_identifier The menu identifier.
*
* @return void
*/
public function add_frontend_inspector_submenu( WP_Admin_Bar $wp_admin_bar, $menu_identifier ) {
if ( ! \is_admin() ) {
$menu_args = [
'parent' => $menu_identifier,
'id' => self::FRONTEND_INSPECTOR_SUBMENU_IDENTIFIER,
'title' => \sprintf(
'%1$s <span class="yoast-badge yoast-beta-badge">%2$s</span>',
\__( 'Front-end SEO inspector', 'wordpress-seo-premium' ),
\__( 'Beta', 'wordpress-seo-premium' )
),
'href' => '#wpseo-frontend-inspector',
'meta' => [
'tabindex' => '0',
],
];
$wp_admin_bar->add_menu( $menu_args );
}
}
/**
* Enqueue the workouts app.
*/
public function enqueue_assets() {
if ( ! \is_admin_bar_showing() || ! WPSEO_Options::get( 'enable_admin_bar_menu' ) ) {
return;
}
// If the current user can't write posts, this is all of no use, so let's not output an admin menu.
if ( ! \current_user_can( 'edit_posts' ) ) {
return;
}
$analysis_seo = new WPSEO_Metabox_Analysis_SEO();
$analysis_readability = new WPSEO_Metabox_Analysis_Readability();
$current_page_meta = \YoastSEO()->meta->for_current_page();
$indexable = $current_page_meta->indexable;
$page_type = $current_page_meta->page_type;
$is_seo_analysis_active = $analysis_seo->is_enabled();
$is_readability_analysis_active = $analysis_readability->is_enabled();
$display_metabox = true;
switch ( $page_type ) {
case 'Home_Page':
case 'Post_Type_Archive':
case 'Date_Archive':
case 'Error_Page':
case 'Fallback':
case 'Search_Result_Page':
break;
case 'Static_Home_Page':
case 'Static_Posts_Page':
case 'Post_Type':
$display_metabox = WPSEO_Options::get( 'display-metabox-pt-' . $indexable->object_sub_type );
break;
case 'Term_Archive':
$display_metabox = WPSEO_Options::get( 'display-metabox-tax-' . $indexable->object_sub_type );
break;
case 'Author_Archive':
$display_metabox = false;
break;
}
if ( ! $display_metabox ) {
$is_seo_analysis_active = false;
$is_readability_analysis_active = false;
}
\wp_enqueue_script( 'yoast-seo-premium-frontend-inspector' );
\wp_localize_script(
'yoast-seo-premium-frontend-inspector',
'wpseoScriptData',
[
'frontendInspector' => [
'isIndexable' => $this->robots_helper->is_indexable( $indexable ),
'indexable' => [
'is_robots_noindex' => $indexable->is_robots_noindex,
'primary_focus_keyword' => $indexable->primary_focus_keyword,
'primary_focus_keyword_score' => $indexable->primary_focus_keyword_score,
'readability_score' => $indexable->readability_score,
],
'contentAnalysisActive' => $is_readability_analysis_active,
'keywordAnalysisActive' => $is_seo_analysis_active,
],
]
);
}
}

View File

@@ -0,0 +1,191 @@
<?php
namespace Yoast\WP\SEO\Premium\Integrations;
use WP_Post;
use WPSEO_Remote_Request;
use Yoast\WP\SEO\Conditionals\No_Conditionals;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Post_Type_Helper;
use Yoast\WP\SEO\Helpers\Request_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
/**
* Index_Now_Ping class.
*/
class Index_Now_Ping implements Integration_Interface {
use No_Conditionals;
/**
* The options helper.
*
* @var Options_Helper
*/
private $options_helper;
/**
* The request helper.
*
* @var Request_Helper
*/
private $request_helper;
/**
* The post type helper.
*
* @var Post_Type_Helper
*/
private $post_type_helper;
/**
* The IndexNow endpoint URL we're using.
*
* @var string
*/
private $endpoint;
/**
* Index_Now_Ping integration constructor.
*
* @param Options_Helper $options_helper The option helper.
* @param Request_Helper $request_helper The request helper.
* @param Post_Type_Helper $post_type_helper The post type helper.
*/
public function __construct(
Options_Helper $options_helper,
Request_Helper $request_helper,
Post_Type_Helper $post_type_helper
) {
$this->options_helper = $options_helper;
$this->request_helper = $request_helper;
$this->post_type_helper = $post_type_helper;
/**
* Filter: 'Yoast\WP\SEO\indexnow_endpoint' - Allow changing the Indexnow endpoint.
*
* Note: This is a Premium plugin-only hook.
*
* @since 18.8
*
* @param string $endpoint The IndexNow endpoint URL.
*/
$this->endpoint = \apply_filters( 'Yoast\WP\SEO\indexnow_endpoint', 'https://api.indexnow.org/indexnow' );
}
/**
* Registers the hooks this integration acts on.
*
* @return void
*/
public function register_hooks() {
if ( $this->options_helper->get( 'enable_index_now' ) === false ) {
return;
}
if ( \wp_get_environment_type() !== 'production' ) {
return;
}
/**
* Please note that the name transition_post_status is misleading.
* The hook does not only fire on a post status transition but also when a post is updated
* while the status is not changed from one to another at all.
*/
\add_action( 'transition_post_status', [ $this, 'ping_index_now' ], 10, 3 );
}
/**
* Pings IndexNow for changes.
*
* @param string $new_status The new status for the post.
* @param string $old_status The old status for the post.
* @param WP_Post $post The post.
*
* @return void
*/
public function ping_index_now( $new_status, $old_status, $post ) {
if ( $new_status !== 'publish' && $old_status !== 'publish' ) {
// If we're not transitioning to or from a published status, do nothing.
return;
}
// The block editor saves published posts twice, we want to ping only on the first request.
if ( $new_status === 'publish' && $this->request_helper->is_rest_request() ) {
return;
}
if ( ! $post instanceof WP_Post ) {
return;
}
if ( ! \in_array( $post->post_type, $this->post_type_helper->get_accessible_post_types(), true )
|| ! $this->post_type_helper->is_indexable( $post->post_type ) ) {
return;
}
// Bail out if last ping was less than two minutes ago.
$indexnow_last_ping = \get_post_meta( $post->ID, '_yoast_indexnow_last_ping', true );
if ( \is_numeric( $indexnow_last_ping ) && \abs( \time() - ( (int) $indexnow_last_ping ) ) < 120 ) {
return;
}
$key = $this->options_helper->get( 'index_now_key' );
$permalink = $this->get_permalink( $post );
$urls = [ $permalink ];
if ( $post->post_type === 'post' ) {
$urls[] = \get_home_url();
}
if ( ! empty( \get_option( 'permalink_structure' ) ) ) {
$key_location = \trailingslashit( \get_home_url() ) . 'yoast-index-now-' . $key . '.txt';
}
else {
$key_location = \add_query_arg( 'yoast_index_now_key', $key, \trailingslashit( \get_home_url() ) );
}
$content = (object) [
'host' => \wp_parse_url( \get_home_url(), \PHP_URL_HOST ),
'key' => $key,
'keyLocation' => $key_location,
'urlList' => $urls,
];
// Set a 'content-type' header of 'application/json' and an identifying source header.
// The "false" on the end of the x-source-info header determines whether this is a manual submission or not.
$request_args = [
'headers' => [
'content-type' => 'application/json; charset=utf-8',
'x-source-info' => 'https://yoast.com/wordpress/plugins/seo-premium/' . \WPSEO_PREMIUM_VERSION . '/false',
],
];
$request = new WPSEO_Remote_Request( $this->endpoint, $request_args );
// phpcs:ignore Yoast.Yoast.AlternativeFunctions.json_encode_wp_json_encode -- This is being sent to an API, not displayed.
$request->set_body( \wp_json_encode( $content ) );
$request->send();
\update_post_meta( $post->ID, '_yoast_indexnow_last_ping', \time() );
}
/**
* Determines the (former) permalink for a post.
*
* @param WP_Post $post Post object.
*
* @return string Permalink.
*/
private function get_permalink( WP_Post $post ) {
if ( \in_array( $post->post_status, [ 'trash', 'draft', 'pending', 'future' ], true ) ) {
if ( $post->post_status === 'trash' ) {
// Fix the post_name.
$post->post_name = \preg_replace( '/__trashed$/', '', $post->post_name );
}
// Force post_status to publish briefly, so we get the correct URL.
$post->post_status = 'publish';
}
return \get_permalink( $post );
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace Yoast\WP\SEO\Premium\Integrations;
use Yoast\WP\SEO\Actions\Indexing\Indexation_Action_Interface;
use Yoast\WP\SEO\Conditionals\No_Conditionals;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Premium\Actions\Prominent_Words\Content_Action;
/**
* Adds prominent words to the missing indexables bucket.
*
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
*/
class Missing_Indexables_Count_Integration implements Integration_Interface {
use No_Conditionals;
/**
* The content indexable action.
*
* @var Content_Action
*/
private $content_action;
/**
* The constructor.
*
* @param Content_Action $content_action The action.
*/
public function __construct( Content_Action $content_action ) {
$this->content_action = $content_action;
}
/**
* Registers hooks with WordPress.
*/
public function register_hooks() {
\add_filter( 'wpseo_indexable_collector_add_indexation_actions', [ $this, 'add_index_action' ] );
}
/**
* Adds the Content_Action to the indexable collector.
*
* @param array<Indexation_Action_Interface> $indexation_actions The current indexation actions.
* @return array<Indexation_Action_Interface>
*/
public function add_index_action( $indexation_actions ) {
$indexation_actions[] = $this->content_action;
return $indexation_actions;
}
}

View File

@@ -0,0 +1,201 @@
<?php
namespace Yoast\WP\SEO\Premium\Integrations;
use Yoast\WP\SEO\Conditionals\Front_End_Conditional;
use Yoast\WP\SEO\Helpers\Indexable_Helper;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Post_Type_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Repositories\Indexable_Repository;
use function get_post;
/**
* Integration to add Publishing Principles to the Schema.
*
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
*/
class Publishing_Principles_Schema_Integration implements Integration_Interface {
/**
* Constant holding the mapping between database option and actual schema name.
*/
public const PRINCIPLES_MAPPING = [
[ 'publishing_principles_id', 'publishingPrinciples' ],
[ 'ownership_funding_info_id', 'ownershipFundingInfo' ],
[ 'actionable_feedback_policy_id', 'actionableFeedbackPolicy' ],
[ 'corrections_policy_id', 'correctionsPolicy' ],
[ 'ethics_policy_id', 'ethicsPolicy' ],
[ 'diversity_policy_id', 'diversityPolicy' ],
[ 'diversity_staffing_report_id', 'diversityStaffingReport' ],
];
/**
* The indexable helper.
*
* @var Indexable_Helper $indexable_helper
*/
private $indexable_helper;
/**
* The post type helper.
*
* @var Post_Type_Helper $post_type_helper
*/
private $post_type_helper;
/**
* The options helper.
*
* @var Options_Helper $options_helper
*/
private $options_helper;
/**
* The indexable repository.
*
* @var Indexable_Repository
*/
private $indexable_repository;
/**
* Returns the conditionals based on which this loadable should be active.
*
* @return array
*/
public static function get_conditionals() {
return [ Front_End_Conditional::class ];
}
/**
* Publishing_Principles_Schema_Integration constructor.
*
* @param Options_Helper $options_helper The options helper.
* @param Indexable_Repository $indexable_repository The indexables repository.
* @param Indexable_Helper $indexable_helper The indexables helper.
* @param Post_Type_Helper $post_type_helper The post type helper.
*/
public function __construct(
Options_Helper $options_helper,
Indexable_Repository $indexable_repository,
Indexable_Helper $indexable_helper,
Post_Type_Helper $post_type_helper
) {
$this->options_helper = $options_helper;
$this->indexable_repository = $indexable_repository;
$this->indexable_helper = $indexable_helper;
$this->post_type_helper = $post_type_helper;
}
/**
* Initializes the integration.
*
* This is the place to register hooks and filters.
*
* @return void
*/
public function register_hooks() {
\add_filter( 'wpseo_schema_organization', [ $this, 'filter_organization_schema' ] );
}
/**
* Make sure the Organization policies are added to the schema output.
*
* @param array $data The organization schema.
*
* @return array
*/
public function filter_organization_schema( $data ) {
$policy_indexables = $this->get_indexables_for_publishing_principle_pages(
self::PRINCIPLES_MAPPING
);
foreach ( $policy_indexables as $policy_data ) {
$data = $this->add_schema_piece( $data, $policy_data );
}
return $data;
}
/**
* Adds the data to the schema array.
*
* @param array $schema_graph The current schema graph.
* @param array $policy_data The data present for a policy.
*
* @return array The new schema graph.
*/
private function add_schema_piece( $schema_graph, $policy_data ): array {
if ( ! \is_null( $policy_data['permalink'] ) ) {
$schema_graph[ $policy_data['schema'] ] = $policy_data['permalink'];
}
return $schema_graph;
}
/**
* Finds the indexables for all the given principles if they are set.
*
* @param array $principles_data The data for all the principles.
*
* @return array
*/
private function get_indexables_for_publishing_principle_pages( $principles_data ): array {
$principle_ids = [];
$policies = [];
$ids = [];
foreach ( $principles_data as $principle ) {
$option_value = $this->options_helper->get( $principle[0], false );
if ( $option_value ) {
$principle_ids[ $principle[0] ] = [
'value' => $option_value,
'schema' => $principle[1],
];
$ids[] = $option_value;
}
}
if ( \count( $ids ) === 0 ) {
// Early return to not run an empty query.
return [];
}
if ( $this->indexable_helper->should_index_indexables() && $this->post_type_helper->is_of_indexable_post_type( 'page' ) ) {
$indexables = $this->indexable_repository->find_by_multiple_ids_and_type( array_unique( $ids ), 'post' );
foreach ( $principle_ids as $key => $principle_id ) {
foreach ( $indexables as $indexable ) {
if ( $indexable && $principle_id['value'] === $indexable->object_id ) {
if ( $indexable->post_status === 'publish' && $indexable->is_protected === false ) {
$policies[ $key ] = [
'permalink' => $indexable->permalink,
'schema' => $principle_id['schema'],
];
}
break;
}
}
}
return $policies;
}
foreach ( $principle_ids as $key => $principle_id ) {
foreach ( $ids as $post_id ) {
$post = get_post( (int) $post_id );
if ( is_object( $post ) ) {
if ( (int) $principle_id['value'] === (int) $post_id && \get_post_status( $post_id ) === 'publish' && $post->post_password === '' ) {
$policies[ $key ] = [
'permalink' => get_permalink( $post_id ),
'schema' => $principle_id['schema'],
];
break;
}
}
}
}
return $policies;
}
}

View File

@@ -0,0 +1,301 @@
<?php
namespace Yoast\WP\SEO\Premium\Integrations\Routes;
use RuntimeException;
use WP_REST_Request;
use WP_REST_Response;
use Yoast\WP\SEO\Conditionals\No_Conditionals;
use Yoast\WP\SEO\Main;
use Yoast\WP\SEO\Premium\Actions\AI_Generator_Action;
use Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Bad_Request_Exception;
use Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Forbidden_Exception;
use Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Internal_Server_Error_Exception;
use Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Not_Found_Exception;
use Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Payment_Required_Exception;
use Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Request_Timeout_Exception;
use Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Service_Unavailable_Exception;
use Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Too_Many_Requests_Exception;
use Yoast\WP\SEO\Premium\Exceptions\Remote_Request\Unauthorized_Exception;
use Yoast\WP\SEO\Premium\Helpers\AI_Generator_Helper;
use Yoast\WP\SEO\Routes\Route_Interface;
/**
* Registers the route for the AI_Generator integration.
*/
class AI_Generator_Route implements Route_Interface {
use No_Conditionals;
/**
* The AI_Generator route prefix.
*
* @var string
*/
const ROUTE_PREFIX = 'ai_generator';
/**
* The callback route constant (invoked by the API).
*
* @var string
*/
const CALLBACK_ROUTE = self::ROUTE_PREFIX . '/callback';
/**
* The refresh callback route constant (invoked by the API).
*
* @var string
*/
const REFRESH_CALLBACK_ROUTE = self::ROUTE_PREFIX . '/refresh_callback';
/**
* The get_suggestions route constant.
*
* @var string
*/
const GET_SUGGESTIONS_ROUTE = self::ROUTE_PREFIX . '/get_suggestions';
/**
* The get_suggestions route constant.
*
* @var string
*/
const CONSENT_ROUTE = self::ROUTE_PREFIX . '/consent';
/**
* The bust_subscription_cache route constant.
*
* @var string
*/
const BUST_SUBSCRIPTION_CACHE_ROUTE = self::ROUTE_PREFIX . '/bust_subscription_cache';
/**
* Instance of the AI_Generator_Action.
*
* @var AI_Generator_Action
*/
protected $ai_generator_action;
/**
* Instance of the AI_Generator_Helper.
*
* @var AI_Generator_Helper
*/
protected $ai_generator_helper;
/**
* AI_Generator_Route constructor.
*
* @param AI_Generator_Action $ai_generator_action The action to handle the requests to the endpoint.
* @param AI_Generator_Helper $ai_generator_helper The AI_Generator helper.
*/
public function __construct( AI_Generator_Action $ai_generator_action, AI_Generator_Helper $ai_generator_helper ) {
$this->ai_generator_action = $ai_generator_action;
$this->ai_generator_helper = $ai_generator_helper;
}
/**
* Registers routes with WordPress.
*
* @return void
*/
public function register_routes() {
\register_rest_route(
Main::API_V1_NAMESPACE,
self::CONSENT_ROUTE,
[
'methods' => 'POST',
'args' => [
'consent' => [
'required' => true,
'type' => 'boolean',
'description' => 'Whether the consent to use AI-based services has been given by the user.',
],
],
'callback' => [ $this, 'consent' ],
'permission_callback' => [ $this, 'check_permissions' ],
]
);
// Avoid registering the other routes if the feature is not enabled.
if ( ! $this->ai_generator_helper->is_ai_generator_enabled() ) {
return;
}
$callback_route_args = [
'methods' => 'POST',
'args' => [
'access_jwt' => [
'required' => true,
'type' => 'string',
'description' => 'The access JWT.',
],
'refresh_jwt' => [
'required' => true,
'type' => 'string',
'description' => 'The JWT to be used when the access JWT needs to be refreshed.',
],
'code_challenge' => [
'required' => true,
'type' => 'string',
'description' => 'The SHA266 of the verification code used to check the authenticity of a callback call.',
],
'user_id' => [
'required' => true,
'type' => 'integer',
'description' => 'The id of the user associated to the code verifier.',
],
],
'callback' => [ $this, 'callback' ],
'permission_callback' => '__return_true',
];
\register_rest_route( Main::API_V1_NAMESPACE, self::CALLBACK_ROUTE, $callback_route_args );
\register_rest_route( Main::API_V1_NAMESPACE, self::REFRESH_CALLBACK_ROUTE, $callback_route_args );
\register_rest_route(
Main::API_V1_NAMESPACE,
self::GET_SUGGESTIONS_ROUTE,
[
'methods' => 'POST',
'args' => [
'type' => [
'required' => true,
'type' => 'string',
'enum' => [
'seo-title',
'meta-description',
],
'description' => 'The type of suggestion requested.',
],
'prompt_content' => [
'required' => true,
'type' => 'string',
'description' => 'The content needed by the prompt to ask for suggestions.',
],
'focus_keyphrase' => [
'required' => true,
'type' => 'string',
'description' => 'The focus keyphrase associated to the post.',
],
'language' => [
'required' => true,
'type' => 'string',
'description' => 'The language the post is written in.',
],
'platform' => [
'required' => true,
'type' => 'string',
'enum' => [
'Google',
'Facebook',
'Twitter',
],
'description' => 'The platform the post is intended for.',
],
],
'callback' => [ $this, 'get_suggestions' ],
'permission_callback' => [ $this, 'check_permissions' ],
]
);
\register_rest_route(
Main::API_V1_NAMESPACE,
self::BUST_SUBSCRIPTION_CACHE_ROUTE,
[
'methods' => 'POST',
'args' => [],
'callback' => [ $this, 'bust_subscription_cache' ],
'permission_callback' => [ $this, 'check_permissions' ],
]
);
}
/**
* Runs the callback to store connection credentials and the tokens locally.
*
* @param WP_REST_Request $request The request object.
*
* @return WP_REST_Response The response of the callback action.
*/
public function callback( WP_REST_Request $request ) {
try {
$code_verifier = $this->ai_generator_action->callback( $request['access_jwt'], $request['refresh_jwt'], $request['code_challenge'], $request['user_id'] );
} catch ( Unauthorized_Exception $e ) {
return new WP_REST_Response( 'Unauthorized.', 401 );
}
return new WP_REST_Response(
[
'message' => 'Tokens successfully stored.',
'code_verifier' => $code_verifier,
]
);
}
/**
* Runs the callback to get ai-generated suggestions.
*
* @param WP_REST_Request $request The request object.
*
* @return WP_REST_Response The response of the get_suggestions action.
*/
public function get_suggestions( WP_REST_Request $request ) {
try {
$user = \wp_get_current_user();
$data = $this->ai_generator_action->get_suggestions( $user, $request['type'], $request['prompt_content'], $request['focus_keyphrase'], $request['language'], $request['platform'] );
} catch ( Bad_Request_Exception | Forbidden_Exception | Internal_Server_Error_Exception | Not_Found_Exception | Payment_Required_Exception | Request_Timeout_Exception | Service_Unavailable_Exception | Too_Many_Requests_Exception | Unauthorized_Exception $e ) {
return new WP_REST_Response( $e->getMessage(), $e->getCode() );
} catch ( RuntimeException $e ) {
return new WP_REST_Response( 'Failed to get suggestions.', 500 );
}
return new WP_REST_Response( $data );
}
/**
* Runs the callback to store the consent given by the user to use AI-based services.
*
* @param WP_REST_Request $request The request object.
*
* @return WP_REST_Response The response of the callback action.
*/
public function consent( WP_REST_Request $request ) {
$user_id = \get_current_user_id();
$consent = \boolval( $request['consent'] );
try {
$this->ai_generator_action->consent( $user_id, $consent );
} catch ( Bad_Request_Exception | Forbidden_Exception | Internal_Server_Error_Exception | Not_Found_Exception | Payment_Required_Exception | Request_Timeout_Exception | Service_Unavailable_Exception | Too_Many_Requests_Exception | RuntimeException $e ) {
return new WP_REST_Response( ( $consent ) ? 'Failed to store consent.' : 'Failed to revoke consent.', 500 );
}
return new WP_REST_Response( ( $consent ) ? 'Consent successfully stored.' : 'Consent successfully revoked.' );
}
/**
* Runs the callback that busts the subscription cache.
*
* @return WP_REST_Response The response of the callback action.
*/
public function bust_subscription_cache() {
$this->ai_generator_action->bust_subscription_cache();
return new WP_REST_Response( 'Subscription cache successfully busted.' );
}
/**
* Checks:
* - if the user is logged
* - if the user can edit posts
*
* @return bool Whether the user is logged in, can edit posts and the feature is active.
*/
public function check_permissions() {
$user = \wp_get_current_user();
if ( $user === null || $user->ID < 1 ) {
return false;
}
return \user_can( $user, 'edit_posts' );
}
}

View File

@@ -0,0 +1,311 @@
<?php
namespace Yoast\WP\SEO\Premium\Integrations\Routes;
use WPSEO_Admin_Asset_Manager;
use WPSEO_Shortlinker;
use Yoast\WP\SEO\Conditionals\No_Conditionals;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Post_Type_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Premium\Actions\Link_Suggestions_Action;
use Yoast\WP\SEO\Premium\Helpers\Prominent_Words_Helper;
use Yoast\WP\SEO\Repositories\Indexable_Repository;
/**
* Workouts_Routes_Integration class
*/
class Workouts_Routes_Integration implements Integration_Interface {
use No_Conditionals;
/**
* Allowed cornerstone steps.
*
* @var array
*/
const ALLOWED_CORNERSTONE_STEPS = [
'chooseCornerstones',
'checkLinks',
'addLinks',
'improved',
'skipped',
];
/**
* Allowed orphaned steps.
*
* @var array
*/
const ALLOWED_ORPHANED_STEPS = [
'improveRemove',
'update',
'addLinks',
'removed',
'noindexed',
'improved',
'skipped',
];
/**
* The indexable repository.
*
* @var Indexable_Repository The indexable repository.
*/
private $indexable_repository;
/**
* The link suggestions action.
*
* @var Link_Suggestions_Action The action.
*/
private $link_suggestions_action;
/**
* The admin asset manager.
*
* @var WPSEO_Admin_Asset_Manager
*/
private $admin_asset_manager;
/**
* The shortlinker.
*
* @var WPSEO_Shortlinker
*/
private $shortlinker;
/**
* The options helper.
*
* @var Options_Helper
*/
private $options_helper;
/**
* The prominent words helper.
*
* @var Prominent_Words_Helper
*/
private $prominent_words_helper;
/**
* The post type helper.
*
* @var Post_Type_Helper
*/
private $post_type_helper;
/**
* Workouts_Integration constructor.
*
* @param Indexable_Repository $indexable_repository The indexables repository.
* @param Link_Suggestions_Action $link_suggestions_action The link suggestions action.
* @param WPSEO_Admin_Asset_Manager $admin_asset_manager The admin asset manager.
* @param WPSEO_Shortlinker $shortlinker The shortlinker.
* @param Options_Helper $options_helper The options helper.
* @param Prominent_Words_Helper $prominent_words_helper The prominent words helper.
* @param Post_Type_Helper $post_type_helper The post type helper.
*/
public function __construct(
Indexable_Repository $indexable_repository,
Link_Suggestions_Action $link_suggestions_action,
WPSEO_Admin_Asset_Manager $admin_asset_manager,
WPSEO_Shortlinker $shortlinker,
Options_Helper $options_helper,
Prominent_Words_Helper $prominent_words_helper,
Post_Type_Helper $post_type_helper
) {
$this->indexable_repository = $indexable_repository;
$this->link_suggestions_action = $link_suggestions_action;
$this->admin_asset_manager = $admin_asset_manager;
$this->shortlinker = $shortlinker;
$this->options_helper = $options_helper;
$this->prominent_words_helper = $prominent_words_helper;
$this->post_type_helper = $post_type_helper;
}
/**
* {@inheritDoc}
*/
public function register_hooks() {
\add_filter( 'Yoast\WP\SEO\workouts_route_args', [ $this, 'add_args_to_set_workouts_route' ] );
\add_filter( 'Yoast\WP\SEO\workouts_route_save', [ $this, 'save_workouts_data' ], 10, 2 );
\add_filter( 'Yoast\WP\SEO\workouts_options', [ $this, 'get_options' ] );
}
/**
* Adds arguments to `set_workouts` route registration.
*
* @param array $args_array The existing array of arguments.
*
* @return array
*/
public function add_args_to_set_workouts_route( $args_array ) {
$premium_args_array = [
'cornerstone' => [
'validate_callback' => [ $this, 'cornerstone_is_allowed' ],
'required' => true,
],
'orphaned' => [
'validate_callback' => [ $this, 'orphaned_is_allowed' ],
'required' => true,
],
];
return \array_merge( $args_array, $premium_args_array );
}
/**
* Validates the cornerstone attribute.
*
* @param array $workout The cornerstone workout.
* @return bool If the payload is valid or not.
*/
public function cornerstone_is_allowed( $workout ) {
return $this->is_allowed( $workout, self::ALLOWED_CORNERSTONE_STEPS );
}
/**
* Validates the orphaned attribute.
*
* @param array $workout The orphaned workout.
* @return bool If the payload is valid or not.
*/
public function orphaned_is_allowed( $workout ) {
return $this->is_allowed( $workout, self::ALLOWED_ORPHANED_STEPS );
}
/**
* Validates a workout.
*
* @param array $workout The workout.
* @param array $allowed_steps The allowed steps for this workout.
* @return bool If the payload is valid or not.
*/
public function is_allowed( $workout, $allowed_steps ) {
// Only 3 properties are allowed, the below validated finishedSteps property.
if ( \count( $workout ) !== 3 ) {
return false;
}
if ( isset( $workout['finishedSteps'] ) && \is_array( $workout['finishedSteps'] ) ) {
foreach ( $workout['finishedSteps'] as $step ) {
if ( ! \in_array( $step, $allowed_steps, true ) ) {
return false;
}
}
return true;
}
return false;
}
/**
* Saves the Premium workouts data to the database.
*
* @param mixed|null $result The result of the previous save operations.
* @param array $workouts_data The complete workouts data.
*
* @return mixed|null
*/
public function save_workouts_data( $result, $workouts_data ) {
$premium_workouts_data = [];
$premium_workouts_data['cornerstone'] = $workouts_data['cornerstone'];
$premium_workouts_data['orphaned'] = $workouts_data['orphaned'];
foreach ( $premium_workouts_data as $workout => $data ) {
if ( isset( $data['indexablesByStep'] ) && \is_array( $data['indexablesByStep'] ) ) {
foreach ( $data['indexablesByStep'] as $step => $indexables ) {
if ( $step === 'removed' ) {
continue;
}
$premium_workouts_data[ $workout ]['indexablesByStep'][ $step ] = \wp_list_pluck( $indexables, 'id' );
}
}
}
return $this->options_helper->set( 'workouts', $premium_workouts_data );
}
/**
* Retrieves the Premium workouts options from the database and adds it to the global array of workouts options.
*
* @param array $workouts_option The previous content of the workouts options.
*
* @return array The workouts options updated with the addition of the Premium workouts data.
*/
public function get_options( $workouts_option ) {
$premium_option = $this->options_helper->get( 'workouts' );
if ( ! ( isset( $premium_option['orphaned']['indexablesByStep'] )
&& \is_array( $premium_option['orphaned']['indexablesByStep'] )
&& isset( $premium_option['cornerstone']['indexablesByStep'] )
&& \is_array( $premium_option['cornerstone']['indexablesByStep'] ) )
) {
return \array_merge( $workouts_option, $premium_option );
}
// Get all indexable ids from all workouts and all steps.
$indexable_ids_in_workouts = [ 0 ];
foreach ( [ 'orphaned', 'cornerstone' ] as $workout ) {
foreach ( $premium_option[ $workout ]['indexablesByStep'] as $step => $indexables ) {
if ( $step === 'removed' ) {
continue;
}
foreach ( $indexables as $indexable_id ) {
$indexable_ids_in_workouts[] = $indexable_id;
}
}
}
// Get all indexables corresponding to the indexable ids.
$indexables_in_workouts = $this->indexable_repository->find_by_ids( $indexable_ids_in_workouts );
// Extend the workouts option with the indexables data.
foreach ( [ 'orphaned', 'cornerstone' ] as $workout ) {
// Don't add indexables for steps that are not allowed.
$premium_option[ $workout ]['finishedSteps'] = \array_values(
\array_intersect(
$premium_option[ $workout ]['finishedSteps'],
[
'orphaned' => self::ALLOWED_ORPHANED_STEPS,
'cornerstone' => self::ALLOWED_CORNERSTONE_STEPS,
][ $workout ]
)
);
// Don't add indexables that are not published or are no-indexed.
foreach ( $premium_option[ $workout ]['indexablesByStep'] as $step => $indexables ) {
if ( $step === 'removed' ) {
continue;
}
$premium_option[ $workout ]['indexablesByStep'][ $step ] = \array_values(
\array_filter(
\array_map(
static function( $indexable_id ) use ( $indexables_in_workouts ) {
foreach ( $indexables_in_workouts as $updated_indexable ) {
if ( \is_array( $indexable_id ) ) {
$indexable_id = $indexable_id['id'];
}
if ( (int) $indexable_id === $updated_indexable->id ) {
if ( $updated_indexable->post_status !== 'publish' && $updated_indexable->post_status !== null ) {
return false;
}
if ( $updated_indexable->is_robots_noindex ) {
return false;
}
return $updated_indexable;
}
}
return false;
},
$indexables
)
)
);
}
}
return \array_merge( $workouts_option, $premium_option );
}
}

View File

@@ -0,0 +1,191 @@
<?php
namespace Yoast\WP\SEO\Premium\Integrations\Third_Party;
use WP_Post;
use WP_Term;
use WP_User;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Premium\Conditionals\Algolia_Enabled_Conditional;
use Yoast\WP\SEO\Surfaces\Meta_Surface;
use Yoast\WP\SEO\Surfaces\Values\Meta;
/**
* BbPress integration.
*/
class Algolia implements Integration_Interface {
/**
* The options helper.
*
* @var Options_Helper
*/
protected $options;
/**
* The meta helper.
*
* @var Meta_Surface
*/
protected $meta;
/**
* Algolia constructor.
*
* @codeCoverageIgnore It only sets dependencies.
*
* @param Options_Helper $options The options helper.
* @param Meta_Surface $meta The meta surface.
*/
public function __construct( Options_Helper $options, Meta_Surface $meta ) {
$this->options = $options;
$this->meta = $meta;
}
/**
* Returns the conditionals based in which this loadable should be active.
*
* @return array
*/
public static function get_conditionals() {
return [
Algolia_Enabled_Conditional::class,
];
}
/**
* Initializes the integration.
*
* This is the place to register hooks and filters.
*
* @return void
*/
public function register_hooks() {
\add_filter( 'algolia_searchable_post_shared_attributes', [ $this, 'add_attributes_post' ], 10, 2 );
\add_filter( 'algolia_term_record', [ $this, 'add_attributes_term' ] );
\add_filter( 'algolia_user_record', [ $this, 'add_attributes_user' ] );
\add_filter( 'algolia_should_index_searchable_post', [ $this, 'blacklist_no_index_posts' ], 10, 2 );
\add_filter( 'algolia_should_index_term', [ $this, 'blacklist_no_index_terms' ], 10, 2 );
\add_filter( 'algolia_should_index_user', [ $this, 'blacklist_no_index_users' ], 10, 2 );
}
/**
* Adds the search result priority and the number of internal links to an article to Algolia's index.
*
* @param array $attributes The attributes Algolia should index.
* @param WP_Post $post The post object that is being indexed.
*
* @return array The attributes Algolia should index.
*/
public function add_attributes_post( $attributes, $post ) {
$meta = $this->meta->for_post( $post->ID );
return $this->add_attributes( $attributes, $meta );
}
/**
* Adds the attributes for a term.
*
* @param array $attributes The recorded attributes.
*
* @return array The recorded attributes.
*/
public function add_attributes_term( $attributes ) {
$meta = $this->meta->for_term( $attributes['objectID'] );
return $this->add_attributes( $attributes, $meta );
}
/**
* Adds the attributes for a term.
*
* @param array $attributes The recorded attributes.
*
* @return array The recorded attributes.
*/
public function add_attributes_user( $attributes ) {
$meta = $this->meta->for_author( $attributes['objectID'] );
return $this->add_attributes( $attributes, $meta );
}
/**
* Adds the attributes for a searchable object.
*
* @param array $attributes Attributes to update.
* @param Meta $meta Meta value object for the current object.
*
* @return array Attributes for the searchable object.
*/
private function add_attributes( array $attributes, Meta $meta ) {
$attributes['yoast_seo_links'] = (int) $meta->indexable->incoming_link_count;
$attributes['yoast_seo_metadesc'] = $meta->meta_description;
return $this->add_social_image( $attributes, $meta->open_graph_images );
}
/**
* Adds the social image to an attributes array if we have one.
*
* @param array $attributes The array of search attributes for a record.
* @param array $og_images The social images for the current item.
*
* @return array The array of search attributes for a record.
*/
private function add_social_image( $attributes, $og_images ) {
if ( \is_array( $og_images ) && \count( $og_images ) > 0 ) {
$attributes['images']['social'] = \reset( $og_images );
}
return $attributes;
}
/**
* Checks whether a post should be indexed, taking the Yoast SEO no-index state into account.
*
* @param bool $should_index Whether Algolia should index the post or not.
* @param WP_Post $post The post object.
*
* @return bool Whether Algolia should index the post or not.
*/
public function blacklist_no_index_posts( $should_index, $post ) {
if ( $this->meta->for_post( $post->ID )->robots['index'] === 'noindex' ) {
return false;
}
return $should_index;
}
/**
* Checks whether a term should be indexed, taking the Yoast SEO no-index state into account.
*
* @param bool $should_index Whether Algolia should index the term or not.
* @param WP_Term $term The term object.
*
* @return bool Whether Algolia should index the term or not.
*/
public function blacklist_no_index_terms( $should_index, $term ) {
if ( $this->meta->for_term( $term->term_id )->robots['index'] === 'noindex' ) {
return false;
}
return $should_index;
}
/**
* Checks whether a user should be indexed, taking the Yoast SEO no-index state into account.
*
* @param bool $should_index Whether Algolia should index the user or not.
* @param WP_User $user The user object.
*
* @return bool Whether Algolia should index the user or not.
*/
public function blacklist_no_index_users( $should_index, $user ) {
if ( $this->meta->for_author( $user->ID )->robots['index'] === 'noindex' ) {
return false;
}
return $should_index;
}
}

View File

@@ -0,0 +1,135 @@
<?php
namespace Yoast\WP\SEO\Premium\Integrations\Third_Party;
use WPSEO_Schema_Context;
use Yoast\WP\SEO\Conditionals\Front_End_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Premium\Conditionals\EDD_Conditional;
use Yoast\WP\SEO\Surfaces\Meta_Surface;
/**
* EDD integration.
*/
class EDD implements Integration_Interface {
/**
* The meta surface.
*
* @var Meta_Surface
*/
private $meta;
/**
* Returns the conditionals based on which this loadable should be active.
*
* @return array
*/
public static function get_conditionals() {
return [ Front_End_Conditional::class, EDD_Conditional::class ];
}
/**
* EDD constructor.
*
* @codeCoverageIgnore It only sets dependencies.
*
* @param Meta_Surface $meta The meta surface.
*/
public function __construct( Meta_Surface $meta ) {
$this->meta = $meta;
}
/**
* Initializes the integration.
*
* This is the place to register hooks and filters.
*
* @return void
*/
public function register_hooks() {
\add_filter( 'edd_generate_download_structured_data', [ $this, 'filter_download_schema' ] );
\add_filter( 'wpseo_schema_organization', [ $this, 'filter_organization_schema' ] );
\add_filter( 'wpseo_schema_webpage', [ $this, 'filter_webpage_schema' ], 10, 2 );
}
/**
* Make sure the Organization is classified as a Brand too.
*
* @param array $data The organization schema.
*
* @return array
*/
public function filter_organization_schema( $data ) {
if ( \is_singular( 'download' ) ) {
$data['@type'] = [ 'Organization', 'Brand' ];
}
return $data;
}
/**
* Make sure the WebPage schema contains reference to the product.
*
* @param array $data The schema Webpage data.
* @param WPSEO_Schema_Context $context Context object.
*
* @return array
*/
public function filter_webpage_schema( $data, $context ) {
if ( \is_singular( [ 'download' ] ) ) {
$data['about'] = [ '@id' => $context->canonical . '#/schema/edd-product/' . \get_the_ID() ];
$data['mainEntity'] = [ '@id' => $context->canonical . '#/schema/edd-product/' . \get_the_ID() ];
}
return $data;
}
/**
* Filter the structured data output for a download to tie into Yoast SEO's output.
*
* @param array $data Structured data for a download.
*
* @return array
*/
public function filter_download_schema( $data ) {
$data['@id'] = $this->meta->for_current_page()->canonical . '#/schema/edd-product/' . \get_the_ID();
$data['sku'] = (string) $data['sku'];
$data['brand'] = $this->return_organization_node();
$data['offers'] = $this->clean_up_offer( $data['offers'] );
if ( ! isset( $data['description'] ) ) {
$data['description'] = $this->meta->for_current_page()->open_graph_description;
}
return $data;
}
/**
* Cleans up EDD generated Offers.
*
* @param array $offer The schema array.
*
* @return array
*/
private function clean_up_offer( $offer ) {
if ( \array_key_exists( 'priceValidUntil', $offer ) && $offer['priceValidUntil'] === null ) {
unset( $offer['priceValidUntil'] );
}
$offer['seller'] = $this->return_organization_node();
return $offer;
}
/**
* Returns a Schema node for the current site's Organization.
*
* @return string[]
*/
private function return_organization_node() {
return [
'@type' => [ 'Organization', 'Brand' ],
'@id' => $this->meta->for_home_page()->canonical . '#organization',
];
}
}

View File

@@ -1,9 +1,9 @@
<?php
namespace Yoast\WP\SEO\Integrations\Third_Party;
namespace Yoast\WP\SEO\Premium\Integrations\Third_Party;
use WP_Post;
use WPSEO_Admin_Asset_Yoast_Components_L10n;
use WPSEO_Admin_Asset_Manager;
use WPSEO_Capability_Utils;
use WPSEO_Custom_Fields_Plugin;
use WPSEO_Language_Utils;
@@ -18,9 +18,11 @@ use WPSEO_Premium_Prominent_Words_Support;
use WPSEO_Social_Previews;
use WPSEO_Utils;
use Yoast\WP\SEO\Conditionals\Third_Party\Elementor_Edit_Conditional;
use Yoast\WP\SEO\Helpers\Prominent_Words_Helper;
use Yoast\WP\SEO\Integrations\Admin\Prominent_Words\Indexing_Integration;
use Yoast\WP\SEO\Integrations\Integration_Interface;
use Yoast\WP\SEO\Premium\Helpers\Current_Page_Helper;
use Yoast\WP\SEO\Premium\Helpers\Prominent_Words_Helper;
use Yoast\WP\SEO\Premium\Integrations\Admin\Prominent_Words\Indexing_Integration;
use Yoast\WP\SEO\Premium\Integrations\Admin\Replacement_Variables_Integration;
/**
* Elementor integration class for Yoast SEO Premium.
@@ -34,6 +36,13 @@ class Elementor_Premium implements Integration_Interface {
*/
const SCRIPT_HANDLE = 'elementor-premium';
/**
* Holds the Current_Page_Helper.
*
* @var Current_Page_Helper
*/
protected $current_page_helper;
/**
* Represents the post.
*
@@ -68,10 +77,12 @@ class Elementor_Premium implements Integration_Interface {
* Constructs the class.
*
* @param Prominent_Words_Helper $prominent_words_helper The prominent words helper.
* @param Current_Page_Helper $current_page_helper The Current_Page_Helper.
*/
public function __construct( Prominent_Words_Helper $prominent_words_helper ) {
public function __construct( Prominent_Words_Helper $prominent_words_helper, Current_Page_Helper $current_page_helper ) {
$this->prominent_words_helper = $prominent_words_helper;
$this->post_watcher = new WPSEO_Post_Watcher();
$this->current_page_helper = $current_page_helper;
}
/**
@@ -112,6 +123,9 @@ class Elementor_Premium implements Integration_Interface {
$social_previews->enqueue_assets();
$custom_fields = new WPSEO_Custom_Fields_Plugin();
$custom_fields->enqueue();
$replacement_variables = new Replacement_Variables_Integration();
$replacement_variables->enqueue_assets();
}
// Below is mostly copied from `premium-metabox.php`.
@@ -127,9 +141,6 @@ class Elementor_Premium implements Integration_Interface {
\wp_enqueue_script( static::SCRIPT_HANDLE );
\wp_enqueue_style( static::SCRIPT_HANDLE );
$localization = new WPSEO_Admin_Asset_Yoast_Components_L10n();
$localization->localize_script( static::SCRIPT_HANDLE );
$premium_localization = new WPSEO_Premium_Asset_JS_L10n();
$premium_localization->localize_script( static::SCRIPT_HANDLE );
@@ -142,17 +153,37 @@ class Elementor_Premium implements Integration_Interface {
* @return void
*/
public function send_data_to_assets() {
$analysis_seo = new WPSEO_Metabox_Analysis_SEO();
$analysis_seo = new WPSEO_Metabox_Analysis_SEO();
$assets_manager = new WPSEO_Admin_Asset_Manager();
$data = [
'restApi' => $this->get_rest_api_config(),
'seoAnalysisEnabled' => $analysis_seo->is_enabled(),
'licensedURL' => WPSEO_Utils::get_home_url(),
'settingsPageUrl' => \admin_url( 'admin.php?page=wpseo_dashboard#top#features' ),
'integrationsTabURL' => \admin_url( 'admin.php?page=wpseo_dashboard#top#integrations' ),
'restApi' => $this->get_rest_api_config(),
'seoAnalysisEnabled' => $analysis_seo->is_enabled(),
'licensedURL' => WPSEO_Utils::get_home_url(),
'settingsPageUrl' => \admin_url( 'admin.php?page=wpseo_page_settings#/site-features#card-wpseo-enable_link_suggestions' ),
'integrationsTabURL' => \admin_url( 'admin.php?page=wpseo_integrations' ),
'commonsScriptUrl' => \plugins_url(
'assets/js/dist/commons-premium-' . $assets_manager->flatten_version( \WPSEO_PREMIUM_VERSION ) . \WPSEO_CSSJS_SUFFIX . '.js',
\WPSEO_PREMIUM_FILE
),
'premiumAssessmentsScriptUrl' => \plugins_url(
'assets/js/dist/register-premium-assessments-' . $assets_manager->flatten_version( \WPSEO_PREMIUM_VERSION ) . \WPSEO_CSSJS_SUFFIX . '.js',
\WPSEO_PREMIUM_FILE
),
'pluginUrl' => \plugins_url( '', \WPSEO_PREMIUM_FILE ),
];
if ( \defined( 'YOAST_SEO_TEXT_FORMALITY' ) && \YOAST_SEO_TEXT_FORMALITY === true ) {
$data['textFormalityScriptUrl'] = \plugins_url(
'assets/js/dist/register-text-formality-' . $assets_manager->flatten_version( \WPSEO_PREMIUM_VERSION ) . \WPSEO_CSSJS_SUFFIX . '.js',
\WPSEO_PREMIUM_FILE
);
}
$data = \array_merge( $data, $this->get_post_metabox_config() );
if ( \current_user_can( 'edit_others_posts' ) ) {
$data['workoutsUrl'] = \admin_url( 'admin.php?page=wpseo_workouts' );
}
// Use an extra level in the array to preserve booleans. WordPress sanitizes scalar values in the first level of the array.
\wp_localize_script( static::SCRIPT_HANDLE, 'wpseoPremiumMetaboxData', [ 'data' => $data ] );
}
@@ -163,25 +194,23 @@ class Elementor_Premium implements Integration_Interface {
* @return array The config.
*/
protected function get_post_metabox_config() {
$insights_enabled = WPSEO_Options::get( 'enable_metabox_insights', false );
$link_suggestions_enabled = WPSEO_Options::get( 'enable_link_suggestions', false );
$prominent_words_support = new WPSEO_Premium_Prominent_Words_Support();
if ( ! $prominent_words_support->is_post_type_supported( $this->get_metabox_post()->post_type ) ) {
$insights_enabled = false;
}
$prominent_words_support = new WPSEO_Premium_Prominent_Words_Support();
$is_prominent_words_available = $prominent_words_support->is_post_type_supported( $this->get_metabox_post()->post_type );
$site_locale = \get_locale();
$language = WPSEO_Language_Utils::get_language( $site_locale );
return [
'insightsEnabled' => ( $insights_enabled ) ? 'enabled' : 'disabled',
'currentObjectId' => $this->get_metabox_post()->ID,
'currentObjectType' => 'post',
'linkSuggestionsEnabled' => ( $link_suggestions_enabled ) ? 'enabled' : 'disabled',
'linkSuggestionsAvailable' => $prominent_words_support->is_post_type_supported( $this->get_metabox_post()->post_type ),
'linkSuggestionsUnindexed' => ! $this->is_prominent_words_indexing_completed() && WPSEO_Capability_Utils::current_user_can( 'wpseo_manage_options' ),
'perIndexableLimit' => $this->per_indexable_limit( $language ),
'currentObjectId' => $this->get_metabox_post()->ID,
'currentObjectType' => 'post',
'linkSuggestionsEnabled' => ( $link_suggestions_enabled ) ? 'enabled' : 'disabled',
'linkSuggestionsAvailable' => $is_prominent_words_available,
'linkSuggestionsUnindexed' => ! $this->is_prominent_words_indexing_completed() && WPSEO_Capability_Utils::current_user_can( 'wpseo_manage_options' ),
'perIndexableLimit' => $this->per_indexable_limit( $language ),
'isProminentWordsAvailable' => $is_prominent_words_available,
];
}
@@ -224,9 +253,9 @@ class Elementor_Premium implements Integration_Interface {
return $this->post;
}
$post = \filter_input( \INPUT_GET, 'post' );
if ( ! empty( $post ) ) {
$post_id = (int) WPSEO_Utils::validate_int( $post );
$post_id = $this->current_page_helper->get_current_post_id();
if ( $post_id ) {
$this->post = \get_post( $post_id );
@@ -250,39 +279,13 @@ class Elementor_Premium implements Integration_Interface {
protected function load_metabox() {
// When the current page isn't a post related one.
if ( WPSEO_Metabox::is_post_edit( $this->get_current_page() ) ) {
return WPSEO_Post_Type::has_metabox_enabled( $this->get_current_post_type() );
return WPSEO_Post_Type::has_metabox_enabled( $this->current_page_helper->get_current_post_type() );
}
// Make sure ajax integrations are loaded.
return \wp_doing_ajax();
}
/**
* Retrieves the current post type.
*
* @codeCoverageIgnore It depends on external request input.
*
* @return string The post type.
*/
protected function get_current_post_type() {
$post = \filter_input( \INPUT_GET, 'post', \FILTER_SANITIZE_STRING );
if ( $post ) {
return \get_post_type( \get_post( $post ) );
}
return \filter_input(
\INPUT_GET,
'post_type',
\FILTER_SANITIZE_STRING,
[
'options' => [
'default' => 'post',
],
]
);
}
/**
* Retrieves the value of the pagenow variable.
*

View File

@@ -0,0 +1,175 @@
<?php
namespace Yoast\WP\SEO\Premium\Integrations\Third_Party;
use Yoast\WP\SEO\Conditionals\No_Conditionals;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Social_Profiles_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
/**
* Mastodon integration.
*/
class Mastodon implements Integration_Interface {
use No_Conditionals;
/**
* Holds the options helper.
*
* @var Options_Helper
*/
protected $options_helper;
/**
* Holds the social profiles helper.
*
* @var Social_Profiles_Helper
*/
protected $social_profiles_helper;
/**
* Sets the helpers.
*
* @param Options_Helper $options_helper Options helper.
* @param Social_Profiles_Helper $social_profiles_helper Social Profiles helper.
*/
public function __construct( Options_Helper $options_helper, Social_Profiles_Helper $social_profiles_helper ) {
$this->options_helper = $options_helper;
$this->social_profiles_helper = $social_profiles_helper;
}
/**
* Initializes the integration.
*
* This is the place to register hooks and filters.
*
* @return void
*/
public function register_hooks() {
\add_filter( 'wpseo_frontend_presenter_classes', [ $this, 'add_social_link_tags' ], 10, 2 );
\add_filter( 'wpseo_person_social_profile_fields', [ $this, 'add_mastodon_to_person_social_profile_fields' ], 11, 1 );
\add_filter( 'wpseo_organization_social_profile_fields', [ $this, 'add_mastodon_to_organization_social_profile_fields' ], 11, 1 );
\add_filter( 'wpseo_schema_person_social_profiles', [ $this, 'add_mastodon_to_person_schema' ], 10 );
\add_filter( 'user_contactmethods', [ $this, 'add_mastodon_to_user_contactmethods' ], 10 );
\add_filter( 'wpseo_mastodon_active', [ $this, 'check_mastodon_active' ], 10 );
}
/**
* Adds the social profiles presenter to the list of presenters to use.
*
* @param array $presenters The list of presenters.
* @param string $page_type The page type for which the presenters have been collected.
*
* @return array
*/
public function add_social_link_tags( $presenters, $page_type ) {
// Bail out early if something's wrong with the presenters, let's not add any more confusion there.
if ( ! \is_array( $presenters ) ) {
return $presenters;
}
if ( \in_array( $page_type, [ 'Static_Home_Page', 'Home_Page' ], true ) ) {
$presenters = \array_merge( $presenters, [ 'Yoast\WP\SEO\Premium\Presenters\Mastodon_Link_Presenter' ] );
}
return $presenters;
}
/**
* Adds Mastodon to the list of social profiles.
*
* @param array $social_profile_fields The social profiles array.
*
* @return array The updated social profiles array.
*/
public function add_mastodon_to_person_social_profile_fields( $social_profile_fields ) {
// Bail out early if something's wrong with the social profiles, let's not add any more confusion there.
if ( ! \is_array( $social_profile_fields ) ) {
return $social_profile_fields;
}
$social_profile_fields['mastodon'] = 'get_non_valid_url';
return $social_profile_fields;
}
/**
* Adds Mastodon to the list of social profiles.
*
* @param array $social_profile_fields The social profiles array.
*
* @return array The updated social profiles array.
*/
public function add_mastodon_to_organization_social_profile_fields( $social_profile_fields ) {
// Bail out early if something's wrong with the social profiles, let's not add any more confusion there.
if ( ! \is_array( $social_profile_fields ) ) {
return $social_profile_fields;
}
$social_profile_fields['mastodon_url'] = 'get_non_valid_url';
return $social_profile_fields;
}
/**
* Adds Mastodon to the list of social profiles to add to a Person's Schema.
*
* @param array $social_profiles The social profiles array.
*
* @return array
*/
public function add_mastodon_to_person_schema( $social_profiles ) {
// Bail out early if something's wrong with the social profiles, let's not add any more confusion there.
if ( ! \is_array( $social_profiles ) ) {
return $social_profiles;
}
$social_profiles[] = 'mastodon';
return $social_profiles;
}
/**
* Adds Mastodon to the list of contact methods for persons.
*
* @param array $contactmethods Currently set contactmethods.
*
* @return array
*/
public function add_mastodon_to_user_contactmethods( $contactmethods ) {
// Bail out early if something's wrong with the contact methods, let's not add any more confusion there.
if ( ! \is_array( $contactmethods ) ) {
return $contactmethods;
}
$contactmethods['mastodon'] = \__( 'Mastodon profile URL', 'wordpress-seo-premium' );
return $contactmethods;
}
/**
* Checks if the Mastodon field is filled in.
*
* @param bool $state The current state of the integration.
*
* @return bool
*/
public function check_mastodon_active( $state ) {
switch ( $this->options_helper->get( 'company_or_person', false ) ) {
case 'company':
$social_profiles = $this->social_profiles_helper->get_organization_social_profiles();
if ( ! empty( $social_profiles['mastodon_url'] ) ) {
return true;
}
break;
case 'person':
$company_or_person_id = $this->options_helper->get( 'company_or_person_user_id', 0 );
$social_profiles = $this->social_profiles_helper->get_person_social_profiles( $company_or_person_id );
if ( ! empty( $social_profiles['mastodon'] ) ) {
return true;
}
break;
}
return $state;
}
}

View File

@@ -1,6 +1,9 @@
<?php
namespace Yoast\WP\SEO\Integrations\Third_Party;
use DateTime;
use stdClass;
use Yoast\WP\SEO\Conditionals\No_Conditionals;
use Yoast\WP\SEO\Helpers\Date_Helper;
use Yoast\WP\SEO\Integrations\Integration_Interface;
@@ -33,6 +36,13 @@ class TranslationsPress implements Integration_Interface {
*/
protected $api_url;
/**
* The array to cache our addition to the `site_transient_update_plugins` filter.
*
* @var array|null
*/
protected $cached_translations;
/**
* The Date helper object.
*
@@ -83,6 +93,7 @@ class TranslationsPress implements Integration_Interface {
/**
* Filters the translations transients to include the private plugin or theme.
* Caches our own return value to prevent heavy overhead.
*
* @param bool|object $value The transient value.
*
@@ -90,18 +101,26 @@ class TranslationsPress implements Integration_Interface {
*/
public function site_transient_update_plugins( $value ) {
if ( ! $value ) {
$value = new \stdClass();
$value = new stdClass();
}
if ( ! isset( $value->translations ) ) {
$value->translations = [];
}
if ( \is_array( $this->cached_translations ) ) {
$value->translations = \array_merge( $value->translations, $this->cached_translations );
return $value;
}
$this->cached_translations = [];
$translations = $this->get_translations();
if ( empty( $translations[ $this->slug ]['translations'] ) ) {
return $value;
}
// The following call is the reason we need to cache the results of this method.
$installed_translations = \wp_get_installed_translations( 'plugins' );
$available_languages = \get_available_languages();
foreach ( $translations[ $this->slug ]['translations'] as $translation ) {
@@ -110,18 +129,19 @@ class TranslationsPress implements Integration_Interface {
}
if ( isset( $installed_translations[ $this->slug ][ $translation['language'] ] ) && $translation['updated'] ) {
$local = new \DateTime( $installed_translations[ $this->slug ][ $translation['language'] ]['PO-Revision-Date'] );
$remote = new \DateTime( $translation['updated'] );
$local = new DateTime( $installed_translations[ $this->slug ][ $translation['language'] ]['PO-Revision-Date'] );
$remote = new DateTime( $translation['updated'] );
if ( $local >= $remote ) {
continue;
}
}
$translation['type'] = 'plugin';
$translation['slug'] = $this->slug;
$translation['autoupdate'] = true;
$value->translations[] = $translation;
$translation['type'] = 'plugin';
$translation['slug'] = $this->slug;
$translation['autoupdate'] = true;
$value->translations[] = $translation;
$this->cached_translations[] = $translation;
}
return $value;

View File

@@ -0,0 +1,82 @@
<?php
// phpcs:disable Yoast.NamingConventions.NamespaceName.Invalid
// phpcs:disable Yoast.NamingConventions.NamespaceName.MaxExceeded
namespace Yoast\WP\SEO\Integrations\Third_Party;
use WPSEO_Meta;
use Yoast\WP\SEO\Conditionals\Wincher_Enabled_Conditional;
use Yoast\WP\SEO\Integrations\Integration_Interface;
/**
* Enhances the Wincher keyphrases arrays.
*/
class Wincher_Keyphrases implements Integration_Interface {
/**
* Returns the conditionals based in which this loadable should be active.
*
* @return array
*/
public static function get_conditionals() {
return [ Wincher_Enabled_Conditional::class ];
}
/**
* Initializes the integration.
*
* This is the place to register hooks and filters.
*
* @return void
*/
public function register_hooks() {
\add_filter( 'wpseo_wincher_keyphrases_from_post', [ $this, 'add_additional_keyphrases_from_post' ], 10, 2 );
\add_filter( 'wpseo_wincher_all_keyphrases', [ $this, 'add_all_additional_keyphrases' ] );
}
/**
* Enhances the keyphrases collected from a post with the additional ones.
*
* @param array $keyphrases The keyphrases array.
* @param int $post_id The ID of the post.
*
* @return array The enhanced array.
*/
public function add_additional_keyphrases_from_post( $keyphrases, $post_id ) {
$additional_keywords = \json_decode( WPSEO_Meta::get_value( 'focuskeywords', $post_id ), true );
return \array_merge( $keyphrases, $additional_keywords );
}
/**
* Enhances the keyphrases collected from all the posts with the additional ones.
*
* @param array $keyphrases The keyphrases array.
*
* @return array The enhanced array.
*/
public function add_all_additional_keyphrases( $keyphrases ) {
global $wpdb;
$meta_key = WPSEO_Meta::$meta_prefix . 'focuskeywords';
$query = "
SELECT meta_value
FROM $wpdb->postmeta
JOIN $wpdb->posts ON {$wpdb->posts}.id = {$wpdb->postmeta}.post_id
WHERE meta_key = '$meta_key' AND post_status != 'trash'
";
// phpcs:ignore -- ignoring since it's complaining about not using prepare when it's perfectly safe here.
$results = $wpdb->get_results( $query );
if ( $results ) {
foreach ( $results as $row ) {
$additional_keywords = \json_decode( $row->meta_value, true );
if ( $additional_keywords !== null ) {
$additional_keywords = \array_column( $additional_keywords, 'keyword' );
$keyphrases = \array_merge( $keyphrases, $additional_keywords );
}
}
}
return $keyphrases;
}
}

Some files were not shown because too many files have changed in this diff Show More