PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 28.5
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v28.5
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
← All changes | src/ai/authorization/application/token-manager.php +47 -42 27.628.5 View file →
@@ -9,9 +9,8 @@
9 9 use WPSEO_Utils;
10 10 use Yoast\WP\SEO\AI\Authorization\Infrastructure\Access_Token_User_Meta_Repository_Interface;
11 11 use Yoast\WP\SEO\AI\Authorization\Infrastructure\Code_Verifier_User_Meta_Repository;
12 12 use Yoast\WP\SEO\AI\Authorization\Infrastructure\Refresh_Token_User_Meta_Repository_Interface;
13 -use Yoast\WP\SEO\AI\Consent\Application\Consent_Handler;
14 13 use Yoast\WP\SEO\AI\Generator\Infrastructure\WordPress_URLs;
15 14 use Yoast\WP\SEO\AI\HTTP_Request\Application\Request_Handler;
16 15 use Yoast\WP\SEO\AI\HTTP_Request\Domain\Exceptions\Bad_Request_Exception;
17 16 use Yoast\WP\SEO\AI\HTTP_Request\Domain\Exceptions\Forbidden_Exception;
@@ -47,15 +46,8 @@
47 46 */
48 47 private $code_verifier;
49 48
50 49 /**
51 - * The consent handler.
52 - *
53 - * @var Consent_Handler
54 - */
55 - private $consent_handler;
56 -
57 - /**
58 50 * The refresh token repository.
59 51 *
60 52 * @var Refresh_Token_User_Meta_Repository_Interface
61 53 */
@@ -93,9 +85,8 @@
93 85 * Token_Manager constructor.
94 86 *
95 87 * @param Access_Token_User_Meta_Repository_Interface $access_token_repository The access token repository.
96 88 * @param Code_Verifier_Handler $code_verifier The code verifier service.
97 - * @param Consent_Handler $consent_handler The consent handler.
98 89 * @param Refresh_Token_User_Meta_Repository_Interface $refresh_token_repository The refresh token repository.
99 90 * @param User_Helper $user_helper The user helper.
100 91 * @param Request_Handler $request_handler The request handler.
101 92 * @param Code_Verifier_User_Meta_Repository $code_verifier_repository The code verifier repository.
@@ -103,9 +94,8 @@
103 94 */
104 95 public function __construct(
105 96 Access_Token_User_Meta_Repository_Interface $access_token_repository,
106 97 Code_Verifier_Handler $code_verifier,
107 - Consent_Handler $consent_handler,
108 98 Refresh_Token_User_Meta_Repository_Interface $refresh_token_repository,
109 99 User_Helper $user_helper,
110 100 Request_Handler $request_handler,
111 101 Code_Verifier_User_Meta_Repository $code_verifier_repository,
@@ -112,9 +102,8 @@
112 102 WordPress_URLs $urls
113 103 ) {
114 104 $this->access_token_repository = $access_token_repository;
115 105 $this->code_verifier = $code_verifier;
116 - $this->consent_handler = $consent_handler;
117 106 $this->refresh_token_repository = $refresh_token_repository;
118 107 $this->user_helper = $user_helper;
119 108 $this->request_handler = $request_handler;
120 109 $this->code_verifier_repository = $code_verifier_repository;
@@ -125,10 +114,15 @@
125 114
126 115 /**
127 116 * Invalidates the access token.
128 117 *
129 - * @param string $user_id The user ID.
118 + * The locally stored JWTs are always cleared, even when the remote invalidation fails — the
119 + * remote exception still propagates to the caller, but no credentials are left behind.
130 120 *
121 + * @param int $user_id The user ID.
122 + *
123 + * @return void
124 + *
131 125 * @throws Bad_Request_Exception Bad_Request_Exception.
132 126 * @throws Internal_Server_Error_Exception Internal_Server_Error_Exception.
133 127 * @throws Not_Found_Exception Not_Found_Exception.
134 128 * @throws Payment_Required_Exception Payment_Required_Exception.
@@ -135,11 +129,10 @@
135 129 * @throws Request_Timeout_Exception Request_Timeout_Exception.
136 130 * @throws Service_Unavailable_Exception Service_Unavailable_Exception.
137 131 * @throws Too_Many_Requests_Exception Too_Many_Requests_Exception.
138 132 * @throws RuntimeException Unable to retrieve the access token.
139 - * @return void
140 133 */
141 - public function token_invalidate( string $user_id ): void {
134 + public function token_invalidate( int $user_id ): void {
142 135 try {
143 136 $access_jwt = $this->access_token_repository->get_token( $user_id );
144 137 } catch ( RuntimeException $e ) {
145 138 $access_jwt = '';
@@ -144,43 +137,68 @@
144 137 } catch ( RuntimeException $e ) {
145 138 $access_jwt = '';
146 139 }
147 140
148 - $request_body = [
149 - 'user_id' => (string) $user_id,
150 - ];
151 141 $request_headers = [
152 142 'Authorization' => "Bearer $access_jwt",
153 143 ];
154 144
155 145 try {
146 + // The endpoint takes no request body; the user is identified by the access token.
156 147 $this->request_handler->handle(
157 148 new Request(
158 149 '/token/invalidate',
159 - $request_body,
150 + [],
160 151 $request_headers,
161 152 ),
162 153 );
163 154 } catch ( Unauthorized_Exception | Forbidden_Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch -- Reason: Ignored on purpose.
164 155 // If the credentials in our request were already invalid, our job is done and we continue to remove the tokens client-side.
156 + } finally {
157 + // Always clear the local tokens, even when the remote invalidation fails with an exception
158 + // that propagates: leaving credentials behind would contradict the intent of invalidating.
159 + $this->clear_tokens( $user_id );
165 160 }
166 -
167 - $this->clear_tokens( $user_id );
168 161 }
169 162
170 163 /**
171 164 * Clears the user meta tokens for a specific user.
172 165 *
173 - * @param string $user_id The user id to delete this for.
166 + * @param int $user_id The user id to delete this for.
174 167 *
175 168 * @return void
176 169 */
177 - public function clear_tokens( string $user_id ): void {
170 + public function clear_tokens( int $user_id ): void {
178 171 $this->access_token_repository->delete_token( $user_id );
179 172 $this->refresh_token_repository->delete_token( $user_id );
180 173 }
181 174
182 175 /**
176 + * Checks whether any JWT (access or refresh) is stored locally for the user.
177 + *
178 + * @param int $user_id The user ID.
179 + *
180 + * @return bool Whether a locally stored JWT exists.
181 + */
182 + public function has_local_tokens( int $user_id ): bool {
183 + try {
184 + $this->access_token_repository->get_token( $user_id );
185 +
186 + return true;
187 + } catch ( RuntimeException $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch -- Reason: Ignored on purpose.
188 + // No access token; fall through to the refresh token check.
189 + }
190 +
191 + try {
192 + $this->refresh_token_repository->get_token( $user_id );
193 +
194 + return true;
195 + } catch ( RuntimeException $e ) {
196 + return false;
197 + }
198 + }
199 +
200 + /**
183 201 * Requests a new set of JWT tokens.
184 202 *
185 203 * Requests a new JWT access and refresh token for a user from the Yoast AI Service and stores it in the database
186 204 * under usermeta. The storing of the token happens in a HTTP callback that is triggered by this request.
@@ -186,8 +204,10 @@
186 204 * under usermeta. The storing of the token happens in a HTTP callback that is triggered by this request.
187 205 *
188 206 * @param WP_User $user The WP user.
189 207 *
208 + * @return void
209 + *
190 210 * @throws Bad_Request_Exception Bad_Request_Exception.
191 211 * @throws Forbidden_Exception Forbidden_Exception.
192 212 * @throws Internal_Server_Error_Exception Internal_Server_Error_Exception.
193 213 * @throws Not_Found_Exception Not_Found_Exception.
@@ -195,20 +215,10 @@
195 215 * @throws Request_Timeout_Exception Request_Timeout_Exception.
196 216 * @throws Service_Unavailable_Exception Service_Unavailable_Exception.
197 217 * @throws Too_Many_Requests_Exception Too_Many_Requests_Exception.
198 218 * @throws Unauthorized_Exception Unauthorized_Exception.
199 - * @return void
200 219 */
201 220 public function token_request( WP_User $user ): void {
202 - // Ensure the user has given consent.
203 - if ( $this->user_helper->get_meta( $user->ID, '_yoast_wpseo_ai_consent', true ) !== '1' ) {
204 - // phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- false positive.
205 - $this->consent_handler->revoke_consent( $user->ID );
206 - throw new Forbidden_Exception( 'CONSENT_REVOKED', 403 );
207 -
208 - // phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped
209 - }
210 -
211 221 // Generate a code verifier and store it in the database.
212 222 $code_verifier = $this->code_verifier->generate( $user->user_email );
213 223 $this->code_verifier_repository->store_code_verifier( $user->ID, $code_verifier->get_code(), $code_verifier->get_created_at() );
214 224
@@ -240,8 +250,10 @@
240 250 * usermeta. The storing of the token happens in a HTTP callback that is triggered by this request.
241 251 *
242 252 * @param WP_User $user The WP user.
243 253 *
254 + * @return void
255 + *
244 256 * @throws Bad_Request_Exception Bad_Request_Exception.
245 257 * @throws Forbidden_Exception Forbidden_Exception.
246 258 * @throws Internal_Server_Error_Exception Internal_Server_Error_Exception.
247 259 * @throws Not_Found_Exception Not_Found_Exception.
@@ -250,9 +262,8 @@
250 262 * @throws Service_Unavailable_Exception Service_Unavailable_Exception.
251 263 * @throws Too_Many_Requests_Exception Too_Many_Requests_Exception.
252 264 * @throws Unauthorized_Exception Unauthorized_Exception.
253 265 * @throws RuntimeException Unable to retrieve the refresh token.
254 - * @return void
255 266 */
256 267 public function token_refresh( WP_User $user ): void {
257 268 $refresh_jwt = $this->refresh_token_repository->get_token( $user->ID );
258 269
@@ -306,8 +317,10 @@
306 317 * Retrieves the access token.
307 318 *
308 319 * @param WP_User $user The WP user.
309 320 *
321 + * @return string The access token.
322 + *
310 323 * @throws Bad_Request_Exception Bad_Request_Exception.
311 324 * @throws Forbidden_Exception Forbidden_Exception.
312 325 * @throws Internal_Server_Error_Exception Internal_Server_Error_Exception.
313 326 * @throws Not_Found_Exception Not_Found_Exception.
@@ -316,15 +329,13 @@
316 329 * @throws Service_Unavailable_Exception Service_Unavailable_Exception.
317 330 * @throws Too_Many_Requests_Exception Too_Many_Requests_Exception.
318 331 * @throws Unauthorized_Exception Unauthorized_Exception.
319 332 * @throws RuntimeException Unable to retrieve the access or refresh token.
320 - * @return string The access token.
321 333 */
322 334 public function get_or_request_access_token( WP_User $user ): string {
323 335 // If the site URL has changed since callback URLs were registered, delete stale tokens.
324 336 if ( $this->have_callback_urls_changed( $user ) ) {
325 - $this->user_helper->delete_meta( $user->ID, '_yoast_wpseo_ai_generator_access_jwt' );
326 - $this->user_helper->delete_meta( $user->ID, '_yoast_wpseo_ai_generator_refresh_jwt' );
337 + $this->clear_tokens( $user->ID );
327 338 }
328 339
329 340 $access_jwt = $this->user_helper->get_meta( $user->ID, '_yoast_wpseo_ai_generator_access_jwt', true );
330 341 if ( ! \is_string( $access_jwt ) || $access_jwt === '' ) {
@@ -335,14 +346,8 @@
335 346 try {
336 347 $this->token_refresh( $user );
337 348 } catch ( Unauthorized_Exception $exception ) {
338 349 $this->token_request( $user );
339 - } catch ( Forbidden_Exception $exception ) {
340 - // Follow the API in the consent being revoked (Use case: user sent an e-mail to revoke?).
341 - // phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- false positive.
342 - $this->consent_handler->revoke_consent( $user->ID );
343 - throw new Forbidden_Exception( 'CONSENT_REVOKED', 403 );
344 - // phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped
345 350 }
346 351 $access_jwt = $this->access_token_repository->get_token( $user->ID );
347 352 }
348 353