user_helper = $user_helper; $this->token_manager = $token_manager; $this->request_handler = $request_handler; } // phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber -- PHPCS doesn't take into account exceptions thrown in called methods. /** * Records the user's consent on the Yoast AI service and, on success, in the local user meta. * * Transactional: any HTTP-layer exception is propagated and the local meta is left untouched, so * the local and server state stay in sync. * * @param int $user_id The user ID. * * @return void * * @throws Bad_Request_Exception When the AI service responds with 400. * @throws Forbidden_Exception When the AI service responds with 403. * @throws Internal_Server_Error_Exception When the AI service responds with 500. * @throws Not_Found_Exception When the AI service responds with 404. * @throws Payment_Required_Exception When the AI service responds with 402. * @throws Request_Timeout_Exception When the AI service responds with 408. * @throws Service_Unavailable_Exception When the AI service responds with 503. * @throws Too_Many_Requests_Exception When the AI service responds with 429. * @throws Unauthorized_Exception When the AI service responds with 401. * @throws WP_Request_Exception When the underlying WordPress HTTP call fails. * @throws RuntimeException When the user is not found. */ public function grant_consent( int $user_id ) { $user = \get_user_by( 'id', $user_id ); if ( ! $user instanceof WP_User ) { // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- false positive. throw new RuntimeException( "User not found: $user_id" ); } $jwt = $this->token_manager->get_or_request_access_token( $user ); $body = [ 'user_id' => (string) $user_id, ]; $this->request_handler->handle( new Request( '/user/consent', $body, [ 'Authorization' => "Bearer $jwt" ], Request::METHOD_POST ), ); $this->user_helper->update_meta( $user_id, '_yoast_wpseo_ai_consent', true ); } /** * Revokes the user's consent, both locally (user meta) and remotely (Yoast AI service). * * Security-first: the local meta is always cleared before the remote call, so consent is * revoked locally even if the remote `DELETE /user/consent` fails. Any locally stored JWTs * are then invalidated regardless of the remote outcome — credentials must not outlive * consent. The invalidation runs after the DELETE on purpose: authenticating the DELETE may * mint a fresh JWT, and invalidating afterwards catches that token too. Any HTTP-layer * exception is propagated and its management is deferred to the caller. * * @param int $user_id The user ID. * * @return void * * @throws Bad_Request_Exception When the AI service responds with 400. * @throws Forbidden_Exception When the AI service responds with 403. * @throws Internal_Server_Error_Exception When the AI service responds with 500. * @throws Not_Found_Exception When the AI service responds with 404. * @throws Payment_Required_Exception When the AI service responds with 402. * @throws Request_Timeout_Exception When the AI service responds with 408. * @throws Service_Unavailable_Exception When the AI service responds with 503. * @throws Too_Many_Requests_Exception When the AI service responds with 429. * @throws Unauthorized_Exception When the AI service responds with 401. * @throws WP_Request_Exception When the underlying WordPress HTTP call fails. * @throws RuntimeException When the user is not found. */ public function revoke_consent( int $user_id ) { $user = \get_user_by( 'id', $user_id ); if ( ! $user instanceof WP_User ) { // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- false positive. throw new RuntimeException( "User not found: $user_id" ); } // Local consent is always revoked regardless of remote failures. $this->user_helper->delete_meta( $user_id, '_yoast_wpseo_ai_consent' ); try { $jwt = $this->token_manager->get_or_request_access_token( $user ); $this->request_handler->handle( new Request( '/user/consent', [], [ 'Authorization' => "Bearer $jwt" ], Request::METHOD_DELETE ), ); } finally { // Invalidate the JWTs — including ones minted to authenticate the DELETE above — so // credentials never outlive consent. if ( $this->token_manager->has_local_tokens( $user_id ) ) { $this->token_manager->token_invalidate( $user_id ); } } } // phpcs:enable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber }