PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / trunk
Yoast SEO – Advanced SEO with real-time guidance and built-in AI vtrunk
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/consent/application/consent-handler.php +115 -10 27.9 → trunk View file →
@@ -3,13 +3,29 @@
3 3 // phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
4 4
5 5 namespace Yoast\WP\SEO\AI\Consent\Application;
6 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;
7 23 use Yoast\WP\SEO\Helpers\User_Helper;
8 24
9 25 /**
10 26 * Class Consent_Handler
11 - * Handles the consent given or revoked by the user.
27 + * Handles the consent given or revoked by the user, both locally (user meta) and remotely (Yoast AI service).
12 28 *
13 29 * @makePublic
14 30 */
15 31 class Consent_Handler implements Consent_Handler_Interface {
@@ -21,34 +37,123 @@
21 37 */
22 38 private $user_helper;
23 39
24 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 + /**
25 55 * Class constructor.
26 56 *
27 - * @param User_Helper $user_helper The user helper.
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.
28 60 */
29 - public function __construct( User_Helper $user_helper ) {
30 - $this->user_helper = $user_helper;
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;
31 69 }
32 70
71 + // phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber -- PHPCS doesn't take into account exceptions thrown in called methods.
72 +
33 73 /**
34 - * Handles consent revoked by deleting the consent user metadata from the database.
74 + * Records the user's consent on the Yoast AI service and, on success, in the local user meta.
35 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 + *
36 79 * @param int $user_id The user ID.
37 80 *
38 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.
39 96 */
40 - public function revoke_consent( int $user_id ) {
41 - $this->user_helper->delete_meta( $user_id, '_yoast_wpseo_ai_consent' );
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 );
42 107 }
43 108
44 109 /**
45 - * Handles consent granted by adding the consent user metadata to the database.
110 + * Revokes the user's consent on the Yoast AI service and clears the local user meta.
46 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 + *
47 119 * @param int $user_id The user ID.
48 120 *
49 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.
50 136 */
51 - public function grant_consent( int $user_id ) {
52 - $this->user_helper->update_meta( $user_id, '_yoast_wpseo_ai_consent', true );
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 + }
53 156 }
157 +
158 + // phpcs:enable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber
54 159 }