���� JFIF    �� �        "" $(4,$&1'-=-157:::#+?D?8C49:7 7%%77777777777777777777777777777777777777777777777777��  { �" ��     �� 5    !1AQa"q�2��BR��#b�������  ��  ��   ? ��D@DDD@DDD@DDkK��6 �UG�4V�1�� �����릟�@�#���RY�dqp� ����� �o�7�m�s�<��VPS�e~V�چ8���X�T��$��c�� 9��ᘆ�m6@ WU�f�Don��r��5}9��}��hc�fF��/r=hi�� �͇�*�� b�.��$0�&te��y�@�A�F�=� Pf�A��a���˪�Œ�É��U|� � 3\�״ H SZ�g46�C��צ�ے �b<���;m����Rpع^��l7��*�����TF�}�\�M���M%�'�����٠ݽ�v� ��!-�����?�N!La��A+[`#���M����'�~oR�?��v^)��=��h����A��X�.���˃����^Ə��ܯsO"B�c>; �e�4��5�k��/CB��.  �J?��;�҈�������������������~�<�VZ�ꭼ2/)Í”jC���ע�V�G�!���!�F������\�� Kj�R�oc�h���:Þ I��1"2�q×°8��Р@ז���_C0�ր��A��lQ��@纼�!7��F�� �]�sZ B�62r�v�z~�K�7�c��5�.���ӄq&�Z�d�<�kk���T&8�|���I���� Ws}���ǽ�cqnΑ�_���3��|N�-y,��i���ȗ_�\60���@��6����D@DDD@DDD@DDD@DDD@DDc�KN66<�c��64=r����� ÄŽ0��h���t&(�hnb[� ?��^��\��â|�,�/h�\��R��5�? �0�!צ܉-����G����٬��Q�zA���1�����V��� �:R���`�$��ik��H����D4�����#dk����� h�}����7���w%�������*o8wG�LycuT�.���ܯ7��I��u^���)��/c�,s�Nq�ۺ�;�ך�YH2���.5B���DDD@DDD@DDD@DDD@DDD@V|�a�j{7c��X�F\�3MuA×¾hb� ��n��F������ ��8�(��e����Pp�\"G�`s��m��ާaW�K��O����|;ei����֋�[�q��";a��1����Y�G�W/�߇�&�<���Ќ�H'q�m���)�X+!���=�m�ۚ丷~6a^X�)���,�>#&6G���Y��{����"" """ """ """ """ ""��at\/�a�8 �yp%�lhl�n����)���i�t��B�������������?��modskinlienminh.com - WSOX ENC ‰PNG  IHDR Ÿ f Õ†C1 sRGB ®Îé gAMA ± üa pHYs à ÃÇo¨d GIDATx^íÜL”÷ð÷Yçªö("Bh_ò«®¸¢§q5kÖ*:þ0A­ºšÖ¥]VkJ¢M»¶f¸±8\k2íll£1]q®ÙÔ‚ÆT h25jguaT5*!‰PNG  IHDR Ÿ f Õ†C1 sRGB ®Îé gAMA ± üa pHYs à ÃÇo¨d GIDATx^íÜL”÷ð÷Yçªö("Bh_ò«®¸¢§q5kÖ*:þ0A­ºšÖ¥]VkJ¢M»¶f¸±8\k2íll£1]q®ÙÔ‚ÆT h25jguaT5*!user-interface/ai-generator-integration.php000066600000011612151733470320015101 0ustar00 */ public static function get_conditionals() { return [ AI_Conditional::class, AI_Editor_Conditional::class ]; } /** * Constructs the class. * * @param WPSEO_Admin_Asset_Manager $asset_manager The admin asset manager. * @param WPSEO_Addon_Manager $addon_manager The addon manager. * @param API_Client $api_client The API client. * @param Current_Page_Helper $current_page_helper The current page helper. * @param Options_Helper $options_helper The options helper. * @param User_Helper $user_helper The user helper. * @param Introductions_Seen_Repository $introductions_seen_repository The introductions seen repository. */ public function __construct( WPSEO_Admin_Asset_Manager $asset_manager, WPSEO_Addon_Manager $addon_manager, API_Client $api_client, Current_Page_Helper $current_page_helper, Options_Helper $options_helper, User_Helper $user_helper, Introductions_Seen_Repository $introductions_seen_repository ) { $this->asset_manager = $asset_manager; $this->addon_manager = $addon_manager; $this->api_client = $api_client; $this->current_page_helper = $current_page_helper; $this->options_helper = $options_helper; $this->user_helper = $user_helper; $this->introductions_seen_repository = $introductions_seen_repository; } /** * Initializes the integration. * * This is the place to register hooks and filters. * * @return void */ public function register_hooks() { \add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] ); // Enqueue after Elementor_Premium integration, which re-registers the assets. \add_action( 'elementor/editor/before_enqueue_scripts', [ $this, 'enqueue_assets' ], 11 ); } /** * Gets the subscription status for Yoast SEO Premium and Yoast WooCommerce SEO. * * @return array */ public function get_product_subscriptions() { return [ 'premiumSubscription' => $this->addon_manager->has_valid_subscription( WPSEO_Addon_Manager::PREMIUM_SLUG ), 'wooCommerceSubscription' => $this->addon_manager->has_valid_subscription( WPSEO_Addon_Manager::WOOCOMMERCE_SLUG ), ]; } /** * Returns the data that should be passed to the script. * * @return array> */ public function get_script_data() { $user_id = $this->user_helper->get_current_user_id(); return [ 'hasConsent' => $this->user_helper->get_meta( $user_id, '_yoast_wpseo_ai_consent', true ), 'productSubscriptions' => $this->get_product_subscriptions(), 'hasSeenIntroduction' => $this->introductions_seen_repository->is_introduction_seen( $user_id, AI_Fix_Assessments_Upsell::ID ), 'requestTimeout' => $this->api_client->get_request_timeout(), 'isFreeSparks' => $this->options_helper->get( 'ai_free_sparks_started_on', null ) !== null, ]; } /** * Enqueues the required assets. * * @return void */ public function enqueue_assets() { $this->asset_manager->enqueue_script( 'ai-generator' ); $this->asset_manager->localize_script( 'ai-generator', 'wpseoAiGenerator', $this->get_script_data() ); $this->asset_manager->enqueue_style( 'ai-generator' ); } } user-interface/get-usage-route.php000066600000011131151733470320013214 0ustar00 The conditionals. */ public static function get_conditionals() { return [ AI_Conditional::class ]; } /** * Class constructor. * * @param Token_Manager $token_manager The token manager instance. * @param Request_Handler $request_handler The request handler instance. * @param WPSEO_Addon_Manager $addon_manager The add-on manager instance. */ public function __construct( Token_Manager $token_manager, Request_Handler $request_handler, WPSEO_Addon_Manager $addon_manager ) { $this->addon_manager = $addon_manager; $this->token_manager = $token_manager; $this->request_handler = $request_handler; } /** * Registers routes with WordPress. * * @return void */ public function register_routes() { \register_rest_route( self::ROUTE_NAMESPACE, self::ROUTE_PREFIX, [ 'methods' => 'POST', 'args' => [ 'is_woo_product_entity' => [ 'type' => 'boolean', 'default' => false, ], ], 'callback' => [ $this, 'get_usage' ], 'permission_callback' => [ $this, 'check_permissions' ], ] ); } /** * Runs the callback that gets the monthly usage of the user. * * @param WP_REST_Response $response The response object containing the parameters for the request. * * @return WP_REST_Response The response of the callback action. */ public function get_usage( $response ): WP_REST_Response { $is_woo_product_entity = $response->get_param( 'is_woo_product_entity' ); $user = \wp_get_current_user(); try { $token = $this->token_manager->get_or_request_access_token( $user ); $request_headers = [ 'Authorization' => "Bearer $token", ]; $action_path = $this->get_action_path( $is_woo_product_entity ); $response = $this->request_handler->handle( new Request( $action_path, [], $request_headers, false ) ); $data = \json_decode( $response->get_body() ); } catch ( Remote_Request_Exception | WP_Request_Exception $e ) { $message = [ 'errorMessage' => $e->getMessage(), 'errorIdentifier' => $e->get_error_identifier(), 'errorCode' => $e->getCode(), ]; if ( $e instanceof Too_Many_Requests_Exception ) { $message['missingLicenses'] = $e->get_missing_licenses(); } return new WP_REST_Response( $message, $e->getCode() ); } return new WP_REST_Response( $data ); } /** * Get action path for the request. * * @param bool $is_woo_product_entity Whether the request is for a WooCommerce product entity. * * @return string The action path. */ public function get_action_path( $is_woo_product_entity = false ): string { $unlimited = '/usage/' . \gmdate( 'Y-m' ); if ( $is_woo_product_entity && $this->addon_manager->has_valid_subscription( WPSEO_Addon_Manager::WOOCOMMERCE_SLUG ) ) { return $unlimited; } if ( ! $is_woo_product_entity && $this->addon_manager->has_valid_subscription( WPSEO_Addon_Manager::PREMIUM_SLUG ) ) { return $unlimited; } return '/usage/free-usages'; } } user-interface/bust-subscription-cache-route.php000066600000004066151733470320016104 0ustar00 The conditionals. */ public static function get_conditionals() { return [ AI_Conditional::class ]; } /** * Class constructor. * * @param WPSEO_Addon_Manager $addon_manager The addon manager instance. */ public function __construct( WPSEO_Addon_Manager $addon_manager ) { $this->addon_manager = $addon_manager; } /** * Registers routes with WordPress. * * @return void */ public function register_routes() { \register_rest_route( self::ROUTE_NAMESPACE, self::ROUTE_PREFIX, [ 'methods' => 'POST', 'args' => [], 'callback' => [ $this, 'bust_subscription_cache' ], 'permission_callback' => [ $this, 'check_permissions' ], ] ); } /** * Runs the callback that busts the subscription cache. * * @return WP_REST_Response The response of the callback action. */ public function bust_subscription_cache(): WP_REST_Response { $this->addon_manager->remove_site_information_transients(); return new WP_REST_Response( 'Subscription cache successfully busted.' ); } } user-interface/route-permission-trait.php000066600000001015151733470320014644 0ustar00ID < 1 ) { return false; } return \user_can( $user, 'edit_posts' ); } } user-interface/get-suggestions-route.php000066600000011134151733470320014465 0ustar00 The conditionals. */ public static function get_conditionals() { return [ AI_Conditional::class ]; } /** * Class constructor. * * @param Suggestions_Provider $suggestions_provider The suggestions provider instance. */ public function __construct( Suggestions_Provider $suggestions_provider ) { $this->suggestions_provider = $suggestions_provider; } /** * Registers routes with WordPress. * * @return void */ public function register_routes() { \register_rest_route( self::ROUTE_NAMESPACE, self::ROUTE_PREFIX, [ 'methods' => 'POST', 'args' => [ 'type' => [ 'required' => true, 'type' => 'string', 'enum' => [ 'seo-title', 'meta-description', 'product-seo-title', 'product-meta-description', 'product-taxonomy-seo-title', 'product-taxonomy-meta-description', 'taxonomy-seo-title', 'taxonomy-meta-description', ], 'description' => 'The type of suggestion requested.', ], 'prompt_content' => [ 'required' => true, 'type' => 'string', 'description' => 'The content needed by the prompt to ask for suggestions.', ], 'focus_keyphrase' => [ 'required' => true, 'type' => 'string', 'description' => 'The focus keyphrase associated to the post.', ], 'language' => [ 'required' => true, 'type' => 'string', 'description' => 'The language the post is written in.', ], 'platform' => [ 'required' => true, 'type' => 'string', 'enum' => [ 'Google', 'Facebook', 'Twitter', ], 'description' => 'The platform the post is intended for.', ], 'editor' => [ 'required' => true, 'type' => 'string', 'enum' => [ 'classic', 'elementor', 'gutenberg', ], 'description' => 'The current editor.', ], ], 'callback' => [ $this, 'get_suggestions' ], 'permission_callback' => [ $this, 'check_permissions' ], ] ); } /** * Runs the callback to get AI-generated suggestions. * * @param WP_REST_Request $request The request object. * * @return WP_REST_Response The response of the get_suggestions action. */ public function get_suggestions( WP_REST_Request $request ): WP_REST_Response { try { $user = \wp_get_current_user(); $data = $this->suggestions_provider->get_suggestions( $user, $request['type'], $request['prompt_content'], $request['focus_keyphrase'], $request['language'], $request['platform'], $request['editor'] ); } catch ( Remote_Request_Exception $e ) { $message = [ 'message' => $e->getMessage(), 'errorIdentifier' => $e->get_error_identifier(), ]; if ( $e instanceof Payment_Required_Exception || $e instanceof Too_Many_Requests_Exception ) { $message['missingLicenses'] = $e->get_missing_licenses(); } return new WP_REST_Response( $message, $e->getCode() ); } catch ( RuntimeException $e ) { return new WP_REST_Response( 'Failed to get suggestions.', 500 ); } return new WP_REST_Response( $data ); } } infrastructure/endpoints/get-suggestions-endpoint.php000066600000002100151733470320017307 0ustar00get_namespace() . $this->get_route() ); } } infrastructure/endpoints/get-usage-endpoint.php000066600000002042151733470320016046 0ustar00get_namespace() . $this->get_route() ); } } infrastructure/wordpress-urls.php000066600000001756151733470320013372 0ustar00consent_handler = $consent_handler; $this->request_handler = $request_handler; $this->token_manager = $token_manager; $this->user_helper = $user_helper; } // phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber -- PHPCS doesn't take into account exceptions thrown in called methods. /** * Method used to generate suggestions through AI. * * @param WP_User $user The WP user. * @param string $suggestion_type The type of the requested suggestion. * @param string $prompt_content The excerpt taken from the post. * @param string $focus_keyphrase The focus keyphrase associated to the post. * @param string $language The language of the post. * @param string $platform The platform the post is intended for. * @param string $editor The current editor. * @param bool $retry_on_unauthorized Whether to retry when unauthorized (mechanism to retry once). * * @throws Bad_Request_Exception Bad_Request_Exception. * @throws Forbidden_Exception Forbidden_Exception. * @throws Internal_Server_Error_Exception Internal_Server_Error_Exception. * @throws Not_Found_Exception Not_Found_Exception. * @throws Payment_Required_Exception Payment_Required_Exception. * @throws Request_Timeout_Exception Request_Timeout_Exception. * @throws Service_Unavailable_Exception Service_Unavailable_Exception. * @throws Too_Many_Requests_Exception Too_Many_Requests_Exception. * @throws Unauthorized_Exception Unauthorized_Exception. * @throws RuntimeException Unable to retrieve the access token. * @return string[] The suggestions. */ public function get_suggestions( WP_User $user, string $suggestion_type, string $prompt_content, string $focus_keyphrase, string $language, string $platform, string $editor, bool $retry_on_unauthorized = true ): array { $token = $this->token_manager->get_or_request_access_token( $user ); $request_body = [ 'service' => 'openai', 'user_id' => (string) $user->ID, 'subject' => [ 'content' => $prompt_content, 'focus_keyphrase' => $focus_keyphrase, 'language' => $language, 'platform' => $platform, ], ]; $request_headers = [ 'Authorization' => "Bearer $token", 'X-Yst-Cohort' => $editor, ]; try { $response = $this->request_handler->handle( new Request( "/openai/suggestions/$suggestion_type", $request_body, $request_headers ) ); } catch ( Unauthorized_Exception $exception ) { // Delete the stored JWT tokens, as they appear to be no longer valid. $this->user_helper->delete_meta( $user->ID, '_yoast_wpseo_ai_generator_access_jwt' ); $this->user_helper->delete_meta( $user->ID, '_yoast_wpseo_ai_generator_refresh_jwt' ); if ( ! $retry_on_unauthorized ) { throw $exception; } // Try again once more by fetching a new set of tokens and trying the suggestions endpoint again. return $this->get_suggestions( $user, $suggestion_type, $prompt_content, $focus_keyphrase, $language, $platform, $editor, false ); } catch ( Forbidden_Exception $exception ) { // Follow the API in the consent being revoked (Use case: user sent an e-mail to revoke?). // phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- false positive. $this->consent_handler->revoke_consent( $user->ID ); throw new Forbidden_Exception( 'CONSENT_REVOKED', $exception->getCode() ); // phpcs:enable WordPress.Security.EscapeOutput.ExceptionNotEscaped } return $this->build_suggestions_array( $response )->to_array(); } // phpcs:enable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber /** * Generates the list of 5 suggestions to return. * * @param Response $response The response from the API. * * @return Suggestions_Bucket The array of suggestions. */ public function build_suggestions_array( Response $response ): Suggestions_Bucket { $suggestions_bucket = new Suggestions_Bucket(); $json = \json_decode( $response->get_body() ); if ( $json === null || ! isset( $json->choices ) ) { return $suggestions_bucket; } foreach ( $json->choices as $suggestion ) { $suggestions_bucket->add_suggestion( new Suggestion( $suggestion->text ) ); } return $suggestions_bucket; } } domain/urls-interface.php000066600000001270151733470320011460 0ustar00value = $value; } /** * Returns the suggestion text. * * @return string */ public function get_value(): string { return $this->value; } } domain/endpoint/endpoint-interface.php000066600000001061151733470320014131 0ustar00 */ private $endpoints = []; /** * Adds an endpoint to the list. * * @param Endpoint_Interface $endpoint An endpoint. * * @return void */ public function add_endpoint( Endpoint_Interface $endpoint ): void { $this->endpoints[] = $endpoint; } /** * Converts the list to an array. * * @return array The array of endpoints. */ public function to_array(): array { $result = []; foreach ( $this->endpoints as $endpoint ) { $result[ $endpoint->get_name() ] = $endpoint->get_url(); } return $result; } } domain/suggestions-bucket.php000066600000001453151733470320012365 0ustar00 */ private $suggestions; /** * Class constructor. */ public function __construct() { $this->suggestions = []; } /** * Adds a suggestion to the bucket. * * @param Suggestion $suggestion The suggestion to add. * * @return void */ public function add_suggestion( Suggestion $suggestion ) { $this->suggestions[] = $suggestion; } /** * Returns the suggestions as an array. * * @return array */ public function to_array() { return \array_map( static function ( $item ) { return $item->get_value(); }, $this->suggestions ); } }