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
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.5, at src/ai/consent/application/consent-handler.php

160 lines 7.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 // phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
4
5 namespace Yoast\WP\SEO\AI\Consent\Application;
6
7 use RuntimeException;
8 use WP_User;
9 use Yoast\WP\SEO\AI\Authentication\Application\AI_Request_Sender_Factory;
10 use Yoast\WP\SEO\AI\Authorization\Application\Token_Manager;
11 use Yoast\WP\SEO\AI\HTTP_Request\Domain\Exceptions\Bad_Request_Exception;
12 use Yoast\WP\SEO\AI\HTTP_Request\Domain\Exceptions\Consent_Required_Exception;
13 use Yoast\WP\SEO\AI\HTTP_Request\Domain\Exceptions\Forbidden_Exception;
14 use Yoast\WP\SEO\AI\HTTP_Request\Domain\Exceptions\Insufficient_Scope_Exception;
15 use Yoast\WP\SEO\AI\HTTP_Request\Domain\Exceptions\Internal_Server_Error_Exception;
16 use Yoast\WP\SEO\AI\HTTP_Request\Domain\Exceptions\Not_Found_Exception;
17 use Yoast\WP\SEO\AI\HTTP_Request\Domain\Exceptions\Payment_Required_Exception;
18 use Yoast\WP\SEO\AI\HTTP_Request\Domain\Exceptions\Request_Timeout_Exception;
19 use Yoast\WP\SEO\AI\HTTP_Request\Domain\Exceptions\Service_Unavailable_Exception;
20 use Yoast\WP\SEO\AI\HTTP_Request\Domain\Exceptions\Too_Many_Requests_Exception;
21 use Yoast\WP\SEO\AI\HTTP_Request\Domain\Exceptions\Unauthorized_Exception;
22 use Yoast\WP\SEO\AI\HTTP_Request\Domain\Exceptions\WP_Request_Exception;
23 use Yoast\WP\SEO\Helpers\User_Helper;
24
25 /**
26 * Class Consent_Handler
27 * Handles the consent given or revoked by the user, both locally (user meta) and remotely (Yoast AI service).
28 *
29 * @makePublic
30 */
31 class Consent_Handler implements Consent_Handler_Interface {
32
33 /**
34 * Holds the user helper instance.
35 *
36 * @var User_Helper
37 */
38 private $user_helper;
39
40 /**
41 * The AI request sender factory, used to dispatch the consent calls through the active auth strategy.
42 *
43 * @var AI_Request_Sender_Factory
44 */
45 private $ai_request_sender_factory;
46
47 /**
48 * The token manager, used to invalidate leftover legacy JWTs when consent is revoked.
49 *
50 * @var Token_Manager
51 */
52 private $token_manager;
53
54 /**
55 * Class constructor.
56 *
57 * @param User_Helper $user_helper The user helper.
58 * @param AI_Request_Sender_Factory $ai_request_sender_factory The AI request sender factory.
59 * @param Token_Manager $token_manager The token manager.
60 */
61 public function __construct(
62 User_Helper $user_helper,
63 AI_Request_Sender_Factory $ai_request_sender_factory,
64 Token_Manager $token_manager
65 ) {
66 $this->user_helper = $user_helper;
67 $this->ai_request_sender_factory = $ai_request_sender_factory;
68 $this->token_manager = $token_manager;
69 }
70
71 // phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber -- PHPCS doesn't take into account exceptions thrown in called methods.
72
73 /**
74 * Records the user's consent on the Yoast AI service and, on success, in the local user meta.
75 *
76 * Transactional: any HTTP-layer exception is propagated and the local meta is left untouched, so
77 * the local and server state stay in sync.
78 *
79 * @param int $user_id The user ID.
80 *
81 * @return void
82 *
83 * @throws Bad_Request_Exception When the AI service responds with 400.
84 * @throws Consent_Required_Exception When the AI service responds with a 403 indicating consent is required.
85 * @throws Insufficient_Scope_Exception When the AI service responds with a 403 insufficient_scope.
86 * @throws Forbidden_Exception When the AI service responds with any other 403.
87 * @throws Internal_Server_Error_Exception When the AI service responds with 500.
88 * @throws Not_Found_Exception When the AI service responds with 404.
89 * @throws Payment_Required_Exception When the AI service responds with 402.
90 * @throws Request_Timeout_Exception When the AI service responds with 408.
91 * @throws Service_Unavailable_Exception When the AI service responds with 503.
92 * @throws Too_Many_Requests_Exception When the AI service responds with 429.
93 * @throws Unauthorized_Exception When the AI service responds with 401.
94 * @throws WP_Request_Exception When the underlying WordPress HTTP call fails.
95 * @throws RuntimeException When the user is not found.
96 */
97 public function grant_consent( int $user_id ) {
98 $user = \get_user_by( 'id', $user_id );
99 if ( ! $user instanceof WP_User ) {
100 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- false positive.
101 throw new RuntimeException( "User not found: $user_id" );
102 }
103
104 $this->ai_request_sender_factory->create( $user )->grant_consent( $user );
105
106 $this->user_helper->update_meta( $user_id, '_yoast_wpseo_ai_consent', true );
107 }
108
109 /**
110 * Revokes the user's consent on the Yoast AI service and clears the local user meta.
111 *
112 * Security-first: the local meta is always cleared before the remote call, so consent is
113 * revoked locally even if the remote `DELETE /user/consent` fails. Any locally stored legacy
114 * JWTs are then invalidated regardless of the remote outcome — credentials must not outlive
115 * consent. The invalidation runs after the DELETE on purpose: the legacy Token path may mint
116 * a fresh JWT to authenticate the DELETE, and invalidating afterwards catches that token too.
117 * Any HTTP-layer exception is propagated and its management is deferred to the caller.
118 *
119 * @param int $user_id The user ID.
120 *
121 * @return void
122 *
123 * @throws Bad_Request_Exception When the AI service responds with 400.
124 * @throws Consent_Required_Exception When the AI service responds with a 403 indicating consent is required.
125 * @throws Insufficient_Scope_Exception When the AI service responds with a 403 insufficient_scope.
126 * @throws Forbidden_Exception When the AI service responds with any other 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 $this->ai_request_sender_factory->create( $user )->revoke_consent( $user );
148 } finally {
149 // Invalidate the legacy JWTs — including ones minted to authenticate the DELETE above —
150 // so credentials never outlive consent. Skipped when no local JWTs exist (the OAuth path
151 // without a leftover pre-OAuth grant).
152 if ( $this->token_manager->has_local_tokens( $user_id ) ) {
153 $this->token_manager->token_invalidate( $user_id );
154 }
155 }
156 }
157
158 // phpcs:enable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber
159 }
160