PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 28.0
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v28.0
28.5 28.4 28.3 28.2 28.1 28.0 27.9 27.8 27.7 27.6 27.5 trunk 18.0 18.1 18.2 18.3 18.4 18.4.1 18.5 18.5.1 18.6 18.7 18.8 18.9 19.0 All 129 releases
wordpress-seo / src / ai-generator / application / suggestions-provider.php

suggestions-provider.php in Yoast SEO – Advanced SEO with real-time guidance and built-in AI 28.0, at src/ai-generator/application/suggestions-provider.php

185 lines 6.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Yoast\WP\SEO\AI_Generator\Application;
4
5 use RuntimeException;
6 use WP_User;
7 use Yoast\WP\SEO\AI_Authorization\Application\Token_Manager;
8 use Yoast\WP\SEO\AI_Consent\Application\Consent_Handler;
9 use Yoast\WP\SEO\AI_Generator\Domain\Suggestion;
10 use Yoast\WP\SEO\AI_Generator\Domain\Suggestions_Bucket;
11 use Yoast\WP\SEO\AI_HTTP_Request\Application\Request_Handler;
12 use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Bad_Request_Exception;
13 use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Forbidden_Exception;
14 use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Internal_Server_Error_Exception;
15 use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Not_Found_Exception;
16 use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Payment_Required_Exception;
17 use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Request_Timeout_Exception;
18 use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Service_Unavailable_Exception;
19 use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Too_Many_Requests_Exception;
20 use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Unauthorized_Exception;
21 use Yoast\WP\SEO\AI_HTTP_Request\Domain\Request;
22 use Yoast\WP\SEO\AI_HTTP_Request\Domain\Response;
23 use Yoast\WP\SEO\Helpers\User_Helper;
24
25 /**
26 * The class that handles the suggestions from the AI API.
27 */
28 class Suggestions_Provider {
29
30 /**
31 * The consent handler instance.
32 *
33 * @var Consent_Handler
34 */
35 private $consent_handler;
36
37 /**
38 * The request handler instance.
39 *
40 * @var Request_Handler
41 */
42 private $request_handler;
43
44 /**
45 * The token manager instance.
46 *
47 * @var Token_Manager
48 */
49 private $token_manager;
50
51 /**
52 * The user helper instance.
53 *
54 * @var User_Helper
55 */
56 private $user_helper;
57
58 /**
59 * Class constructor.
60 *
61 * @param Consent_Handler $consent_handler The consent handler instance.
62 * @param Request_Handler $request_handler The request handler instance.
63 * @param Token_Manager $token_manager The token manager instance.
64 * @param User_Helper $user_helper The user helper instance.
65 */
66 public function __construct(
67 Consent_Handler $consent_handler,
68 Request_Handler $request_handler,
69 Token_Manager $token_manager,
70 User_Helper $user_helper
71 ) {
72 $this->consent_handler = $consent_handler;
73 $this->request_handler = $request_handler;
74 $this->token_manager = $token_manager;
75 $this->user_helper = $user_helper;
76 }
77
78 // phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber -- PHPCS doesn't take into account exceptions thrown in called methods.
79
80 /**
81 * Method used to generate suggestions through AI.
82 *
83 * @param WP_User $user The WP user.
84 * @param string $suggestion_type The type of the requested suggestion.
85 * @param string $prompt_content The excerpt taken from the post.
86 * @param string $focus_keyphrase The focus keyphrase associated to the post.
87 * @param string $language The language of the post.
88 * @param string $platform The platform the post is intended for.
89 * @param string $editor The current editor.
90 * @param bool $retry_on_unauthorized Whether to retry when unauthorized (mechanism to retry once).
91 *
92 * @throws Bad_Request_Exception Bad_Request_Exception.
93 * @throws Forbidden_Exception Forbidden_Exception.
94 * @throws Internal_Server_Error_Exception Internal_Server_Error_Exception.
95 * @throws Not_Found_Exception Not_Found_Exception.
96 * @throws Payment_Required_Exception Payment_Required_Exception.
97 * @throws Request_Timeout_Exception Request_Timeout_Exception.
98 * @throws Service_Unavailable_Exception Service_Unavailable_Exception.
99 * @throws Too_Many_Requests_Exception Too_Many_Requests_Exception.
100 * @throws Unauthorized_Exception Unauthorized_Exception.
101 * @throws RuntimeException Unable to retrieve the access token.
102 * @return string[] The suggestions.
103 */
104 public function get_suggestions(
105 WP_User $user,
106 string $suggestion_type,
107 string $prompt_content,
108 string $focus_keyphrase,
109 string $language,
110 string $platform,
111 string $editor,
112 bool $retry_on_unauthorized = true
113 ): array {
114 try {
115 $token = $this->token_manager->get_or_request_access_token( $user );
116 } catch ( Forbidden_Exception $exception ) {
117 // Follow the API in the consent being revoked (Use case: user sent an e-mail to revoke?).
118 // phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- false positive.
119 $this->consent_handler->revoke_consent( $user->ID );
120 throw new Forbidden_Exception( 'CONSENT_REVOKED', $exception->getCode() );
121 // phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped
122 }
123
124 $request_body = [
125 'service' => 'openai',
126 'user_id' => (string) $user->ID,
127 'subject' => [
128 'content' => $prompt_content,
129 'focus_keyphrase' => $focus_keyphrase,
130 'language' => $language,
131 'platform' => $platform,
132 ],
133 ];
134 $request_headers = [
135 'Authorization' => "Bearer $token",
136 'X-Yst-Cohort' => $editor,
137 ];
138
139 try {
140 $response = $this->request_handler->handle( new Request( "/openai/suggestions/$suggestion_type", $request_body, $request_headers ) );
141 } catch ( Unauthorized_Exception $exception ) {
142 // Delete the stored JWT tokens, as they appear to be no longer valid.
143 $this->user_helper->delete_meta( $user->ID, '_yoast_wpseo_ai_generator_access_jwt' );
144 $this->user_helper->delete_meta( $user->ID, '_yoast_wpseo_ai_generator_refresh_jwt' );
145
146 if ( ! $retry_on_unauthorized ) {
147 throw $exception;
148 }
149
150 // Try again once more by fetching a new set of tokens and trying the suggestions endpoint again.
151 return $this->get_suggestions( $user, $suggestion_type, $prompt_content, $focus_keyphrase, $language, $platform, $editor, false );
152 } catch ( Forbidden_Exception $exception ) {
153 // Follow the API in the consent being revoked (Use case: user sent an e-mail to revoke?).
154 // phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- false positive.
155 $this->consent_handler->revoke_consent( $user->ID );
156 throw new Forbidden_Exception( 'CONSENT_REVOKED', $exception->getCode() );
157 // phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped
158 }
159
160 return $this->build_suggestions_array( $response )->to_array();
161 }
162
163 // phpcs:enable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber
164
165 /**
166 * Generates the list of 5 suggestions to return.
167 *
168 * @param Response $response The response from the API.
169 *
170 * @return Suggestions_Bucket The array of suggestions.
171 */
172 public function build_suggestions_array( Response $response ): Suggestions_Bucket {
173 $suggestions_bucket = new Suggestions_Bucket();
174 $json = \json_decode( $response->get_body() );
175 if ( $json === null || ! isset( $json->choices ) ) {
176 return $suggestions_bucket;
177 }
178 foreach ( $json->choices as $suggestion ) {
179 $suggestions_bucket->add_suggestion( new Suggestion( $suggestion->text ) );
180 }
181
182 return $suggestions_bucket;
183 }
184 }
185