PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 28.1
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v28.1
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-consent / application / consent-handler.php

consent-handler.php in Yoast SEO – Advanced SEO with real-time guidance and built-in AI 28.1, at src/ai-consent/application/consent-handler.php

163 lines 6.6 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_Consent\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_HTTP_Request\Application\Request_Handler;
9 use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Bad_Request_Exception;
10 use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Forbidden_Exception;
11 use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Internal_Server_Error_Exception;
12 use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Not_Found_Exception;
13 use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Payment_Required_Exception;
14 use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Request_Timeout_Exception;
15 use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Service_Unavailable_Exception;
16 use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Too_Many_Requests_Exception;
17 use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\Unauthorized_Exception;
18 use Yoast\WP\SEO\AI_HTTP_Request\Domain\Exceptions\WP_Request_Exception;
19 use Yoast\WP\SEO\AI_HTTP_Request\Domain\Request;
20 use Yoast\WP\SEO\Helpers\User_Helper;
21
22 /**
23 * Class Consent_Handler
24 * Handles the consent given or revoked by the user, both locally (user meta) and remotely (Yoast AI service).
25 *
26 * @makePublic
27 */
28 class Consent_Handler implements Consent_Handler_Interface {
29
30 /**
31 * Holds the user helper instance.
32 *
33 * @var User_Helper
34 */
35 private $user_helper;
36
37 /**
38 * The token manager instance.
39 *
40 * @var Token_Manager
41 */
42 private $token_manager;
43
44 /**
45 * The request handler instance.
46 *
47 * @var Request_Handler
48 */
49 private $request_handler;
50
51 /**
52 * Class constructor.
53 *
54 * @param User_Helper $user_helper The user helper.
55 * @param Token_Manager $token_manager The token manager, used to obtain a JWT for the consent endpoints.
56 * @param Request_Handler $request_handler The request handler, used to call the AI service's consent endpoints.
57 */
58 public function __construct(
59 User_Helper $user_helper,
60 Token_Manager $token_manager,
61 Request_Handler $request_handler
62 ) {
63 $this->user_helper = $user_helper;
64 $this->token_manager = $token_manager;
65 $this->request_handler = $request_handler;
66 }
67
68 // phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber -- PHPCS doesn't take into account exceptions thrown in called methods.
69
70 /**
71 * Records the user's consent on the Yoast AI service and, on success, in the local user meta.
72 *
73 * Transactional: any HTTP-layer exception is propagated and the local meta is left untouched, so
74 * the local and server state stay in sync.
75 *
76 * @param int $user_id The user ID.
77 *
78 * @return void
79 *
80 * @throws Bad_Request_Exception When the AI service responds with 400.
81 * @throws Forbidden_Exception When the AI service responds with 403.
82 * @throws Internal_Server_Error_Exception When the AI service responds with 500.
83 * @throws Not_Found_Exception When the AI service responds with 404.
84 * @throws Payment_Required_Exception When the AI service responds with 402.
85 * @throws Request_Timeout_Exception When the AI service responds with 408.
86 * @throws Service_Unavailable_Exception When the AI service responds with 503.
87 * @throws Too_Many_Requests_Exception When the AI service responds with 429.
88 * @throws Unauthorized_Exception When the AI service responds with 401.
89 * @throws WP_Request_Exception When the underlying WordPress HTTP call fails.
90 * @throws RuntimeException When the user is not found.
91 */
92 public function grant_consent( int $user_id ) {
93 $user = \get_user_by( 'id', $user_id );
94 if ( ! $user instanceof WP_User ) {
95 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- false positive.
96 throw new RuntimeException( "User not found: $user_id" );
97 }
98 $jwt = $this->token_manager->get_or_request_access_token( $user );
99
100 $body = [
101 'user_id' => (string) $user_id,
102 ];
103
104 $this->request_handler->handle(
105 new Request( '/user/consent', $body, [ 'Authorization' => "Bearer $jwt" ], Request::METHOD_POST ),
106 );
107
108 $this->user_helper->update_meta( $user_id, '_yoast_wpseo_ai_consent', true );
109 }
110
111 /**
112 * Revokes the user's consent, both locally (user meta) and remotely (Yoast AI service).
113 *
114 * Security-first: the local meta is always cleared before the remote call, so consent is
115 * revoked locally even if the remote `DELETE /user/consent` fails. Any locally stored JWTs
116 * are then invalidated regardless of the remote outcome — credentials must not outlive
117 * consent. The invalidation runs after the DELETE on purpose: authenticating the DELETE may
118 * mint a fresh JWT, and invalidating afterwards catches that token too. Any HTTP-layer
119 * exception is propagated and its management is deferred to the caller.
120 *
121 * @param int $user_id The user ID.
122 *
123 * @return void
124 *
125 * @throws Bad_Request_Exception When the AI service responds with 400.
126 * @throws Forbidden_Exception When the AI service responds with 403.
127 * @throws Internal_Server_Error_Exception When the AI service responds with 500.
128 * @throws Not_Found_Exception When the AI service responds with 404.
129 * @throws Payment_Required_Exception When the AI service responds with 402.
130 * @throws Request_Timeout_Exception When the AI service responds with 408.
131 * @throws Service_Unavailable_Exception When the AI service responds with 503.
132 * @throws Too_Many_Requests_Exception When the AI service responds with 429.
133 * @throws Unauthorized_Exception When the AI service responds with 401.
134 * @throws WP_Request_Exception When the underlying WordPress HTTP call fails.
135 * @throws RuntimeException When the user is not found.
136 */
137 public function revoke_consent( int $user_id ) {
138 $user = \get_user_by( 'id', $user_id );
139 if ( ! $user instanceof WP_User ) {
140 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- false positive.
141 throw new RuntimeException( "User not found: $user_id" );
142 }
143 // Local consent is always revoked regardless of remote failures.
144 $this->user_helper->delete_meta( $user_id, '_yoast_wpseo_ai_consent' );
145
146 try {
147 $jwt = $this->token_manager->get_or_request_access_token( $user );
148
149 $this->request_handler->handle(
150 new Request( '/user/consent', [], [ 'Authorization' => "Bearer $jwt" ], Request::METHOD_DELETE ),
151 );
152 } finally {
153 // Invalidate the JWTs — including ones minted to authenticate the DELETE above — so
154 // credentials never outlive consent.
155 if ( $this->token_manager->has_local_tokens( $user_id ) ) {
156 $this->token_manager->token_invalidate( $user_id );
157 }
158 }
159 }
160
161 // phpcs:enable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber
162 }
163