Phase 5: Content and SEO - Yoast SEO, Schema.org markup, Open Graph, favicon support, XML sitemap
This commit is contained in:
+176
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\AI_Generator\Application;
|
||||
|
||||
use RuntimeException;
|
||||
use WP_User;
|
||||
use Yoast\WP\SEO\AI_Authorization\Application\Token_Manager;
|
||||
use Yoast\WP\SEO\AI_Consent\Application\Consent_Handler;
|
||||
use Yoast\WP\SEO\AI_Generator\Domain\Suggestion;
|
||||
use Yoast\WP\SEO\AI_Generator\Domain\Suggestions_Bucket;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Application\Request_Handler;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Bad_Request_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Forbidden_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Internal_Server_Error_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Not_Found_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Payment_Required_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Request_Timeout_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Service_Unavailable_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Too_Many_Requests_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Unauthorized_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Request;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Response;
|
||||
use Yoast\WP\SEO\Helpers\User_Helper;
|
||||
|
||||
/**
|
||||
* The class that handles the suggestions from the AI API.
|
||||
*/
|
||||
class Suggestions_Provider {
|
||||
|
||||
/**
|
||||
* The consent handler instance.
|
||||
*
|
||||
* @var Consent_Handler
|
||||
*/
|
||||
private $consent_handler;
|
||||
|
||||
/**
|
||||
* The request handler instance.
|
||||
*
|
||||
* @var Request_Handler
|
||||
*/
|
||||
private $request_handler;
|
||||
|
||||
/**
|
||||
* The token manager instance.
|
||||
*
|
||||
* @var Token_Manager
|
||||
*/
|
||||
private $token_manager;
|
||||
|
||||
/**
|
||||
* The user helper instance.
|
||||
*
|
||||
* @var User_Helper
|
||||
*/
|
||||
private $user_helper;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param Consent_Handler $consent_handler The consent handler instance.
|
||||
* @param Request_Handler $request_handler The request handler instance.
|
||||
* @param Token_Manager $token_manager The token manager instance.
|
||||
* @param User_Helper $user_helper The user helper instance.
|
||||
*/
|
||||
public function __construct(
|
||||
Consent_Handler $consent_handler,
|
||||
Request_Handler $request_handler,
|
||||
Token_Manager $token_manager,
|
||||
User_Helper $user_helper
|
||||
) {
|
||||
$this->consent_handler = $consent_handler;
|
||||
$this->request_handler = $request_handler;
|
||||
$this->token_manager = $token_manager;
|
||||
$this->user_helper = $user_helper;
|
||||
}
|
||||
|
||||
// phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber -- PHPCS doesn't take into account exceptions thrown in called methods.
|
||||
|
||||
/**
|
||||
* Method 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 string $editor The current editor.
|
||||
* @param bool $retry_on_unauthorized Whether to retry when unauthorized (mechanism to retry once).
|
||||
*
|
||||
* @throws Bad_Request_Exception Bad_Request_Exception.
|
||||
* @throws Forbidden_Exception Forbidden_Exception.
|
||||
* @throws Internal_Server_Error_Exception Internal_Server_Error_Exception.
|
||||
* @throws Not_Found_Exception Not_Found_Exception.
|
||||
* @throws Payment_Required_Exception Payment_Required_Exception.
|
||||
* @throws Request_Timeout_Exception Request_Timeout_Exception.
|
||||
* @throws Service_Unavailable_Exception Service_Unavailable_Exception.
|
||||
* @throws Too_Many_Requests_Exception Too_Many_Requests_Exception.
|
||||
* @throws Unauthorized_Exception Unauthorized_Exception.
|
||||
* @throws RuntimeException Unable to retrieve the access token.
|
||||
* @return string[] The suggestions.
|
||||
*/
|
||||
public function get_suggestions(
|
||||
WP_User $user,
|
||||
string $suggestion_type,
|
||||
string $prompt_content,
|
||||
string $focus_keyphrase,
|
||||
string $language,
|
||||
string $platform,
|
||||
string $editor,
|
||||
bool $retry_on_unauthorized = true
|
||||
): array {
|
||||
$token = $this->token_manager->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",
|
||||
'X-Yst-Cohort' => $editor,
|
||||
];
|
||||
|
||||
try {
|
||||
$response = $this->request_handler->handle( new 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, $editor, false );
|
||||
} catch ( Forbidden_Exception $exception ) {
|
||||
// Follow the API in the consent being revoked (Use case: user sent an e-mail to revoke?).
|
||||
// phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- false positive.
|
||||
$this->consent_handler->revoke_consent( $user->ID );
|
||||
throw new Forbidden_Exception( 'CONSENT_REVOKED', $exception->getCode() );
|
||||
// phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
}
|
||||
|
||||
return $this->build_suggestions_array( $response )->to_array();
|
||||
}
|
||||
|
||||
// phpcs:enable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber
|
||||
|
||||
/**
|
||||
* Generates the list of 5 suggestions to return.
|
||||
*
|
||||
* @param Response $response The response from the API.
|
||||
*
|
||||
* @return Suggestions_Bucket The array of suggestions.
|
||||
*/
|
||||
public function build_suggestions_array( Response $response ): Suggestions_Bucket {
|
||||
$suggestions_bucket = new Suggestions_Bucket();
|
||||
$json = \json_decode( $response->get_body() );
|
||||
if ( $json === null || ! isset( $json->choices ) ) {
|
||||
return $suggestions_bucket;
|
||||
}
|
||||
foreach ( $json->choices as $suggestion ) {
|
||||
$suggestions_bucket->add_suggestion( new Suggestion( $suggestion->text ) );
|
||||
}
|
||||
|
||||
return $suggestions_bucket;
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
|
||||
namespace Yoast\WP\SEO\AI_Generator\Domain\Endpoint;
|
||||
|
||||
interface Endpoint_Interface {
|
||||
|
||||
/**
|
||||
* Gets the name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_name(): string;
|
||||
|
||||
/**
|
||||
* Gets the namespace.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_namespace(): string;
|
||||
|
||||
/**
|
||||
* Gets the route.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_route(): string;
|
||||
|
||||
/**
|
||||
* Gets the URL.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_url(): string;
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
|
||||
namespace Yoast\WP\SEO\AI_Generator\Domain\Endpoint;
|
||||
|
||||
/**
|
||||
* List of endpoints.
|
||||
*/
|
||||
class Endpoint_List {
|
||||
|
||||
/**
|
||||
* Holds the endpoints.
|
||||
*
|
||||
* @var array<Endpoint_Interface>
|
||||
*/
|
||||
private $endpoints = [];
|
||||
|
||||
/**
|
||||
* Adds an endpoint to the list.
|
||||
*
|
||||
* @param Endpoint_Interface $endpoint An endpoint.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function add_endpoint( Endpoint_Interface $endpoint ): void {
|
||||
$this->endpoints[] = $endpoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the list to an array.
|
||||
*
|
||||
* @return array<string, string> The array of endpoints.
|
||||
*/
|
||||
public function to_array(): array {
|
||||
$result = [];
|
||||
foreach ( $this->endpoints as $endpoint ) {
|
||||
$result[ $endpoint->get_name() ] = $endpoint->get_url();
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\AI_Generator\Domain;
|
||||
|
||||
/**
|
||||
* Class Suggestion
|
||||
* Represents a suggestion from the AI Generator API.
|
||||
*/
|
||||
class Suggestion {
|
||||
|
||||
/**
|
||||
* The suggestion text.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $value;
|
||||
|
||||
/**
|
||||
* The constructor.
|
||||
*
|
||||
* @param string $value The suggestion text.
|
||||
*/
|
||||
public function __construct( string $value ) {
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the suggestion text.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_value(): string {
|
||||
return $this->value;
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\AI_Generator\Domain;
|
||||
|
||||
/**
|
||||
* Class Suggestion_Bucket
|
||||
* Represents a collection of Suggestion objects.
|
||||
*/
|
||||
class Suggestions_Bucket {
|
||||
|
||||
/**
|
||||
* The suggestions.
|
||||
*
|
||||
* @var array<Suggestion>
|
||||
*/
|
||||
private $suggestions;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->suggestions = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a suggestion to the bucket.
|
||||
*
|
||||
* @param Suggestion $suggestion The suggestion to add.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function add_suggestion( Suggestion $suggestion ) {
|
||||
$this->suggestions[] = $suggestion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the suggestions as an array.
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
public function to_array() {
|
||||
return \array_map(
|
||||
static function ( $item ) {
|
||||
return $item->get_value();
|
||||
},
|
||||
$this->suggestions
|
||||
);
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\AI_Generator\Domain;
|
||||
|
||||
/**
|
||||
* Helper class to get the URLs needed for the AI Generator API.
|
||||
*/
|
||||
interface URLs_Interface {
|
||||
|
||||
/**
|
||||
* Gets the licence URL.
|
||||
*
|
||||
* @return string The license URL.
|
||||
*/
|
||||
public function get_license_url(): string;
|
||||
|
||||
/**
|
||||
* Gets the callback URL to be used by the API to send back the access token, refresh token and code challenge.
|
||||
*
|
||||
* @return string The callback URL.
|
||||
*/
|
||||
public function get_callback_url(): string;
|
||||
|
||||
/**
|
||||
* Gets the callback URL to be used by the API to send back the refreshed JWTs once they expire.
|
||||
*
|
||||
* @return string The refresh callback URL.
|
||||
*/
|
||||
public function get_refresh_callback_url(): string;
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
|
||||
namespace Yoast\WP\SEO\AI_Generator\Infrastructure\Endpoints;
|
||||
|
||||
use Exception;
|
||||
use Yoast\WP\SEO\AI_Generator\Domain\Endpoint\Endpoint_Interface;
|
||||
use Yoast\WP\SEO\AI_Generator\User_Interface\Get_Suggestions_Route;
|
||||
|
||||
/**
|
||||
* Represents the setup steps tracking endpoint.
|
||||
*/
|
||||
class Get_Suggestions_Endpoint implements Endpoint_Interface {
|
||||
|
||||
/**
|
||||
* Gets the name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_name(): string {
|
||||
return 'getSuggestions';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the namespace.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_namespace(): string {
|
||||
return Get_Suggestions_Route::ROUTE_NAMESPACE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the route.
|
||||
*
|
||||
* @throws Exception If the route prefix is not overwritten this throws.
|
||||
* @return string
|
||||
*/
|
||||
public function get_route(): string {
|
||||
return Get_Suggestions_Route::ROUTE_PREFIX;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the URL.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_url(): string {
|
||||
return \rest_url( $this->get_namespace() . $this->get_route() );
|
||||
}
|
||||
}
|
||||
Executable
+50
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
|
||||
namespace Yoast\WP\SEO\AI_Generator\Infrastructure\Endpoints;
|
||||
|
||||
use Exception;
|
||||
use Yoast\WP\SEO\AI_Generator\Domain\Endpoint\Endpoint_Interface;
|
||||
use Yoast\WP\SEO\AI_Generator\User_Interface\Get_Usage_Route;
|
||||
|
||||
/**
|
||||
* Represents the setup steps tracking endpoint.
|
||||
*/
|
||||
class Get_Usage_Endpoint implements Endpoint_Interface {
|
||||
|
||||
/**
|
||||
* Gets the name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_name(): string {
|
||||
return 'getUsage';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the namespace.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_namespace(): string {
|
||||
return Get_Usage_Route::ROUTE_NAMESPACE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the route.
|
||||
*
|
||||
* @throws Exception If the route prefix is not overwritten this throws.
|
||||
* @return string
|
||||
*/
|
||||
public function get_route(): string {
|
||||
return Get_Usage_Route::ROUTE_PREFIX;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the URL.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_url(): string {
|
||||
return \rest_url( $this->get_namespace() . $this->get_route() );
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\AI_Generator\Infrastructure;
|
||||
|
||||
use WPSEO_Utils;
|
||||
use Yoast\WP\SEO\AI_Generator\Domain\URLs_Interface;
|
||||
|
||||
/**
|
||||
* Class WordPress_URLs
|
||||
* Provides URLs for the AI Generator API in a WordPress context.
|
||||
*/
|
||||
class WordPress_URLs implements URLs_Interface {
|
||||
|
||||
/**
|
||||
* Gets the license URL.
|
||||
*
|
||||
* @return string The license URL.
|
||||
*/
|
||||
public function get_license_url(): string {
|
||||
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 string The callbacks URL.
|
||||
*/
|
||||
public function get_callback_url(): string {
|
||||
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 string The callbacks URL.
|
||||
*/
|
||||
public function get_refresh_callback_url(): string {
|
||||
return \get_rest_url( null, 'yoast/v1/ai_generator/refresh_callback' );
|
||||
}
|
||||
}
|
||||
Executable
+162
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\AI_Generator\User_Interface;
|
||||
|
||||
use WPSEO_Addon_Manager;
|
||||
use WPSEO_Admin_Asset_Manager;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Infrastructure\API_Client;
|
||||
use Yoast\WP\SEO\Conditionals\AI_Conditional;
|
||||
use Yoast\WP\SEO\Conditionals\AI_Editor_Conditional;
|
||||
use Yoast\WP\SEO\Helpers\Current_Page_Helper;
|
||||
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\Application\Ai_Fix_Assessments_Upsell;
|
||||
use Yoast\WP\SEO\Introductions\Infrastructure\Introductions_Seen_Repository;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Holds the API client instance.
|
||||
*
|
||||
* @var API_Client
|
||||
*/
|
||||
private $api_client;
|
||||
|
||||
/**
|
||||
* Represents the current page helper.
|
||||
*
|
||||
* @var Current_Page_Helper
|
||||
*/
|
||||
private $current_page_helper;
|
||||
|
||||
/**
|
||||
* Represents the options manager.
|
||||
*
|
||||
* @var Options_Helper
|
||||
*/
|
||||
private $options_helper;
|
||||
|
||||
/**
|
||||
* Represents the user helper.
|
||||
*
|
||||
* @var User_Helper
|
||||
*/
|
||||
private $user_helper;
|
||||
|
||||
/**
|
||||
* Represents the introductions seen repository.
|
||||
*
|
||||
* @var Introductions_Seen_Repository
|
||||
*/
|
||||
private $introductions_seen_repository;
|
||||
|
||||
/**
|
||||
* Returns the conditionals based in which this loadable should be active.
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
public static function get_conditionals() {
|
||||
return [ AI_Conditional::class, 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 API_Client $api_client The API client.
|
||||
* @param Current_Page_Helper $current_page_helper The current page helper.
|
||||
* @param Options_Helper $options_helper The options helper.
|
||||
* @param User_Helper $user_helper The user helper.
|
||||
* @param Introductions_Seen_Repository $introductions_seen_repository The introductions seen repository.
|
||||
*/
|
||||
public function __construct(
|
||||
WPSEO_Admin_Asset_Manager $asset_manager,
|
||||
WPSEO_Addon_Manager $addon_manager,
|
||||
API_Client $api_client,
|
||||
Current_Page_Helper $current_page_helper,
|
||||
Options_Helper $options_helper,
|
||||
User_Helper $user_helper,
|
||||
Introductions_Seen_Repository $introductions_seen_repository
|
||||
) {
|
||||
$this->asset_manager = $asset_manager;
|
||||
$this->addon_manager = $addon_manager;
|
||||
$this->api_client = $api_client;
|
||||
$this->current_page_helper = $current_page_helper;
|
||||
$this->options_helper = $options_helper;
|
||||
$this->user_helper = $user_helper;
|
||||
$this->introductions_seen_repository = $introductions_seen_repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the integration.
|
||||
*
|
||||
* This is the place to register hooks and filters.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register_hooks() {
|
||||
\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 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the subscription status for Yoast SEO Premium and Yoast WooCommerce SEO.
|
||||
*
|
||||
* @return array<string, bool>
|
||||
*/
|
||||
public function get_product_subscriptions() {
|
||||
return [
|
||||
'premiumSubscription' => $this->addon_manager->has_valid_subscription( WPSEO_Addon_Manager::PREMIUM_SLUG ),
|
||||
'wooCommerceSubscription' => $this->addon_manager->has_valid_subscription( WPSEO_Addon_Manager::WOOCOMMERCE_SLUG ),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the data that should be passed to the script.
|
||||
*
|
||||
* @return array<string|array<string>>
|
||||
*/
|
||||
public function get_script_data() {
|
||||
$user_id = $this->user_helper->get_current_user_id();
|
||||
|
||||
return [
|
||||
'hasConsent' => $this->user_helper->get_meta( $user_id, '_yoast_wpseo_ai_consent', true ),
|
||||
'productSubscriptions' => $this->get_product_subscriptions(),
|
||||
'hasSeenIntroduction' => $this->introductions_seen_repository->is_introduction_seen( $user_id, AI_Fix_Assessments_Upsell::ID ),
|
||||
'requestTimeout' => $this->api_client->get_request_timeout(),
|
||||
'isFreeSparks' => $this->options_helper->get( 'ai_free_sparks_started_on', null ) !== null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueues the required assets.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function enqueue_assets() {
|
||||
|
||||
$this->asset_manager->enqueue_script( 'ai-generator' );
|
||||
$this->asset_manager->localize_script( 'ai-generator', 'wpseoAiGenerator', $this->get_script_data() );
|
||||
$this->asset_manager->enqueue_style( 'ai-generator' );
|
||||
}
|
||||
}
|
||||
Executable
+89
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
|
||||
namespace Yoast\WP\SEO\AI_Generator\User_Interface;
|
||||
|
||||
use WP_REST_Response;
|
||||
use WPSEO_Addon_Manager;
|
||||
use Yoast\WP\SEO\Conditionals\AI_Conditional;
|
||||
use Yoast\WP\SEO\Main;
|
||||
use Yoast\WP\SEO\Routes\Route_Interface;
|
||||
|
||||
/**
|
||||
* Registers a route to bust the subscription cache.
|
||||
*
|
||||
* @makePublic
|
||||
*
|
||||
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
|
||||
*/
|
||||
class Bust_Subscription_Cache_Route implements Route_Interface {
|
||||
|
||||
use Route_Permission_Trait;
|
||||
|
||||
/**
|
||||
* The namespace for this route.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const ROUTE_NAMESPACE = Main::API_V1_NAMESPACE;
|
||||
|
||||
/**
|
||||
* The prefix for this route.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const ROUTE_PREFIX = '/ai_generator/bust_subscription_cache';
|
||||
|
||||
/**
|
||||
* The addon manager instance.
|
||||
*
|
||||
* @var WPSEO_Addon_Manager
|
||||
*/
|
||||
private $addon_manager;
|
||||
|
||||
/**
|
||||
* Returns the conditionals based in which this loadable should be active.
|
||||
*
|
||||
* @return array<string> The conditionals.
|
||||
*/
|
||||
public static function get_conditionals() {
|
||||
return [ AI_Conditional::class ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param WPSEO_Addon_Manager $addon_manager The addon manager instance.
|
||||
*/
|
||||
public function __construct( WPSEO_Addon_Manager $addon_manager ) {
|
||||
$this->addon_manager = $addon_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers routes with WordPress.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register_routes() {
|
||||
\register_rest_route(
|
||||
self::ROUTE_NAMESPACE,
|
||||
self::ROUTE_PREFIX,
|
||||
[
|
||||
'methods' => 'POST',
|
||||
'args' => [],
|
||||
'callback' => [ $this, 'bust_subscription_cache' ],
|
||||
'permission_callback' => [ $this, 'check_permissions' ],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the callback that busts the subscription cache.
|
||||
*
|
||||
* @return WP_REST_Response The response of the callback action.
|
||||
*/
|
||||
public function bust_subscription_cache(): WP_REST_Response {
|
||||
$this->addon_manager->remove_site_information_transients();
|
||||
|
||||
return new WP_REST_Response( 'Subscription cache successfully busted.' );
|
||||
}
|
||||
}
|
||||
Executable
+174
@@ -0,0 +1,174 @@
|
||||
<?php
|
||||
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
|
||||
namespace Yoast\WP\SEO\AI_Generator\User_Interface;
|
||||
|
||||
use RuntimeException;
|
||||
use WP_REST_Request;
|
||||
use WP_REST_Response;
|
||||
use Yoast\WP\SEO\AI_Generator\Application\Suggestions_Provider;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Payment_Required_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Remote_Request_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Too_Many_Requests_Exception;
|
||||
use Yoast\WP\SEO\Conditionals\AI_Conditional;
|
||||
use Yoast\WP\SEO\Conditionals\No_Conditionals;
|
||||
use Yoast\WP\SEO\Main;
|
||||
use Yoast\WP\SEO\Routes\Route_Interface;
|
||||
|
||||
/**
|
||||
* Registers a route to get suggestions from the AI API
|
||||
*
|
||||
* @makePublic
|
||||
*
|
||||
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
|
||||
*/
|
||||
class Get_Suggestions_Route implements Route_Interface {
|
||||
|
||||
use No_Conditionals;
|
||||
use Route_Permission_Trait;
|
||||
|
||||
/**
|
||||
* The namespace for this route.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const ROUTE_NAMESPACE = Main::API_V1_NAMESPACE;
|
||||
|
||||
/**
|
||||
* The prefix for this route.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const ROUTE_PREFIX = '/ai_generator/get_suggestions';
|
||||
|
||||
/**
|
||||
* The suggestions provider instance.
|
||||
*
|
||||
* @var Suggestions_Provider
|
||||
*/
|
||||
private $suggestions_provider;
|
||||
|
||||
/**
|
||||
* Returns the conditionals based in which this loadable should be active.
|
||||
*
|
||||
* @return array<string> The conditionals.
|
||||
*/
|
||||
public static function get_conditionals() {
|
||||
return [ AI_Conditional::class ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param Suggestions_Provider $suggestions_provider The suggestions provider instance.
|
||||
*/
|
||||
public function __construct( Suggestions_Provider $suggestions_provider ) {
|
||||
$this->suggestions_provider = $suggestions_provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers routes with WordPress.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register_routes() {
|
||||
\register_rest_route(
|
||||
self::ROUTE_NAMESPACE,
|
||||
self::ROUTE_PREFIX,
|
||||
[
|
||||
'methods' => 'POST',
|
||||
'args' => [
|
||||
'type' => [
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
'enum' => [
|
||||
'seo-title',
|
||||
'meta-description',
|
||||
'product-seo-title',
|
||||
'product-meta-description',
|
||||
'product-taxonomy-seo-title',
|
||||
'product-taxonomy-meta-description',
|
||||
'taxonomy-seo-title',
|
||||
'taxonomy-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.',
|
||||
],
|
||||
'editor' => [
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
'enum' => [
|
||||
'classic',
|
||||
'elementor',
|
||||
'gutenberg',
|
||||
],
|
||||
'description' => 'The current editor.',
|
||||
],
|
||||
],
|
||||
'callback' => [ $this, 'get_suggestions' ],
|
||||
'permission_callback' => [ $this, 'check_permissions' ],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 ): WP_REST_Response {
|
||||
try {
|
||||
$user = \wp_get_current_user();
|
||||
$data = $this->suggestions_provider->get_suggestions(
|
||||
$user,
|
||||
$request->get_param( 'type' ),
|
||||
$request->get_param( 'prompt_content' ),
|
||||
$request->get_param( 'focus_keyphrase' ),
|
||||
$request->get_param( 'language' ),
|
||||
$request->get_param( 'platform' ),
|
||||
$request->get_param( 'editor' )
|
||||
);
|
||||
} catch ( Remote_Request_Exception $e ) {
|
||||
$message = [
|
||||
'message' => $e->getMessage(),
|
||||
'errorIdentifier' => $e->get_error_identifier(),
|
||||
];
|
||||
if ( $e instanceof Payment_Required_Exception || $e instanceof Too_Many_Requests_Exception ) {
|
||||
$message['missingLicenses'] = $e->get_missing_licenses();
|
||||
}
|
||||
return new WP_REST_Response(
|
||||
$message,
|
||||
$e->getCode()
|
||||
);
|
||||
} catch ( RuntimeException $e ) {
|
||||
return new WP_REST_Response( 'Failed to get suggestions.', 500 );
|
||||
}
|
||||
|
||||
return new WP_REST_Response( $data );
|
||||
}
|
||||
}
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
|
||||
namespace Yoast\WP\SEO\AI_Generator\User_Interface;
|
||||
|
||||
use WP_REST_Response;
|
||||
use WPSEO_Addon_Manager;
|
||||
use Yoast\WP\SEO\AI_Authorization\Application\Token_Manager;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Application\Request_Handler;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Remote_Request_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Too_Many_Requests_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\WP_Request_Exception;
|
||||
use Yoast\WP\SEO\AI_HTTP_Request\Domain\Request;
|
||||
use Yoast\WP\SEO\Conditionals\AI_Conditional;
|
||||
use Yoast\WP\SEO\Main;
|
||||
use Yoast\WP\SEO\Routes\Route_Interface;
|
||||
|
||||
/**
|
||||
* Registers a route to get suggestions from the AI API
|
||||
*
|
||||
* @makePublic
|
||||
*
|
||||
* @phpcs:disable Yoast.NamingConventions.ObjectNameDepth.MaxExceeded
|
||||
*/
|
||||
class Get_Usage_Route implements Route_Interface {
|
||||
|
||||
use Route_Permission_Trait;
|
||||
|
||||
/**
|
||||
* The namespace for this route.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const ROUTE_NAMESPACE = Main::API_V1_NAMESPACE;
|
||||
|
||||
/**
|
||||
* The prefix for this route.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const ROUTE_PREFIX = '/ai_generator/get_usage';
|
||||
|
||||
/**
|
||||
* The token manager instance.
|
||||
*
|
||||
* @var Token_Manager
|
||||
*/
|
||||
private $token_manager;
|
||||
|
||||
/**
|
||||
* The request handler instance.
|
||||
*
|
||||
* @var Request_Handler
|
||||
*/
|
||||
private $request_handler;
|
||||
|
||||
/**
|
||||
* Represents the add-on manager.
|
||||
*
|
||||
* @var WPSEO_Addon_Manager
|
||||
*/
|
||||
private $addon_manager;
|
||||
|
||||
/**
|
||||
* Returns the conditionals based in which this loadable should be active.
|
||||
*
|
||||
* @return array<string> The conditionals.
|
||||
*/
|
||||
public static function get_conditionals() {
|
||||
return [ AI_Conditional::class ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param Token_Manager $token_manager The token manager instance.
|
||||
* @param Request_Handler $request_handler The request handler instance.
|
||||
* @param WPSEO_Addon_Manager $addon_manager The add-on manager instance.
|
||||
*/
|
||||
public function __construct( Token_Manager $token_manager, Request_Handler $request_handler, WPSEO_Addon_Manager $addon_manager ) {
|
||||
$this->addon_manager = $addon_manager;
|
||||
$this->token_manager = $token_manager;
|
||||
$this->request_handler = $request_handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers routes with WordPress.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register_routes() {
|
||||
\register_rest_route(
|
||||
self::ROUTE_NAMESPACE,
|
||||
self::ROUTE_PREFIX,
|
||||
[
|
||||
'methods' => 'POST',
|
||||
'args' => [
|
||||
'is_woo_product_entity' => [
|
||||
'type' => 'boolean',
|
||||
'default' => false,
|
||||
],
|
||||
],
|
||||
'callback' => [ $this, 'get_usage' ],
|
||||
'permission_callback' => [ $this, 'check_permissions' ],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the callback that gets the monthly usage of the user.
|
||||
*
|
||||
* @param WP_REST_Request $request The request object.
|
||||
*
|
||||
* @return WP_REST_Response The response of the callback action.
|
||||
*/
|
||||
public function get_usage( $request ): WP_REST_Response {
|
||||
$is_woo_product_entity = $request->get_param( 'is_woo_product_entity' );
|
||||
$user = \wp_get_current_user();
|
||||
try {
|
||||
$token = $this->token_manager->get_or_request_access_token( $user );
|
||||
$request_headers = [
|
||||
'Authorization' => "Bearer $token",
|
||||
];
|
||||
$action_path = $this->get_action_path( $is_woo_product_entity );
|
||||
$response = $this->request_handler->handle( new Request( $action_path, [], $request_headers, false ) );
|
||||
$data = \json_decode( $response->get_body() );
|
||||
|
||||
} catch ( Remote_Request_Exception | WP_Request_Exception $e ) {
|
||||
$message = [
|
||||
'errorMessage' => $e->getMessage(),
|
||||
'errorIdentifier' => $e->get_error_identifier(),
|
||||
'errorCode' => $e->getCode(),
|
||||
];
|
||||
if ( $e instanceof Too_Many_Requests_Exception ) {
|
||||
$message['missingLicenses'] = $e->get_missing_licenses();
|
||||
}
|
||||
return new WP_REST_Response(
|
||||
$message,
|
||||
$e->getCode()
|
||||
);
|
||||
}
|
||||
|
||||
return new WP_REST_Response( $data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get action path for the request.
|
||||
*
|
||||
* @param bool $is_woo_product_entity Whether the request is for a WooCommerce product entity.
|
||||
*
|
||||
* @return string The action path.
|
||||
*/
|
||||
public function get_action_path( $is_woo_product_entity = false ): string {
|
||||
$unlimited = '/usage/' . \gmdate( 'Y-m' );
|
||||
if ( $is_woo_product_entity && $this->addon_manager->has_valid_subscription( WPSEO_Addon_Manager::WOOCOMMERCE_SLUG ) ) {
|
||||
return $unlimited;
|
||||
}
|
||||
if ( ! $is_woo_product_entity && $this->addon_manager->has_valid_subscription( WPSEO_Addon_Manager::PREMIUM_SLUG ) ) {
|
||||
return $unlimited;
|
||||
}
|
||||
return '/usage/free-usages';
|
||||
}
|
||||
}
|
||||
Executable
+25
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Yoast\WP\SEO\AI_Generator\User_Interface;
|
||||
|
||||
/**
|
||||
* Trait for common permission checks in route classes.
|
||||
*/
|
||||
trait Route_Permission_Trait {
|
||||
|
||||
/**
|
||||
* 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(): bool {
|
||||
$user = \wp_get_current_user();
|
||||
if ( $user === null || $user->ID < 1 ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return \user_can( $user, 'edit_posts' );
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user