user_helper = $user_helper; $this->ai_request_sender_factory = $ai_request_sender_factory; $this->token_manager = $token_manager; } // 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 Consent_Required_Exception When the AI service responds with a 403 indicating consent is required. * @throws Insufficient_Scope_Exception When the AI service responds with a 403 insufficient_scope. * @throws Forbidden_Exception When the AI service responds with any other 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" ); } $this->ai_request_sender_factory->create( $user )->grant_consent( $user ); $this->user_helper->update_meta( $user_id, '_yoast_wpseo_ai_consent', true ); } /** * Revokes the user's consent on the Yoast AI service and clears the local user meta. * * 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 legacy * JWTs are then invalidated regardless of the remote outcome — credentials must not outlive * consent. The invalidation runs after the DELETE on purpose: the legacy Token path may mint * a fresh JWT to authenticate the DELETE, 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 Consent_Required_Exception When the AI service responds with a 403 indicating consent is required. * @throws Insufficient_Scope_Exception When the AI service responds with a 403 insufficient_scope. * @throws Forbidden_Exception When the AI service responds with any other 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 { $this->ai_request_sender_factory->create( $user )->revoke_consent( $user ); } finally { // Invalidate the legacy JWTs — including ones minted to authenticate the DELETE above — // so credentials never outlive consent. Skipped when no local JWTs exist (the OAuth path // without a leftover pre-OAuth grant). if ( $this->token_manager->has_local_tokens( $user_id ) ) { $this->token_manager->token_invalidate( $user_id ); } } } // phpcs:enable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber }