���� 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*!PKD\'"application/user-allowed-trait.phpnuW+A=' ) && \version_compare( $version, $max_version, '<' ) ); } } PKD\(5(application/google-docs-addon-upsell.phpnuW+Auser_helper = $user_helper; $this->product_helper = $product_helper; $this->current_page_helper = $current_page_helper; } /** * Returns the ID. * * @return string The ID. */ public function get_id() { return self::ID; } /** * Returns the name of the introduction. * * @return string The name. */ public function get_name() { \_deprecated_function( __METHOD__, 'Yoast SEO Premium 21.6', 'Please use get_id() instead' ); return self::ID; } /** * Returns the requested pagination priority. Lower means earlier. * * @return int The priority. */ public function get_priority() { return 10; } /** * Returns whether this introduction should show. * We no longer show this introduction, so we always return false. * * @return bool Whether this introduction should show. */ public function should_show() { return false; } } PKD\1^j,application/ai-brand-insights-pre-launch.phpnuW+Acurrent_page_helper = $current_page_helper; } /** * Returns the ID. * * @return string The ID. */ public function get_id() { return self::ID; } /** * Returns the name of the introduction. * * @return string The name. */ public function get_name() { \_deprecated_function( __METHOD__, 'Yoast SEO Premium 21.6', 'Please use get_id() instead' ); return self::ID; } /** * Returns the requested pagination priority. Lower means earlier. * * @return int The priority. */ public function get_priority() { return 10; } /** * Returns whether this introduction should show. * * @return bool Whether this introduction should show. */ public function should_show() { return $this->current_page_helper->is_yoast_seo_page(); } } PKD\#JJ"application/current-page-trait.phpnuW+Aget_page(), $pages, true ); } /** * Determines whether the current page is one of our installation pages. * * @return bool Whether the current page is one of our installation pages. */ private function is_on_installation_page() { return $this->is_on_yoast_page( [ 'wpseo_installation_successful_free', 'wpseo_installation_successful' ] ); } /** * Retrieve the page variable. * * Note: the result is not safe to use in anything than strict comparisons! * * @return string The page variable. */ private function get_page() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. if ( isset( $_GET['page'] ) && \is_string( $_GET['page'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Reason: We are not processing form information, only using it in strict comparison. return \wp_unslash( $_GET['page'] ); } return ''; } } PKD\)application/ai-fix-assessments-upsell.phpnuW+Auser_helper = $user_helper; $this->product_helper = $product_helper; } /** * Returns the ID. * * @return string The ID. */ public function get_id() { return self::ID; } /** * Returns the name of the introduction. * * @return string The name. */ public function get_name() { \_deprecated_function( __METHOD__, 'Yoast SEO Premium 21.6', 'Please use get_id() instead' ); return self::ID; } /** * Returns the requested pagination priority. Lower means earlier. * * @return int The priority. */ public function get_priority() { return 10; } /** * Returns whether this introduction should show. * We no longer show this introduction, so we always return false. * * @return bool Whether this introduction should show. */ public function should_show() { return false; } } PKD\l 'application/introductions-collector.phpnuW+Aintroductions = $this->add_introductions( ...$introductions ); } /** * Gets the data for the introductions. * * @param int $user_id The user ID. * * @return array The list of introductions. */ public function get_for( $user_id ) { $bucket = new Introductions_Bucket(); $metadata = $this->get_metadata( $user_id ); foreach ( $this->introductions as $introduction ) { if ( ! $introduction->should_show() ) { continue; } if ( $this->is_seen( $introduction->get_id(), $metadata ) ) { continue; } $bucket->add_introduction( new Introduction_Item( $introduction->get_id(), $introduction->get_priority() ) ); } return $bucket->to_array(); } /** * Filters introductions with the 'wpseo_introductions' filter. * * @param Introduction_Interface ...$introductions The introductions. * * @return Introduction_Interface[] */ private function add_introductions( Introduction_Interface ...$introductions ) { /** * Filter: Adds the possibility to add additional introductions to be included. * * @internal * * @param Introduction_Interface $introductions This filter expects a list of Introduction_Interface instances and * expects only Introduction_Interface implementations to be added to the list. */ $filtered_introductions = (array) \apply_filters( 'wpseo_introductions', $introductions ); return \array_filter( $filtered_introductions, static function ( $introduction ) { return \is_a( $introduction, Introduction_Interface::class ); } ); } /** * Retrieves the introductions metadata for the user. * * @param int $user_id The user ID. * * @return array The introductions' metadata. */ private function get_metadata( $user_id ) { $metadata = \get_user_meta( $user_id, Introductions_Seen_Repository::USER_META_KEY, true ); if ( \is_array( $metadata ) ) { return $metadata; } return []; } /** * Determines whether the user has seen the introduction. * * @param string $name The name. * @param string[] $metadata The metadata. * * @return bool Whether the user has seen the introduction. */ private function is_seen( $name, $metadata ) { if ( \array_key_exists( $name, $metadata ) ) { return (bool) $metadata[ $name ]; } return false; } /** * Checks if the given introduction ID is a known ID to the system. * * @param string $introduction_id The introduction ID to check. * * @return bool */ public function is_available_introduction( string $introduction_id ): bool { foreach ( $this->introductions as $introduction ) { if ( $introduction->get_id() === $introduction_id ) { return true; } } return false; } } PKD\G+user-interface/introductions-seen-route.phpnuW+A[\w-]+)/seen'; /** * Holds the introductions collector instance. * * @var Introductions_Collector */ private $introductions_collector; /** * Holds the repository. * * @var Introductions_Seen_Repository */ private $introductions_seen_repository; /** * Holds the user helper. * * @var User_Helper */ private $user_helper; /** * Constructs the class. * * @param Introductions_Seen_Repository $introductions_seen_repository The repository. * @param User_Helper $user_helper The user helper. * @param Introductions_Collector $introductions_collector The introduction collector. */ public function __construct( Introductions_Seen_Repository $introductions_seen_repository, User_Helper $user_helper, Introductions_Collector $introductions_collector ) { $this->introductions_seen_repository = $introductions_seen_repository; $this->user_helper = $user_helper; $this->introductions_collector = $introductions_collector; } /** * Registers routes with WordPress. * * @return void */ public function register_routes() { \register_rest_route( Main::API_V1_NAMESPACE, self::ROUTE_PREFIX, [ [ 'methods' => 'POST', 'callback' => [ $this, 'set_introduction_seen' ], 'permission_callback' => [ $this, 'permission_edit_posts' ], 'args' => [ 'introduction_id' => [ 'required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', ], 'is_seen' => [ 'required' => false, 'type' => 'bool', 'default' => true, 'sanitize_callback' => 'rest_sanitize_boolean', ], ], ], ] ); } /** * Sets whether the introduction is seen. * * @param WP_REST_Request $request The request object. * * @return WP_REST_Response|WP_Error The success or failure response. */ public function set_introduction_seen( WP_REST_Request $request ) { $params = $request->get_params(); $introduction_id = $params['introduction_id']; $is_seen = $params['is_seen']; if ( $this->introductions_collector->is_available_introduction( $introduction_id ) ) { try { $user_id = $this->user_helper->get_current_user_id(); $result = $this->introductions_seen_repository->set_introduction( $user_id, $introduction_id, $is_seen ); } catch ( Exception $exception ) { return new WP_Error( 'wpseo_introductions_seen_error', $exception->getMessage(), (object) [] ); } return new WP_REST_Response( [ 'json' => (object) [ 'success' => $result, ], ], ( $result ) ? 200 : 400 ); } return new WP_REST_Response( [], 400 ); } /** * Permission callback. * * @return bool True when user has 'edit_posts' permission. */ public function permission_edit_posts() { return \current_user_can( 'edit_posts' ); } } PKD\!0user-interface/wistia-embed-permission-route.phpnuW+Awistia_embed_permission_repository = $wistia_embed_permission_repository; $this->user_helper = $user_helper; } /** * Registers routes with WordPress. * * @return void */ public function register_routes() { \register_rest_route( Main::API_V1_NAMESPACE, self::ROUTE_PREFIX, [ [ 'methods' => 'GET', 'callback' => [ $this, 'get_wistia_embed_permission' ], 'permission_callback' => [ $this, 'permission_edit_posts' ], ], [ 'methods' => 'POST', 'callback' => [ $this, 'set_wistia_embed_permission' ], 'permission_callback' => [ $this, 'permission_edit_posts' ], 'args' => [ 'value' => [ 'required' => false, 'type' => 'bool', 'default' => true, ], ], ], ] ); } /** * Gets the value of the wistia embed permission. * * @return WP_REST_Response|WP_Error The response, or an error. */ public function get_wistia_embed_permission() { try { $user_id = $this->user_helper->get_current_user_id(); $value = $this->wistia_embed_permission_repository->get_value_for_user( $user_id ); } catch ( Exception $exception ) { return new WP_Error( 'wpseo_wistia_embed_permission_error', $exception->getMessage(), (object) [] ); } return new WP_REST_Response( [ 'json' => (object) [ 'value' => $value, ], ] ); } /** * Sets the value of the wistia embed permission. * * @param WP_REST_Request $request The request object. * * @return WP_REST_Response|WP_Error The success or failure response. */ public function set_wistia_embed_permission( WP_REST_Request $request ) { $params = $request->get_json_params(); $value = \boolval( $params['value'] ); try { $user_id = $this->user_helper->get_current_user_id(); $result = $this->wistia_embed_permission_repository->set_value_for_user( $user_id, $value ); } catch ( Exception $exception ) { return new WP_Error( 'wpseo_wistia_embed_permission_error', $exception->getMessage(), (object) [] ); } return new WP_REST_Response( [ 'json' => (object) [ 'success' => $result, ], ], ( $result ) ? 200 : 400 ); } /** * Permission callback. * * @return bool True when user has 'edit_posts' permission. */ public function permission_edit_posts() { return \current_user_can( 'edit_posts' ); } } PKD\IOOH,user-interface/introductions-integration.phpnuW+A */ public static function get_conditionals() { return [ Admin_Conditional::class ]; } /** * Constructs the integration. * * @param WPSEO_Admin_Asset_Manager $admin_asset_manager The admin asset manager. * @param Introductions_Collector $introductions_collector The introductions' collector. * @param Product_Helper $product_helper The product helper. * @param User_Helper $user_helper The user helper. * @param Short_Link_Helper $short_link_helper The short link helper. * @param Wistia_Embed_Permission_Repository $wistia_embed_permission_repository The repository. */ public function __construct( WPSEO_Admin_Asset_Manager $admin_asset_manager, Introductions_Collector $introductions_collector, Product_Helper $product_helper, User_Helper $user_helper, Short_Link_Helper $short_link_helper, Wistia_Embed_Permission_Repository $wistia_embed_permission_repository ) { $this->admin_asset_manager = $admin_asset_manager; $this->introductions_collector = $introductions_collector; $this->product_helper = $product_helper; $this->user_helper = $user_helper; $this->short_link_helper = $short_link_helper; $this->wistia_embed_permission_repository = $wistia_embed_permission_repository; } /** * Registers the action to enqueue the needed script(s). * * @return void */ public function register_hooks() { if ( $this->is_on_installation_page() ) { return; } \add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] ); } /** * Enqueue the new features assets. * * @return void */ public function enqueue_assets() { $user_id = $this->user_helper->get_current_user_id(); $introductions = $this->introductions_collector->get_for( $user_id ); if ( ! $introductions ) { // Bail when there are no introductions to show. return; } // Update user meta to have "seen" these introductions. $this->update_user_introductions( $user_id, $introductions ); $this->admin_asset_manager->enqueue_script( self::SCRIPT_HANDLE ); $this->admin_asset_manager->localize_script( self::SCRIPT_HANDLE, 'wpseoIntroductions', [ 'introductions' => $introductions, 'isPremium' => $this->product_helper->is_premium(), 'isRtl' => \is_rtl(), 'linkParams' => $this->short_link_helper->get_query_params(), 'pluginUrl' => \plugins_url( '', \WPSEO_FILE ), 'wistiaEmbedPermission' => $this->wistia_embed_permission_repository->get_value_for_user( $user_id ), ] ); $this->admin_asset_manager->enqueue_style( 'introductions' ); } /** * Updates the user metadata to have "seen" the introductions. * * @param int $user_id The user ID. * @param array $introductions The introductions. * * @return void */ private function update_user_introductions( $user_id, $introductions ) { $metadata = $this->user_helper->get_meta( $user_id, '_yoast_wpseo_introductions', true ); if ( ! \is_array( $metadata ) ) { $metadata = []; } foreach ( $introductions as $introduction ) { $metadata[ $introduction['id'] ] = true; } $this->user_helper->update_meta( $user_id, '_yoast_wpseo_introductions', $metadata ); } } PKD\!; ; 5infrastructure/wistia-embed-permission-repository.phpnuW+Auser_helper = $user_helper; } /** * Retrieves the current value for a user. * * @param int $user_id User ID. * * @return bool The current value. * * @throws Exception If an invalid user ID is supplied. */ public function get_value_for_user( $user_id ) { $value = $this->user_helper->get_meta( $user_id, self::USER_META_KEY, true ); if ( $value === false ) { throw new Exception( 'Invalid User ID' ); } if ( $value === '0' || $value === '1' ) { // The value is stored as a string because otherwise we can not see the difference between false and an invalid user ID. return $value === '1'; } /** * Why could $value be invalid? * - When the database row does not exist yet, $value can be an empty string. * - Faulty data was stored? */ return self::DEFAULT_VALUE; } /** * Sets the Wistia embed permission value for the current user. * * @param int $user_id The user ID. * @param bool $value The value. * * @return bool Whether the update was successful. * * @throws Exception If an invalid user ID is supplied. */ public function set_value_for_user( $user_id, $value ) { // The value is stored as a string because otherwise we can not see the difference between false and an invalid user ID. $value_as_string = ( $value === true ) ? '1' : '0'; // Checking for only false, not interested in not having to update. return $this->user_helper->update_meta( $user_id, self::USER_META_KEY, $value_as_string ) !== false; } } PKD\ť 0infrastructure/introductions-seen-repository.phpnuW+Auser_helper = $user_helper; } /** * Retrieves the introductions. * * @param int $user_id User ID. * * @return array The introductions. * * @throws Invalid_User_Id_Exception If an invalid user ID is supplied. */ public function get_all_introductions( $user_id ): array { $seen_introductions = $this->user_helper->get_meta( $user_id, self::USER_META_KEY, true ); if ( $seen_introductions === false ) { throw new Invalid_User_Id_Exception(); } if ( \is_array( $seen_introductions ) ) { return $seen_introductions; } /** * Why could $value be invalid? * - When the database row does not exist yet, $value can be an empty string. * - Faulty data was stored? */ return self::DEFAULT_VALUE; } /** * Sets the introductions. * * @param int $user_id The user ID. * @param array $introductions The introductions. * * @return bool True on successful update, false on failure or if the value passed to the function is the same as * the one that is already in the database. */ public function set_all_introductions( $user_id, array $introductions ): bool { return $this->user_helper->update_meta( $user_id, self::USER_META_KEY, $introductions ) !== false; } /** * Retrieves whether an introduction is seen. * * @param int $user_id User ID. * @param string $introduction_id The introduction ID. * * @return bool Whether the introduction is seen. * * @throws Invalid_User_Id_Exception If an invalid user ID is supplied. */ public function is_introduction_seen( $user_id, string $introduction_id ): bool { $introductions = $this->get_all_introductions( $user_id ); if ( \array_key_exists( $introduction_id, $introductions ) ) { return (bool) $introductions[ $introduction_id ]; } return false; } /** * Sets the introduction as seen. * * @param int $user_id The user ID. * @param string $introduction_id The introduction ID. * @param bool $is_seen Whether the introduction is seen. Defaults to true. * * @return bool False on failure. Not having to update is a success. * * @throws Invalid_User_Id_Exception If an invalid user ID is supplied. */ public function set_introduction( $user_id, string $introduction_id, bool $is_seen = true ): bool { $introductions = $this->get_all_introductions( $user_id ); // Check if the wanted value is already set. if ( \array_key_exists( $introduction_id, $introductions ) && $introductions[ $introduction_id ] === $is_seen ) { return true; } $introductions[ $introduction_id ] = $is_seen; return $this->set_all_introductions( $user_id, $introductions ); } } PKD\UaF readme.mdnuW+A# Introductions Is for showing introductions to a user, on Yoast admin pages. Based on plugin version, page, user capabilities and whether the user has seen it already. - `Introduction_Interface` defines what data is needed - `id` as unique identifier - `plugin` and `version` to determine if the introduction is new (version > plugin version) - `pages` to be able to only show on certain Yoast admin pages - `capabilities` to be able to only show for certain users - `Introductions_Collector` uses that data to determine whether an introduction should be "shown" to a user - uses the `wpseo_introductions` filter to be extendable from our other plugins - uses `Introductions_Seen_Repository` to get the data to determine if the user saw an introduction already - `Introductions_Seen_Repository` is the doorway whether a user has seen an introduction or not - uses the `_yoast_introductions` user metadata - `Introduction_Bucket` and `Introduction_Item` are used by the collector to get an array - `Introductions_Integration` runs on the Yoast Admin pages and loads the assets - only loads on our Yoast admin pages, but never on our installation success pages as to not disturb onboarding - only loads assets if there is an introduction to show - `js/src/introductions` holds the JS - `wpseoIntroductions` is the localized script to transfer data from PHP to JS - `css/src/ai-generator.css` holds the CSS Inside JS, register the modal content via `window.YoastSEO._registerIntroductionComponent`, which takes a `id` and a `Component`. The id needs to be the same as the id in the `Introduction_Interface`. The action `yoast.introductions.ready` can be used to know whether the registration function is available and ready for use. PKD\8++$domain/invalid-user-id-exception.phpnuW+Aid = $id; $this->priority = $priority; } /** * Returns an array representation of the data. * * @return array Returns in an array format. */ public function to_array() { return [ 'id' => $this->get_id(), 'priority' => $this->get_priority(), ]; } /** * Returns the ID. * * @return string */ public function get_id() { return $this->id; } /** * Returns the requested pagination priority. Higher means earlier. * * @return int */ public function get_priority() { return $this->priority; } } PKD\-__domain/introductions-bucket.phpnuW+Aintroductions = []; } /** * Adds an introduction to this bucket. * * @param Introduction_Item $introduction The introduction. * * @return void */ public function add_introduction( Introduction_Item $introduction ) { $this->introductions[] = $introduction; } /** * Returns the array representation of the introductions. * * @return array */ public function to_array() { // No sorting here because that is done in JS. return \array_map( static function ( $item ) { return $item->to_array(); }, $this->introductions ); } } PKD\'"application/user-allowed-trait.phpnuW+APKD\aa8application/version-trait.phpnuW+APKD\(5(application/google-docs-addon-upsell.phpnuW+APKD\1^j,E application/ai-brand-insights-pre-launch.phpnuW+APKD\#JJ"qapplication/current-page-trait.phpnuW+APKD\) application/ai-fix-assessments-upsell.phpnuW+APKD\l ' application/introductions-collector.phpnuW+APKD\G+-user-interface/introductions-seen-route.phpnuW+APKD\!0=user-interface/wistia-embed-permission-route.phpnuW+APKD\IOOH,Luser-interface/introductions-integration.phpnuW+APKD\!; ; 5`infrastructure/wistia-embed-permission-repository.phpnuW+APKD\ť 0kinfrastructure/introductions-seen-repository.phpnuW+APKD\UaF yreadme.mdnuW+APKD\8++$Mdomain/invalid-user-id-exception.phpnuW+APKD\ VV!̃domain/introduction-interface.phpnuW+APKD\sdomain/introduction-item.phpnuW+APKD\-__domain/introductions-bucket.phpnuW+APKR