���� 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*!semrush/semrush-phrases-action.php000066600000004441151733344100013357 0ustar00client = $client; } /** * Gets the related keyphrases and data based on the passed keyphrase and database country code. * * @param string $keyphrase The keyphrase to search for. * @param string $database The database's country code. * * @return object The response object. */ public function get_related_keyphrases( $keyphrase, $database ) { try { $transient_key = \sprintf( static::TRANSIENT_CACHE_KEY, $keyphrase, $database ); $transient = \get_transient( $transient_key ); if ( $transient !== false && isset( $transient['data']['columnNames'] ) && \count( $transient['data']['columnNames'] ) === 5 ) { return $this->to_result_object( $transient ); } $options = [ 'params' => [ 'phrase' => $keyphrase, 'database' => $database, 'export_columns' => 'Ph,Nq,Td,In,Kd', 'display_limit' => 10, 'display_offset' => 0, 'display_sort' => 'nq_desc', 'display_filter' => '%2B|Nq|Lt|1000', ], ]; $results = $this->client->get( self::KEYPHRASES_URL, $options ); \set_transient( $transient_key, $results, \DAY_IN_SECONDS ); return $this->to_result_object( $results ); } catch ( Exception $e ) { return (object) [ 'error' => $e->getMessage(), 'status' => $e->getCode(), ]; } } /** * Converts the passed dataset to an object. * * @param array $result The result dataset to convert to an object. * * @return object The result object. */ protected function to_result_object( $result ) { return (object) [ 'results' => $result['data'], 'status' => $result['status'], ]; } } semrush/semrush-login-action.php000066600000002360151733344100013020 0ustar00client = $client; } /** * Authenticates with SEMrush to request the necessary tokens. * * @param string $code The authentication code to use to request a token with. * * @return object The response object. */ public function authenticate( $code ) { // Code has already been validated at this point. No need to do that again. try { $tokens = $this->client->request_tokens( $code ); return (object) [ 'tokens' => $tokens->to_array(), 'status' => 200, ]; } catch ( Authentication_Failed_Exception $e ) { return $e->get_response(); } } /** * Performs the login request, if necessary. * * @return void */ public function login() { if ( $this->client->has_valid_tokens() ) { return; } // Prompt with login screen. } } semrush/semrush-options-action.php000066600000002140151733344100013377 0ustar00options_helper = $options_helper; } /** * Stores SEMrush country code in the WPSEO options. * * @param string $country_code The country code to store. * * @return object The response object. */ public function set_country_code( $country_code ) { // The country code has already been validated at this point. No need to do that again. $success = $this->options_helper->set( 'semrush_country_code', $country_code ); if ( $success ) { return (object) [ 'success' => true, 'status' => 200, ]; } return (object) [ 'success' => false, 'status' => 500, 'error' => 'Could not save option in the database', ]; } } alert-dismissal-action.php000066600000012576151733344100011645 0ustar00user = $user; } /** * Dismisses an alert. * * @param string $alert_identifier Alert identifier. * * @return bool Whether the dismiss was successful or not. */ public function dismiss( $alert_identifier ) { $user_id = $this->user->get_current_user_id(); if ( $user_id === 0 ) { return false; } if ( $this->is_allowed( $alert_identifier ) === false ) { return false; } $dismissed_alerts = $this->get_dismissed_alerts( $user_id ); if ( $dismissed_alerts === false ) { return false; } if ( \array_key_exists( $alert_identifier, $dismissed_alerts ) === true ) { // The alert is already dismissed. return true; } // Add this alert to the dismissed alerts. $dismissed_alerts[ $alert_identifier ] = true; // Save. return $this->user->update_meta( $user_id, static::USER_META_KEY, $dismissed_alerts ) !== false; } /** * Resets an alert. * * @param string $alert_identifier Alert identifier. * * @return bool Whether the reset was successful or not. */ public function reset( $alert_identifier ) { $user_id = $this->user->get_current_user_id(); if ( $user_id === 0 ) { return false; } if ( $this->is_allowed( $alert_identifier ) === false ) { return false; } $dismissed_alerts = $this->get_dismissed_alerts( $user_id ); if ( $dismissed_alerts === false ) { return false; } $amount_of_dismissed_alerts = \count( $dismissed_alerts ); if ( $amount_of_dismissed_alerts === 0 ) { // No alerts: nothing to reset. return true; } if ( \array_key_exists( $alert_identifier, $dismissed_alerts ) === false ) { // Alert not found: nothing to reset. return true; } if ( $amount_of_dismissed_alerts === 1 ) { // The 1 remaining dismissed alert is the alert to reset: delete the alerts user meta row. return $this->user->delete_meta( $user_id, static::USER_META_KEY, $dismissed_alerts ); } // Remove this alert from the dismissed alerts. unset( $dismissed_alerts[ $alert_identifier ] ); // Save. return $this->user->update_meta( $user_id, static::USER_META_KEY, $dismissed_alerts ) !== false; } /** * Returns if an alert is dismissed or not. * * @param string $alert_identifier Alert identifier. * * @return bool Whether the alert has been dismissed. */ public function is_dismissed( $alert_identifier ) { $user_id = $this->user->get_current_user_id(); if ( $user_id === 0 ) { return false; } if ( $this->is_allowed( $alert_identifier ) === false ) { return false; } $dismissed_alerts = $this->get_dismissed_alerts( $user_id ); if ( $dismissed_alerts === false ) { return false; } return \array_key_exists( $alert_identifier, $dismissed_alerts ); } /** * Returns an object with all alerts dismissed by current user. * * @return array|false An array with the keys of all Alerts that have been dismissed * by the current user or `false`. */ public function all_dismissed() { $user_id = $this->user->get_current_user_id(); if ( $user_id === 0 ) { return false; } $dismissed_alerts = $this->get_dismissed_alerts( $user_id ); if ( $dismissed_alerts === false ) { return false; } return $dismissed_alerts; } /** * Returns if an alert is allowed or not. * * @param string $alert_identifier Alert identifier. * * @return bool Whether the alert is allowed. */ public function is_allowed( $alert_identifier ) { return \in_array( $alert_identifier, $this->get_allowed_dismissable_alerts(), true ); } /** * Retrieves the dismissed alerts. * * @param int $user_id User ID. * * @return string[]|false The dismissed alerts. False for an invalid $user_id. */ protected function get_dismissed_alerts( $user_id ) { $dismissed_alerts = $this->user->get_meta( $user_id, static::USER_META_KEY, true ); if ( $dismissed_alerts === false ) { // Invalid user ID. return false; } if ( $dismissed_alerts === '' ) { /* * When no database row exists yet, an empty string is returned because of the `single` parameter. * We do want a single result returned, but the default should be an empty array instead. */ return []; } return $dismissed_alerts; } /** * Retrieves the allowed dismissable alerts. * * @return string[] The allowed dismissable alerts. */ protected function get_allowed_dismissable_alerts() { /** * Filter: 'wpseo_allowed_dismissable_alerts' - List of allowed dismissable alerts. * * @param string[] $allowed_dismissable_alerts Allowed dismissable alerts list. */ $allowed_dismissable_alerts = \apply_filters( 'wpseo_allowed_dismissable_alerts', [] ); if ( \is_array( $allowed_dismissable_alerts ) === false ) { return []; } // Only allow strings. $allowed_dismissable_alerts = \array_filter( $allowed_dismissable_alerts, 'is_string' ); // Filter unique and reorder indices. $allowed_dismissable_alerts = \array_values( \array_unique( $allowed_dismissable_alerts ) ); return $allowed_dismissable_alerts; } } importing/importing-indexation-action-interface.php000066600000001445151733344100016657 0ustar00wpdb = $wpdb; $this->options = $options; $this->post_importing_action = $post_importing_action; $this->settings_importing_actions = [ $custom_archive_action, $default_archive_action, $general_settings_action, $posttype_defaults_settings_action, $taxonomy_settings_action, ]; } /** * Just checks if the action has been completed in the past. * * @return int 1 if it hasn't been completed in the past, 0 if it has. */ public function get_total_unindexed() { return ( ! $this->get_completed() ) ? 1 : 0; } /** * Just checks if the action has been completed in the past. * * @param int $limit The maximum number of unimported objects to be returned. Not used, exists to comply with the interface. * * @return int 1 if it hasn't been completed in the past, 0 if it has. */ public function get_limited_unindexed_count( $limit ) { return ( ! $this->get_completed() ) ? 1 : 0; } /** * Validates AIOSEO data. * * @return array An array of validated data or false if aioseo data did not pass validation. * * @throws Aioseo_Validation_Exception If the validation fails. */ public function index() { if ( $this->get_completed() ) { return []; } $validated_aioseo_table = $this->validate_aioseo_table(); $validated_aioseo_settings = $this->validate_aioseo_settings(); $validated_robot_settings = $this->validate_robot_settings(); if ( $validated_aioseo_table === false || $validated_aioseo_settings === false || $validated_robot_settings === false ) { throw new Aioseo_Validation_Exception(); } $this->set_completed( true ); return [ 'validated_aioseo_table' => $validated_aioseo_table, 'validated_aioseo_settings' => $validated_aioseo_settings, 'validated_robot_settings' => $validated_robot_settings, ]; } /** * Validates the AIOSEO indexable table. * * @return bool Whether the AIOSEO table exists and has the structure we expect. */ public function validate_aioseo_table() { if ( ! $this->aioseo_helper->aioseo_exists() ) { return false; } $table = $this->aioseo_helper->get_table(); $needed_data = $this->post_importing_action->get_needed_data(); $aioseo_columns = $this->wpdb->get_col( "SHOW COLUMNS FROM {$table}", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: There is no unescaped user input. 0 ); return $needed_data === \array_intersect( $needed_data, $aioseo_columns ); } /** * Validates the AIOSEO settings from the options table. * * @return bool Whether the AIOSEO settings from the options table exist and have the structure we expect. */ public function validate_aioseo_settings() { foreach ( $this->settings_importing_actions as $settings_import_action ) { $aioseo_settings = \json_decode( \get_option( $settings_import_action->get_source_option_name(), '' ), true ); if ( ! $settings_import_action->isset_settings_tab( $aioseo_settings ) ) { return false; } } return true; } /** * Validates the AIOSEO robots settings from the options table. * * @return bool Whether the AIOSEO robots settings from the options table exist and have the structure we expect. */ public function validate_robot_settings() { if ( $this->validate_post_robot_settings() && $this->validate_default_robot_settings() ) { return true; } return false; } /** * Validates the post AIOSEO robots settings from the options table. * * @return bool Whether the post AIOSEO robots settings from the options table exist and have the structure we expect. */ public function validate_post_robot_settings() { $post_robot_mapping = $this->post_importing_action->enhance_mapping(); // We're gonna validate against posttype robot settings only for posts, assuming the robot settings stay the same for other post types. $post_robot_mapping['subtype'] = 'post'; // Let's get both the aioseo_options and the aioseo_options_dynamic options. $aioseo_global_settings = $this->aioseo_helper->get_global_option(); $aioseo_posts_settings = \json_decode( \get_option( $post_robot_mapping['option_name'], '' ), true ); $needed_robots_data = $this->post_importing_action->get_needed_robot_data(); \array_push( $needed_robots_data, 'default', 'noindex' ); foreach ( $needed_robots_data as $robot_setting ) { // Validate against global settings. if ( ! isset( $aioseo_global_settings['searchAppearance']['advanced']['globalRobotsMeta'][ $robot_setting ] ) ) { return false; } // Validate against posttype settings. if ( ! isset( $aioseo_posts_settings['searchAppearance'][ $post_robot_mapping['type'] ][ $post_robot_mapping['subtype'] ]['advanced']['robotsMeta'][ $robot_setting ] ) ) { return false; } } return true; } /** * Validates the default AIOSEO robots settings for search appearance settings from the options table. * * @return bool Whether the AIOSEO robots settings for search appearance settings from the options table exist and have the structure we expect. */ public function validate_default_robot_settings() { foreach ( $this->settings_importing_actions as $settings_import_action ) { $robot_setting_map = $settings_import_action->pluck_robot_setting_from_mapping(); // Some actions return empty robot settings, let's not validate against those. if ( ! empty( $robot_setting_map ) ) { $aioseo_settings = \json_decode( \get_option( $robot_setting_map['option_name'], '' ), true ); if ( ! isset( $aioseo_settings['searchAppearance'][ $robot_setting_map['type'] ][ $robot_setting_map['subtype'] ]['advanced']['robotsMeta']['default'] ) ) { return false; } } } return true; } /** * Used nowhere. Exists to comply with the interface. * * @return int The limit. */ public function get_limit() { /** * Filter 'wpseo_aioseo_cleanup_limit' - Allow filtering the number of validations during each action pass. * * @param int $limit The maximum number of validations. */ $limit = \apply_filters( 'wpseo_aioseo_validation_limit', 25 ); if ( ! \is_int( $limit ) || $limit < 1 ) { $limit = 25; } return $limit; } } importing/aioseo/abstract-aioseo-settings-importing-action.php000066600000024610151733344100020753 0ustar00import_helper = $import_helper; } /** * Retrieves the source option_name. * * @return string The source option_name. * * @throws Exception If the SOURCE_OPTION_NAME constant is not set in the child class. */ public function get_source_option_name() { $source_option_name = static::SOURCE_OPTION_NAME; if ( empty( $source_option_name ) ) { throw new Exception( 'Importing settings action without explicit source option_name' ); } return $source_option_name; } /** * Returns the total number of unimported objects. * * @return int The total number of unimported objects. */ public function get_total_unindexed() { return $this->get_unindexed_count(); } /** * Returns the limited number of unimported objects. * * @param int $limit The maximum number of unimported objects to be returned. * * @return int The limited number of unindexed posts. */ public function get_limited_unindexed_count( $limit ) { return $this->get_unindexed_count( $limit ); } /** * Returns the number of unimported objects (limited if limit is applied). * * @param int|null $limit The maximum number of unimported objects to be returned. * * @return int The number of unindexed posts. */ protected function get_unindexed_count( $limit = null ) { if ( ! \is_int( $limit ) || $limit < 1 ) { $limit = null; } $settings_to_create = $this->query( $limit ); $number_of_settings_to_create = \count( $settings_to_create ); $completed = $number_of_settings_to_create === 0; $this->set_completed( $completed ); return $number_of_settings_to_create; } /** * Imports AIOSEO settings. * * @return array|false An array of the AIOSEO settings that were imported or false if aioseo data was not found. */ public function index() { $limit = $this->get_limit(); $aioseo_settings = $this->query( $limit ); $created_settings = []; $completed = \count( $aioseo_settings ) === 0; $this->set_completed( $completed ); // Prepare the setting keys mapping. $this->build_mapping(); // Prepare the replacement var mapping. foreach ( $this->replace_vars_edited_map as $aioseo_var => $yoast_var ) { $this->replacevar_handler->compose_map( $aioseo_var, $yoast_var ); } $last_imported_setting = ''; try { foreach ( $aioseo_settings as $setting => $setting_value ) { // Map and import the values of the setting we're working with (eg. post, book-category, etc.) to the respective Yoast option. $this->map( $setting_value, $setting ); // Save the type of the settings that were just imported, so that we can allow chunked imports. $last_imported_setting = $setting; $created_settings[] = $setting; } } finally { $cursor_id = $this->get_cursor_id(); $this->import_cursor->set_cursor( $cursor_id, $last_imported_setting ); } return $created_settings; } /** * Checks if the settings tab subsetting is set in the AIOSEO option. * * @param string $aioseo_settings The AIOSEO option. * * @return bool Whether the settings are set. */ public function isset_settings_tab( $aioseo_settings ) { return isset( $aioseo_settings['searchAppearance'][ $this->settings_tab ] ); } /** * Queries the database and retrieves unimported AiOSEO settings (in chunks if a limit is applied). * * @param int|null $limit The maximum number of unimported objects to be returned. * * @return array The (maybe chunked) unimported AiOSEO settings to import. */ protected function query( $limit = null ) { $aioseo_settings = \json_decode( \get_option( $this->get_source_option_name(), '' ), true ); if ( empty( $aioseo_settings ) ) { return []; } // We specifically want the setttings of the tab we're working with, eg. postTypes, taxonomies, etc. $settings_values = $aioseo_settings['searchAppearance'][ $this->settings_tab ]; if ( ! \is_array( $settings_values ) ) { return []; } $flattened_settings = $this->import_helper->flatten_settings( $settings_values ); return $this->get_unimported_chunk( $flattened_settings, $limit ); } /** * Retrieves (a chunk of, if limit is applied) the unimported AIOSEO settings. * To apply a chunk, we manipulate the cursor to the keys of the AIOSEO settings. * * @param array $importable_data All of the available AIOSEO settings. * @param int $limit The maximum number of unimported objects to be returned. * * @return array The (chunk of, if limit is applied)) unimported AIOSEO settings. */ protected function get_unimported_chunk( $importable_data, $limit ) { \ksort( $importable_data ); $cursor_id = $this->get_cursor_id(); $cursor = $this->import_cursor->get_cursor( $cursor_id, '' ); /** * Filter 'wpseo_aioseo__import_cursor' - Allow filtering the value of the aioseo settings import cursor. * * @param int $import_cursor The value of the aioseo posttype default settings import cursor. */ $cursor = \apply_filters( 'wpseo_aioseo_' . $this->get_type() . '_import_cursor', $cursor ); if ( $cursor === '' ) { return \array_slice( $importable_data, 0, $limit, true ); } // Let's find the position of the cursor in the alphabetically sorted importable data, so we can return only the unimported data. $keys = \array_flip( \array_keys( $importable_data ) ); // If the stored cursor now no longer exists in the data, we have no choice but to start over. $position = ( isset( $keys[ $cursor ] ) ) ? ( $keys[ $cursor ] + 1 ) : 0; return \array_slice( $importable_data, $position, $limit, true ); } /** * Returns the number of objects that will be imported in a single importing pass. * * @return int The limit. */ public function get_limit() { /** * Filter 'wpseo_aioseo__indexation_limit' - Allow filtering the number of settings imported during each importing pass. * * @param int $max_posts The maximum number of posts indexed. */ $limit = \apply_filters( 'wpseo_aioseo_' . $this->get_type() . '_indexation_limit', 25 ); if ( ! \is_int( $limit ) || $limit < 1 ) { $limit = 25; } return $limit; } /** * Maps/imports AIOSEO settings into the respective Yoast settings. * * @param string|array $setting_value The value of the AIOSEO setting at hand. * @param string $setting The setting at hand, eg. post or movie-category, separator etc. * * @return void */ protected function map( $setting_value, $setting ) { $aioseo_options_to_yoast_map = $this->aioseo_options_to_yoast_map; if ( isset( $aioseo_options_to_yoast_map[ $setting ] ) ) { $this->import_single_setting( $setting, $setting_value, $aioseo_options_to_yoast_map[ $setting ] ); } } /** * Imports a single setting in the db after transforming it to adhere to Yoast conventions. * * @param string $setting The name of the setting. * @param string $setting_value The values of the setting. * @param array $setting_mapping The mapping of the setting to Yoast formats. * * @return void */ protected function import_single_setting( $setting, $setting_value, $setting_mapping ) { $yoast_key = $setting_mapping['yoast_name']; // Check if we're supposed to save the setting. if ( $this->options->get_default( 'wpseo_titles', $yoast_key ) !== null ) { // Then, do any needed data transfomation before actually saving the incoming data. $transformed_data = \call_user_func( [ $this, $setting_mapping['transform_method'] ], $setting_value, $setting_mapping ); $this->options->set( $yoast_key, $transformed_data ); } } /** * Minimally transforms boolean data to be imported. * * @param bool $meta_data The boolean meta data to be imported. * * @return bool The transformed boolean meta data. */ public function simple_boolean_import( $meta_data ) { return $meta_data; } /** * Imports the noindex setting, taking into consideration whether they defer to global defaults. * * @param bool $noindex The noindex of the type, without taking into consideration whether the type defers to global defaults. * @param array $mapping The mapping of the setting we're working with. * * @return bool The noindex setting. */ public function import_noindex( $noindex, $mapping ) { return $this->robots_transformer->transform_robot_setting( 'noindex', $noindex, $mapping ); } /** * Returns a setting map of the robot setting for one subset of post types/taxonomies/archives. * For custom archives, it returns an empty array because AIOSEO excludes some custom archives from this option structure, eg. WooCommerce's products and we don't want to raise a false alarm. * * @return array The setting map of the robot setting for one subset of post types/taxonomies/archives or an empty array. */ public function pluck_robot_setting_from_mapping() { return []; } } importing/aioseo/aioseo-posttype-defaults-settings-importing-action.php000066600000005540151733344100022645 0ustar00 true ], 'objects' ); foreach ( $post_type_objects as $pt ) { // Use all the custom post types that are public. $this->aioseo_options_to_yoast_map[ '/' . $pt->name . '/title' ] = [ 'yoast_name' => 'title-' . $pt->name, 'transform_method' => 'simple_import', ]; $this->aioseo_options_to_yoast_map[ '/' . $pt->name . '/metaDescription' ] = [ 'yoast_name' => 'metadesc-' . $pt->name, 'transform_method' => 'simple_import', ]; $this->aioseo_options_to_yoast_map[ '/' . $pt->name . '/advanced/showMetaBox' ] = [ 'yoast_name' => 'display-metabox-pt-' . $pt->name, 'transform_method' => 'simple_boolean_import', ]; $this->aioseo_options_to_yoast_map[ '/' . $pt->name . '/advanced/robotsMeta/noindex' ] = [ 'yoast_name' => 'noindex-' . $pt->name, 'transform_method' => 'import_noindex', 'type' => 'postTypes', 'subtype' => $pt->name, 'option_name' => 'aioseo_options_dynamic', ]; if ( $pt->name === 'attachment' ) { $this->aioseo_options_to_yoast_map['/attachment/redirectAttachmentUrls'] = [ 'yoast_name' => 'disable-attachment', 'transform_method' => 'import_redirect_attachment', ]; } } } /** * Transforms the redirect_attachment setting. * * @param string $redirect_attachment The redirect_attachment setting. * * @return bool The transformed redirect_attachment setting. */ public function import_redirect_attachment( $redirect_attachment ) { switch ( $redirect_attachment ) { case 'disabled': return false; case 'attachment': case 'attachment_parent': default: return true; } } } importing/aioseo/aioseo-general-settings-importing-action.php000066600000013752151733344100020572 0ustar00image = $image; } /** * Builds the mapping that ties AOISEO option keys with Yoast ones and their data transformation method. * * @return void */ protected function build_mapping() { $this->aioseo_options_to_yoast_map = [ '/separator' => [ 'yoast_name' => 'separator', 'transform_method' => 'transform_separator', ], '/siteTitle' => [ 'yoast_name' => 'title-home-wpseo', 'transform_method' => 'simple_import', ], '/metaDescription' => [ 'yoast_name' => 'metadesc-home-wpseo', 'transform_method' => 'simple_import', ], '/schema/siteRepresents' => [ 'yoast_name' => 'company_or_person', 'transform_method' => 'transform_site_represents', ], '/schema/person' => [ 'yoast_name' => 'company_or_person_user_id', 'transform_method' => 'simple_import', ], '/schema/organizationName' => [ 'yoast_name' => 'company_name', 'transform_method' => 'simple_import', ], '/schema/organizationLogo' => [ 'yoast_name' => 'company_logo', 'transform_method' => 'import_company_logo', ], '/schema/personLogo' => [ 'yoast_name' => 'person_logo', 'transform_method' => 'import_person_logo', ], ]; } /** * Imports the organization logo while also accounting for the id of the log to be saved in the separate Yoast option. * * @param string $logo_url The company logo url coming from AIOSEO settings. * * @return string The transformed company logo url. */ public function import_company_logo( $logo_url ) { $logo_id = $this->image->get_attachment_by_url( $logo_url ); $this->options->set( 'company_logo_id', $logo_id ); $this->options->set( 'company_logo_meta', false ); $logo_meta = $this->image->get_attachment_meta_from_settings( 'company_logo' ); $this->options->set( 'company_logo_meta', $logo_meta ); return $this->url_import( $logo_url ); } /** * Imports the person logo while also accounting for the id of the log to be saved in the separate Yoast option. * * @param string $logo_url The person logo url coming from AIOSEO settings. * * @return string The transformed person logo url. */ public function import_person_logo( $logo_url ) { $logo_id = $this->image->get_attachment_by_url( $logo_url ); $this->options->set( 'person_logo_id', $logo_id ); $this->options->set( 'person_logo_meta', false ); $logo_meta = $this->image->get_attachment_meta_from_settings( 'person_logo' ); $this->options->set( 'person_logo_meta', $logo_meta ); return $this->url_import( $logo_url ); } /** * Transforms the site represents setting. * * @param string $site_represents The site represents setting. * * @return string The transformed site represents setting. */ public function transform_site_represents( $site_represents ) { switch ( $site_represents ) { case 'person': return 'person'; case 'organization': default: return 'company'; } } /** * Transforms the separator setting. * * @param string $separator The separator setting. * * @return string The transformed separator. */ public function transform_separator( $separator ) { switch ( $separator ) { case '-': return 'sc-dash'; case '–': return 'sc-ndash'; case '—': return 'sc-mdash'; case '»': return 'sc-raquo'; case '«': return 'sc-laquo'; case '>': return 'sc-gt'; case '•': return 'sc-bull'; case '|': return 'sc-pipe'; default: return 'sc-dash'; } } } importing/aioseo/aioseo-posts-importing-action.php000066600000055143151733344100016467 0ustar00>> */ protected $aioseo_to_yoast_map = [ 'title' => [ 'yoast_name' => 'title', 'transform_method' => 'simple_import_post', ], 'description' => [ 'yoast_name' => 'description', 'transform_method' => 'simple_import_post', ], 'og_title' => [ 'yoast_name' => 'open_graph_title', 'transform_method' => 'simple_import_post', ], 'og_description' => [ 'yoast_name' => 'open_graph_description', 'transform_method' => 'simple_import_post', ], 'twitter_title' => [ 'yoast_name' => 'twitter_title', 'transform_method' => 'simple_import_post', 'twitter_import' => true, ], 'twitter_description' => [ 'yoast_name' => 'twitter_description', 'transform_method' => 'simple_import_post', 'twitter_import' => true, ], 'canonical_url' => [ 'yoast_name' => 'canonical', 'transform_method' => 'url_import_post', ], 'keyphrases' => [ 'yoast_name' => 'primary_focus_keyword', 'transform_method' => 'keyphrase_import', ], 'og_image_url' => [ 'yoast_name' => 'open_graph_image', 'social_image_import' => true, 'social_setting_prefix_aioseo' => 'og_', 'social_setting_prefix_yoast' => 'open_graph_', 'transform_method' => 'social_image_url_import', ], 'twitter_image_url' => [ 'yoast_name' => 'twitter_image', 'social_image_import' => true, 'social_setting_prefix_aioseo' => 'twitter_', 'social_setting_prefix_yoast' => 'twitter_', 'transform_method' => 'social_image_url_import', ], 'robots_noindex' => [ 'yoast_name' => 'is_robots_noindex', 'transform_method' => 'post_robots_noindex_import', 'robots_import' => true, ], 'robots_nofollow' => [ 'yoast_name' => 'is_robots_nofollow', 'transform_method' => 'post_general_robots_import', 'robots_import' => true, 'robot_type' => 'nofollow', ], 'robots_noarchive' => [ 'yoast_name' => 'is_robots_noarchive', 'transform_method' => 'post_general_robots_import', 'robots_import' => true, 'robot_type' => 'noarchive', ], 'robots_nosnippet' => [ 'yoast_name' => 'is_robots_nosnippet', 'transform_method' => 'post_general_robots_import', 'robots_import' => true, 'robot_type' => 'nosnippet', ], 'robots_noimageindex' => [ 'yoast_name' => 'is_robots_noimageindex', 'transform_method' => 'post_general_robots_import', 'robots_import' => true, 'robot_type' => 'noimageindex', ], ]; /** * Represents the indexables repository. * * @var Indexable_Repository */ protected $indexable_repository; /** * The WordPress database instance. * * @var wpdb */ protected $wpdb; /** * The image helper. * * @var Image_Helper */ protected $image; /** * The indexable_to_postmeta helper. * * @var Indexable_To_Postmeta_Helper */ protected $indexable_to_postmeta; /** * The indexable helper. * * @var Indexable_Helper */ protected $indexable_helper; /** * The social images provider service. * * @var Aioseo_Social_Images_Provider_Service */ protected $social_images_provider; /** * Class constructor. * * @param Indexable_Repository $indexable_repository The indexables repository. * @param wpdb $wpdb The WordPress database instance. * @param Import_Cursor_Helper $import_cursor The import cursor helper. * @param Indexable_Helper $indexable_helper The indexable helper. * @param Indexable_To_Postmeta_Helper $indexable_to_postmeta The indexable_to_postmeta helper. * @param Options_Helper $options The options helper. * @param Image_Helper $image The image helper. * @param Sanitization_Helper $sanitization The sanitization helper. * @param Aioseo_Replacevar_Service $replacevar_handler The replacevar handler. * @param Aioseo_Robots_Provider_Service $robots_provider The robots provider service. * @param Aioseo_Robots_Transformer_Service $robots_transformer The robots transfomer service. * @param Aioseo_Social_Images_Provider_Service $social_images_provider The social images provider service. */ public function __construct( Indexable_Repository $indexable_repository, wpdb $wpdb, Import_Cursor_Helper $import_cursor, Indexable_Helper $indexable_helper, Indexable_To_Postmeta_Helper $indexable_to_postmeta, Options_Helper $options, Image_Helper $image, Sanitization_Helper $sanitization, Aioseo_Replacevar_Service $replacevar_handler, Aioseo_Robots_Provider_Service $robots_provider, Aioseo_Robots_Transformer_Service $robots_transformer, Aioseo_Social_Images_Provider_Service $social_images_provider ) { parent::__construct( $import_cursor, $options, $sanitization, $replacevar_handler, $robots_provider, $robots_transformer ); $this->indexable_repository = $indexable_repository; $this->wpdb = $wpdb; $this->image = $image; $this->indexable_helper = $indexable_helper; $this->indexable_to_postmeta = $indexable_to_postmeta; $this->social_images_provider = $social_images_provider; } // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- Reason: They are already prepared. /** * Returns the total number of unimported objects. * * @return int The total number of unimported objects. */ public function get_total_unindexed() { if ( ! $this->aioseo_helper->aioseo_exists() ) { return 0; } $limit = false; $just_detect = true; $indexables_to_create = $this->wpdb->get_col( $this->query( $limit, $just_detect ) ); $number_of_indexables_to_create = \count( $indexables_to_create ); $completed = $number_of_indexables_to_create === 0; $this->set_completed( $completed ); return $number_of_indexables_to_create; } /** * Returns the limited number of unimported objects. * * @param int $limit The maximum number of unimported objects to be returned. * * @return int|false The limited number of unindexed posts. False if the query fails. */ public function get_limited_unindexed_count( $limit ) { if ( ! $this->aioseo_helper->aioseo_exists() ) { return 0; } $just_detect = true; $indexables_to_create = $this->wpdb->get_col( $this->query( $limit, $just_detect ) ); $number_of_indexables_to_create = \count( $indexables_to_create ); $completed = $number_of_indexables_to_create === 0; $this->set_completed( $completed ); return $number_of_indexables_to_create; } /** * Imports AIOSEO meta data and creates the respective Yoast indexables and postmeta. * * @return Indexable[]|false An array of created indexables or false if aioseo data was not found. */ public function index() { if ( ! $this->aioseo_helper->aioseo_exists() ) { return false; } $limit = $this->get_limit(); $aioseo_indexables = $this->wpdb->get_results( $this->query( $limit ), \ARRAY_A ); $created_indexables = []; $completed = \count( $aioseo_indexables ) === 0; $this->set_completed( $completed ); // Let's build the list of fields to check their defaults, to identify whether we're gonna import AIOSEO data in the indexable or not. $check_defaults_fields = []; foreach ( $this->aioseo_to_yoast_map as $yoast_mapping ) { // We don't want to check all the imported fields. if ( ! \in_array( $yoast_mapping['yoast_name'], [ 'open_graph_image', 'twitter_image' ], true ) ) { $check_defaults_fields[] = $yoast_mapping['yoast_name']; } } $last_indexed_aioseo_id = 0; foreach ( $aioseo_indexables as $aioseo_indexable ) { $last_indexed_aioseo_id = $aioseo_indexable['id']; $indexable = $this->indexable_repository->find_by_id_and_type( $aioseo_indexable['post_id'], 'post' ); // Let's ensure that the current post id represents something that we want to index (eg. *not* shop_order). if ( ! \is_a( $indexable, 'Yoast\WP\SEO\Models\Indexable' ) ) { continue; } if ( $this->indexable_helper->check_if_default_indexable( $indexable, $check_defaults_fields ) ) { $indexable = $this->map( $indexable, $aioseo_indexable ); $this->indexable_helper->save_indexable( $indexable ); // To ensure that indexables can be rebuild after a reset, we have to store the data in the postmeta table too. $this->indexable_to_postmeta->map_to_postmeta( $indexable ); } $last_indexed_aioseo_id = $aioseo_indexable['id']; $created_indexables[] = $indexable; } $cursor_id = $this->get_cursor_id(); $this->import_cursor->set_cursor( $cursor_id, $last_indexed_aioseo_id ); return $created_indexables; } // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared /** * Maps AIOSEO meta data to Yoast meta data. * * @param Indexable $indexable The Yoast indexable. * @param array $aioseo_indexable The AIOSEO indexable. * * @return Indexable The created indexables. */ public function map( $indexable, $aioseo_indexable ) { foreach ( $this->aioseo_to_yoast_map as $aioseo_key => $yoast_mapping ) { // For robots import. if ( isset( $yoast_mapping['robots_import'] ) && $yoast_mapping['robots_import'] ) { $yoast_mapping['subtype'] = $indexable->object_sub_type; $indexable->{$yoast_mapping['yoast_name']} = $this->transform_import_data( $yoast_mapping['transform_method'], $aioseo_indexable, $aioseo_key, $yoast_mapping, $indexable ); continue; } // For social images, like open graph and twitter image. if ( isset( $yoast_mapping['social_image_import'] ) && $yoast_mapping['social_image_import'] ) { $image_url = $this->transform_import_data( $yoast_mapping['transform_method'], $aioseo_indexable, $aioseo_key, $yoast_mapping, $indexable ); // Update the indexable's social image only where there's actually a url to import, so as not to lose the social images that we came up with when we originally built the indexable. if ( ! empty( $image_url ) ) { $indexable->{$yoast_mapping['yoast_name']} = $image_url; $image_source_key = $yoast_mapping['social_setting_prefix_yoast'] . 'image_source'; $indexable->$image_source_key = 'imported'; $image_id_key = $yoast_mapping['social_setting_prefix_yoast'] . 'image_id'; $indexable->$image_id_key = $this->image->get_attachment_by_url( $image_url ); if ( $yoast_mapping['yoast_name'] === 'open_graph_image' ) { $indexable->open_graph_image_meta = null; } } continue; } // For twitter import, take the respective open graph data if the appropriate setting is enabled. if ( isset( $yoast_mapping['twitter_import'] ) && $yoast_mapping['twitter_import'] && $aioseo_indexable['twitter_use_og'] ) { $aioseo_indexable['twitter_title'] = $aioseo_indexable['og_title']; $aioseo_indexable['twitter_description'] = $aioseo_indexable['og_description']; } if ( ! empty( $aioseo_indexable[ $aioseo_key ] ) ) { $indexable->{$yoast_mapping['yoast_name']} = $this->transform_import_data( $yoast_mapping['transform_method'], $aioseo_indexable, $aioseo_key, $yoast_mapping, $indexable ); } } return $indexable; } /** * Transforms the data to be imported. * * @param string $transform_method The method that is going to be used for transforming the data. * @param array $aioseo_indexable The data of the AIOSEO indexable data that is being imported. * @param string $aioseo_key The name of the specific set of data that is going to be transformed. * @param array $yoast_mapping Extra details for the import of the specific data that is going to be transformed. * @param Indexable $indexable The Yoast indexable that we are going to import the transformed data into. * * @return string|bool|null The transformed data to be imported. */ protected function transform_import_data( $transform_method, $aioseo_indexable, $aioseo_key, $yoast_mapping, $indexable ) { return \call_user_func( [ $this, $transform_method ], $aioseo_indexable, $aioseo_key, $yoast_mapping, $indexable ); } /** * Returns the number of objects that will be imported in a single importing pass. * * @return int The limit. */ public function get_limit() { /** * Filter 'wpseo_aioseo_post_indexation_limit' - Allow filtering the number of posts indexed during each indexing pass. * * @param int $max_posts The maximum number of posts indexed. */ $limit = \apply_filters( 'wpseo_aioseo_post_indexation_limit', 25 ); if ( ! \is_int( $limit ) || $limit < 1 ) { $limit = 25; } return $limit; } /** * Populates the needed data array based on which columns we use from the AIOSEO indexable table. * * @return array The needed data array that contains all the needed columns. */ public function get_needed_data() { $needed_data = \array_keys( $this->aioseo_to_yoast_map ); \array_push( $needed_data, 'id', 'post_id', 'robots_default', 'og_image_custom_url', 'og_image_type', 'twitter_image_custom_url', 'twitter_image_type', 'twitter_use_og' ); return $needed_data; } /** * Populates the needed robot data array to be used in validating against its structure. * * @return array The needed data array that contains all the needed columns. */ public function get_needed_robot_data() { $needed_robot_data = []; foreach ( $this->aioseo_to_yoast_map as $yoast_mapping ) { if ( isset( $yoast_mapping['robot_type'] ) ) { $needed_robot_data[] = $yoast_mapping['robot_type']; } } return $needed_robot_data; } /** * Creates a query for gathering AiOSEO data from the database. * * @param int|false $limit The maximum number of unimported objects to be returned. * False for "no limit". * @param bool $just_detect Whether we want to just detect if there are unimported objects. If false, we want to actually import them too. * * @return string The query to use for importing or counting the number of items to import. */ public function query( $limit = false, $just_detect = false ) { $table = $this->aioseo_helper->get_table(); $select_statement = 'id'; if ( ! $just_detect ) { // If we want to import too, we need the actual needed data from AIOSEO indexables. $needed_data = $this->get_needed_data(); $select_statement = \implode( ', ', $needed_data ); } $cursor_id = $this->get_cursor_id(); $cursor = $this->import_cursor->get_cursor( $cursor_id ); /** * Filter 'wpseo_aioseo_post_cursor' - Allow filtering the value of the aioseo post import cursor. * * @param int $import_cursor The value of the aioseo post import cursor. */ $cursor = \apply_filters( 'wpseo_aioseo_post_import_cursor', $cursor ); $replacements = [ $cursor ]; $limit_statement = ''; if ( ! empty( $limit ) ) { $replacements[] = $limit; $limit_statement = ' LIMIT %d'; } // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: There is no unescaped user input. return $this->wpdb->prepare( "SELECT {$select_statement} FROM {$table} WHERE id > %d ORDER BY id{$limit_statement}", $replacements ); // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared } /** * Minimally transforms data to be imported. * * @param array $aioseo_data All of the AIOSEO data to be imported. * @param string $aioseo_key The AIOSEO key that contains the setting we're working with. * * @return string The transformed meta data. */ public function simple_import_post( $aioseo_data, $aioseo_key ) { return $this->simple_import( $aioseo_data[ $aioseo_key ] ); } /** * Transforms URL to be imported. * * @param array $aioseo_data All of the AIOSEO data to be imported. * @param string $aioseo_key The AIOSEO key that contains the setting we're working with. * * @return string The transformed URL. */ public function url_import_post( $aioseo_data, $aioseo_key ) { return $this->url_import( $aioseo_data[ $aioseo_key ] ); } /** * Plucks the keyphrase to be imported from the AIOSEO array of keyphrase meta data. * * @param array $aioseo_data All of the AIOSEO data to be imported. * @param string $aioseo_key The AIOSEO key that contains the setting we're working with, aka keyphrases. * * @return string|null The plucked keyphrase. */ public function keyphrase_import( $aioseo_data, $aioseo_key ) { $meta_data = \json_decode( $aioseo_data[ $aioseo_key ], true ); if ( ! isset( $meta_data['focus']['keyphrase'] ) ) { return null; } return $this->sanitization->sanitize_text_field( $meta_data['focus']['keyphrase'] ); } /** * Imports the post's noindex setting. * * @param bool $aioseo_robots_settings AIOSEO's set of robot settings for the post. * * @return bool|null The value of Yoast's noindex setting for the post. */ public function post_robots_noindex_import( $aioseo_robots_settings ) { // If robot settings defer to default settings, we have null in the is_robots_noindex field. if ( $aioseo_robots_settings['robots_default'] ) { return null; } return $aioseo_robots_settings['robots_noindex']; } /** * Imports the post's robots setting. * * @param bool $aioseo_robots_settings AIOSEO's set of robot settings for the post. * @param string $aioseo_key The AIOSEO key that contains the robot setting we're working with. * @param array $mapping The mapping of the setting we're working with. * * @return bool|null The value of Yoast's noindex setting for the post. */ public function post_general_robots_import( $aioseo_robots_settings, $aioseo_key, $mapping ) { $mapping = $this->enhance_mapping( $mapping ); if ( $aioseo_robots_settings['robots_default'] ) { // Let's first get the subtype's setting value and then transform it taking into consideration whether it defers to global defaults. $subtype_setting = $this->robots_provider->get_subtype_robot_setting( $mapping ); return $this->robots_transformer->transform_robot_setting( $mapping['robot_type'], $subtype_setting, $mapping ); } return $aioseo_robots_settings[ $aioseo_key ]; } /** * Enhances the mapping of the setting we're working with, with type and the option name, so that we can retrieve the settings for the object we're working with. * * @param array $mapping The mapping of the setting we're working with. * * @return array The enhanced mapping. */ public function enhance_mapping( $mapping = [] ) { $mapping['type'] = 'postTypes'; $mapping['option_name'] = 'aioseo_options_dynamic'; return $mapping; } /** * Imports the og and twitter image url. * * @param bool $aioseo_social_image_settings AIOSEO's set of social image settings for the post. * @param string $aioseo_key The AIOSEO key that contains the robot setting we're working with. * @param array $mapping The mapping of the setting we're working with. * @param Indexable $indexable The Yoast indexable we're importing into. * * @return bool|null The url of the social image we're importing, null if there's none. */ public function social_image_url_import( $aioseo_social_image_settings, $aioseo_key, $mapping, $indexable ) { if ( $mapping['social_setting_prefix_aioseo'] === 'twitter_' && $aioseo_social_image_settings['twitter_use_og'] ) { $mapping['social_setting_prefix_aioseo'] = 'og_'; } $social_setting = \rtrim( $mapping['social_setting_prefix_aioseo'], '_' ); $image_type = $aioseo_social_image_settings[ $mapping['social_setting_prefix_aioseo'] . 'image_type' ]; if ( $image_type === 'default' ) { $image_type = $this->social_images_provider->get_default_social_image_source( $social_setting ); } switch ( $image_type ) { case 'attach': $image_url = $this->social_images_provider->get_first_attached_image( $indexable->object_id ); break; case 'auto': if ( $this->social_images_provider->get_featured_image( $indexable->object_id ) ) { // If there's a featured image, lets not import it, as our indexable calculation has already set that as active social image. That way we achieve dynamicality. return null; } $image_url = $this->social_images_provider->get_auto_image( $indexable->object_id ); break; case 'content': $image_url = $this->social_images_provider->get_first_image_in_content( $indexable->object_id ); break; case 'custom_image': $image_url = $aioseo_social_image_settings[ $mapping['social_setting_prefix_aioseo'] . 'image_custom_url' ]; break; case 'featured': return null; // Our auto-calculation when the indexable was built/updated has taken care of it, so it's not needed to transfer any data now. case 'author': return null; case 'custom': return null; case 'default': $image_url = $this->social_images_provider->get_default_custom_social_image( $social_setting ); break; default: $image_url = $aioseo_social_image_settings[ $mapping['social_setting_prefix_aioseo'] . 'image_url' ]; break; } if ( empty( $image_url ) ) { $image_url = $this->social_images_provider->get_default_custom_social_image( $social_setting ); } if ( empty( $image_url ) ) { return null; } return $this->sanitization->sanitize_url( $image_url, null ); } } importing/aioseo/aioseo-cleanup-action.php000066600000011025151733344100014727 0ustar00 */ protected $aioseo_postmeta_keys = [ '_aioseo_title', '_aioseo_description', '_aioseo_og_title', '_aioseo_og_description', '_aioseo_twitter_title', '_aioseo_twitter_description', ]; /** * The WordPress database instance. * * @var wpdb */ protected $wpdb; /** * Class constructor. * * @param wpdb $wpdb The WordPress database instance. * @param Options_Helper $options The options helper. */ public function __construct( wpdb $wpdb, Options_Helper $options ) { $this->wpdb = $wpdb; $this->options = $options; } /** * Retrieves the postmeta along with the db prefix. * * @return string The postmeta table name along with the db prefix. */ protected function get_postmeta_table() { return $this->wpdb->prefix . 'postmeta'; } /** * Just checks if the cleanup has been completed in the past. * * @return int The total number of unimported objects. */ public function get_total_unindexed() { if ( ! $this->aioseo_helper->aioseo_exists() ) { return 0; } return ( ! $this->get_completed() ) ? 1 : 0; } /** * Just checks if the cleanup has been completed in the past. * * @param int $limit The maximum number of unimported objects to be returned. * * @return int|false The limited number of unindexed posts. False if the query fails. */ public function get_limited_unindexed_count( $limit ) { if ( ! $this->aioseo_helper->aioseo_exists() ) { return 0; } return ( ! $this->get_completed() ) ? 1 : 0; } /** * Cleans up AIOSEO data. * * @return Indexable[]|false An array of created indexables or false if aioseo data was not found. */ public function index() { if ( $this->get_completed() ) { return []; } // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- Reason: There is no unescaped user input. $meta_data = $this->wpdb->query( $this->cleanup_postmeta_query() ); $aioseo_table_truncate_done = $this->wpdb->query( $this->truncate_query() ); // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared if ( $meta_data === false && $aioseo_table_truncate_done === false ) { return false; } $this->set_completed( true ); return [ 'metadata_cleanup' => $meta_data, 'indexables_cleanup' => $aioseo_table_truncate_done, ]; } /** * Creates a DELETE query string for deleting AIOSEO postmeta data. * * @return string The query to use for importing or counting the number of items to import. */ public function cleanup_postmeta_query() { $table = $this->get_postmeta_table(); $meta_keys_to_delete = $this->aioseo_postmeta_keys; // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: There is no unescaped user input. return $this->wpdb->prepare( "DELETE FROM {$table} WHERE meta_key IN (" . \implode( ', ', \array_fill( 0, \count( $meta_keys_to_delete ), '%s' ) ) . ')', $meta_keys_to_delete ); // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared } /** * Creates a TRUNCATE query string for emptying the AIOSEO indexable table, if it exists. * * @return string The query to use for importing or counting the number of items to import. */ public function truncate_query() { if ( ! $this->aioseo_helper->aioseo_exists() ) { // If the table doesn't exist, we need a string that will amount to a quick query that doesn't return false when ran. return 'SELECT 1'; } $table = $this->aioseo_helper->get_table(); return "TRUNCATE TABLE {$table}"; } /** * Used nowhere. Exists to comply with the interface. * * @return int The limit. */ public function get_limit() { /** * Filter 'wpseo_aioseo_cleanup_limit' - Allow filtering the number of posts indexed during each indexing pass. * * @param int $max_posts The maximum number of posts cleaned up. */ $limit = \apply_filters( 'wpseo_aioseo_cleanup_limit', 25 ); if ( ! \is_int( $limit ) || $limit < 1 ) { $limit = 25; } return $limit; } } importing/aioseo/aioseo-default-archive-settings-importing-action.php000066600000005610151733344100022212 0ustar00aioseo_options_to_yoast_map = [ '/author/title' => [ 'yoast_name' => 'title-author-wpseo', 'transform_method' => 'simple_import', ], '/author/metaDescription' => [ 'yoast_name' => 'metadesc-author-wpseo', 'transform_method' => 'simple_import', ], '/date/title' => [ 'yoast_name' => 'title-archive-wpseo', 'transform_method' => 'simple_import', ], '/date/metaDescription' => [ 'yoast_name' => 'metadesc-archive-wpseo', 'transform_method' => 'simple_import', ], '/search/title' => [ 'yoast_name' => 'title-search-wpseo', 'transform_method' => 'simple_import', ], '/author/advanced/robotsMeta/noindex' => [ 'yoast_name' => 'noindex-author-wpseo', 'transform_method' => 'import_noindex', 'type' => 'archives', 'subtype' => 'author', 'option_name' => 'aioseo_options', ], '/date/advanced/robotsMeta/noindex' => [ 'yoast_name' => 'noindex-archive-wpseo', 'transform_method' => 'import_noindex', 'type' => 'archives', 'subtype' => 'date', 'option_name' => 'aioseo_options', ], ]; } /** * Returns a setting map of the robot setting for author archives. * * @return array The setting map of the robot setting for author archives. */ public function pluck_robot_setting_from_mapping() { $this->build_mapping(); foreach ( $this->aioseo_options_to_yoast_map as $setting ) { // Return the first archive setting map. if ( $setting['transform_method'] === 'import_noindex' && isset( $setting['subtype'] ) && $setting['subtype'] === 'author' ) { return $setting; } } return []; } } importing/aioseo/aioseo-taxonomy-settings-importing-action.php000066600000007645151733344100021037 0ustar00 '', // Empty string, as AIOSEO shows nothing for that tag. '#breadcrumb_archive_post_type_format' => '', // Empty string, as AIOSEO shows nothing for that tag. '#breadcrumb_archive_post_type_name' => '', // Empty string, as AIOSEO shows nothing for that tag. '#breadcrumb_author_display_name' => '', // Empty string, as AIOSEO shows nothing for that tag. '#breadcrumb_author_first_name' => '', // Empty string, as AIOSEO shows nothing for that tag. '#breadcrumb_blog_page_title' => '', // Empty string, as AIOSEO shows nothing for that tag. '#breadcrumb_label' => '', // Empty string, as AIOSEO shows nothing for that tag. '#breadcrumb_link' => '', // Empty string, as AIOSEO shows nothing for that tag. '#breadcrumb_search_result_format' => '', // Empty string, as AIOSEO shows nothing for that tag. '#breadcrumb_search_string' => '', // Empty string, as AIOSEO shows nothing for that tag. '#breadcrumb_separator' => '', // Empty string, as AIOSEO shows nothing for that tag. '#breadcrumb_taxonomy_title' => '', // Empty string, as AIOSEO shows nothing for that tag. '#taxonomy_title' => '%%term_title%%', ]; /** * Builds the mapping that ties AOISEO option keys with Yoast ones and their data transformation method. * * @return void */ protected function build_mapping() { $taxonomy_objects = \get_taxonomies( [ 'public' => true ], 'object' ); foreach ( $taxonomy_objects as $tax ) { // Use all the public taxonomies. $this->aioseo_options_to_yoast_map[ '/' . $tax->name . '/title' ] = [ 'yoast_name' => 'title-tax-' . $tax->name, 'transform_method' => 'simple_import', ]; $this->aioseo_options_to_yoast_map[ '/' . $tax->name . '/metaDescription' ] = [ 'yoast_name' => 'metadesc-tax-' . $tax->name, 'transform_method' => 'simple_import', ]; $this->aioseo_options_to_yoast_map[ '/' . $tax->name . '/advanced/robotsMeta/noindex' ] = [ 'yoast_name' => 'noindex-tax-' . $tax->name, 'transform_method' => 'import_noindex', 'type' => 'taxonomies', 'subtype' => $tax->name, 'option_name' => 'aioseo_options_dynamic', ]; } } /** * Returns a setting map of the robot setting for post category taxonomies. * * @return array The setting map of the robot setting for post category taxonomies. */ public function pluck_robot_setting_from_mapping() { $this->build_mapping(); foreach ( $this->aioseo_options_to_yoast_map as $setting ) { // Return the first archive setting map. if ( $setting['transform_method'] === 'import_noindex' && isset( $setting['subtype'] ) && $setting['subtype'] === 'category' ) { return $setting; } } return []; } } importing/aioseo/aioseo-custom-archive-settings-importing-action.php000066600000007366151733344100022112 0ustar00post_type = $post_type; } /** * Builds the mapping that ties AOISEO option keys with Yoast ones and their data transformation method. * * @return void */ protected function build_mapping() { $post_type_objects = \get_post_types( [ 'public' => true ], 'objects' ); foreach ( $post_type_objects as $pt ) { // Use all the custom post types that have archives. if ( ! $pt->_builtin && $this->post_type->has_archive( $pt ) ) { $this->aioseo_options_to_yoast_map[ '/' . $pt->name . '/title' ] = [ 'yoast_name' => 'title-ptarchive-' . $pt->name, 'transform_method' => 'simple_import', ]; $this->aioseo_options_to_yoast_map[ '/' . $pt->name . '/metaDescription' ] = [ 'yoast_name' => 'metadesc-ptarchive-' . $pt->name, 'transform_method' => 'simple_import', ]; $this->aioseo_options_to_yoast_map[ '/' . $pt->name . '/advanced/robotsMeta/noindex' ] = [ 'yoast_name' => 'noindex-ptarchive-' . $pt->name, 'transform_method' => 'import_noindex', 'type' => 'archives', 'subtype' => $pt->name, 'option_name' => 'aioseo_options_dynamic', ]; } } } } importing/deactivate-conflicting-plugins-action.php000066600000010417151733344100016635 0ustar00conflicting_plugins = $conflicting_plugins_service; $this->detected_plugins = []; } /** * Get the total number of conflicting plugins. * * @return int */ public function get_total_unindexed() { return \count( $this->get_detected_plugins() ); } /** * Returns whether the updated importer framework is enabled. * * @return bool True if the updated importer framework is enabled. */ public function is_enabled() { $updated_importer_framework_conditional = \YoastSEO()->classes->get( Updated_Importer_Framework_Conditional::class ); return $updated_importer_framework_conditional->is_met(); } /** * Deactivate conflicting plugins. * * @return array */ public function index() { $detected_plugins = $this->get_detected_plugins(); $this->conflicting_plugins->deactivate_conflicting_plugins( $detected_plugins ); // We need to conform to the interface, so we report that no indexables were created. return []; } /** * {@inheritDoc} */ public function get_limit() { return \count( Conflicting_Plugins::all_plugins() ); } /** * Returns the total number of unindexed objects up to a limit. * * @param int $limit The maximum. * * @return int The total number of unindexed objects. */ public function get_limited_unindexed_count( $limit ) { $count = \count( $this->get_detected_plugins() ); return ( $count <= $limit ) ? $count : $limit; } /** * Returns all detected plugins. * * @return array The detected plugins. */ protected function get_detected_plugins() { // The active plugins won't change much. We can reuse the result for the duration of the request. if ( \count( $this->detected_plugins ) < 1 ) { $this->detected_plugins = $this->conflicting_plugins->detect_conflicting_plugins(); } return $this->detected_plugins; } } wincher/wincher-account-action.php000066600000004744151733344100013276 0ustar00client = $client; $this->options_helper = $options_helper; } /** * Checks the account limit for tracking keyphrases. * * @return object The response object. */ public function check_limit() { // Code has already been validated at this point. No need to do that again. try { $results = $this->client->get( self::ACCOUNT_URL ); $usage = $results['limits']['keywords']['usage']; $limit = $results['limits']['keywords']['limit']; $history = $results['limits']['history_days']; return (object) [ 'canTrack' => ( $limit === null || $usage < $limit ), 'limit' => $limit, 'usage' => $usage, 'historyDays' => $history, 'status' => 200, ]; } catch ( Exception $e ) { return (object) [ 'status' => $e->getCode(), 'error' => $e->getMessage(), ]; } } /** * Gets the upgrade campaign. * * @return object The response object. */ public function get_upgrade_campaign() { try { $result = $this->client->get( self::UPGRADE_CAMPAIGN_URL ); $type = ( $result['type'] ?? null ); $months = ( $result['months'] ?? null ); $discount = ( $result['value'] ?? null ); // We display upgrade discount only if it's a rate discount and positive months/discount. if ( $type === 'RATE' && $months && $discount ) { return (object) [ 'discount' => $discount, 'months' => $months, 'status' => 200, ]; } return (object) [ 'discount' => null, 'months' => null, 'status' => 200, ]; } catch ( Exception $e ) { return (object) [ 'status' => $e->getCode(), 'error' => $e->getMessage(), ]; } } } wincher/wincher-keyphrases-action.php000066600000022224151733344100014011 0ustar00client = $client; $this->options_helper = $options_helper; $this->indexable_repository = $indexable_repository; } /** * Sends the tracking API request for one or more keyphrases. * * @param string|array $keyphrases One or more keyphrases that should be tracked. * @param Object $limits The limits API call response data. * * @return Object The reponse object. */ public function track_keyphrases( $keyphrases, $limits ) { try { $endpoint = \sprintf( self::KEYPHRASES_ADD_URL, $this->options_helper->get( 'wincher_website_id' ) ); // Enforce arrrays to ensure a consistent way of preparing the request. if ( ! \is_array( $keyphrases ) ) { $keyphrases = [ $keyphrases ]; } // Calculate if the user would exceed their limit. // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- To ensure JS code style, this can be ignored. if ( ! $limits->canTrack || $this->would_exceed_limits( $keyphrases, $limits ) ) { $response = [ 'limit' => $limits->limit, 'error' => 'Account limit exceeded', 'status' => 400, ]; return $this->to_result_object( $response ); } $formatted_keyphrases = \array_values( \array_map( static function ( $keyphrase ) { return [ 'keyword' => $keyphrase, 'groups' => [], ]; }, $keyphrases ) ); $results = $this->client->post( $endpoint, WPSEO_Utils::format_json_encode( $formatted_keyphrases ) ); if ( ! \array_key_exists( 'data', $results ) ) { return $this->to_result_object( $results ); } // The endpoint returns a lot of stuff that we don't want/need. $results['data'] = \array_map( static function ( $keyphrase ) { return [ 'id' => $keyphrase['id'], 'keyword' => $keyphrase['keyword'], ]; }, $results['data'] ); $results['data'] = \array_combine( \array_column( $results['data'], 'keyword' ), \array_values( $results['data'] ) ); return $this->to_result_object( $results ); } catch ( Exception $e ) { return (object) [ 'error' => $e->getMessage(), 'status' => $e->getCode(), ]; } } /** * Sends an untrack request for the passed keyword ID. * * @param int $keyphrase_id The ID of the keyphrase to untrack. * * @return object The response object. */ public function untrack_keyphrase( $keyphrase_id ) { try { $endpoint = \sprintf( self::KEYPHRASE_DELETE_URL, $this->options_helper->get( 'wincher_website_id' ), $keyphrase_id ); $this->client->delete( $endpoint ); return (object) [ 'status' => 200, ]; } catch ( Exception $e ) { return (object) [ 'error' => $e->getMessage(), 'status' => $e->getCode(), ]; } } /** * Gets the keyphrase data for the passed keyphrases. * Retrieves all available data if no keyphrases are provided. * * @param array|null $used_keyphrases The currently used keyphrases. Optional. * @param string|null $permalink The current permalink. Optional. * @param string|null $start_at The position start date. Optional. * * @return object The keyphrase chart data. */ public function get_tracked_keyphrases( $used_keyphrases = null, $permalink = null, $start_at = null ) { try { if ( $used_keyphrases === null ) { $used_keyphrases = $this->collect_all_keyphrases(); } // If we still have no keyphrases the API will return an error, so // don't even bother sending a request. if ( empty( $used_keyphrases ) ) { return $this->to_result_object( [ 'data' => [], 'status' => 200, ] ); } $endpoint = \sprintf( self::KEYPHRASES_URL, $this->options_helper->get( 'wincher_website_id' ) ); $results = $this->client->post( $endpoint, WPSEO_Utils::format_json_encode( [ 'keywords' => $used_keyphrases, 'url' => $permalink, 'start_at' => $start_at, ] ), [ 'timeout' => 60, ] ); if ( ! \array_key_exists( 'data', $results ) ) { return $this->to_result_object( $results ); } $results['data'] = $this->filter_results_by_used_keyphrases( $results['data'], $used_keyphrases ); // Extract the positional data and assign it to the keyphrase. $results['data'] = \array_combine( \array_column( $results['data'], 'keyword' ), \array_values( $results['data'] ) ); return $this->to_result_object( $results ); } catch ( Exception $e ) { return (object) [ 'error' => $e->getMessage(), 'status' => $e->getCode(), ]; } } /** * Collects the keyphrases associated with the post. * * @param WP_Post $post The post object. * * @return array The keyphrases. */ public function collect_keyphrases_from_post( $post ) { $keyphrases = []; $primary_keyphrase = $this->indexable_repository ->query() ->select( 'primary_focus_keyword' ) ->where( 'object_id', $post->ID ) ->find_one(); if ( $primary_keyphrase ) { $keyphrases[] = $primary_keyphrase->primary_focus_keyword; } /** * Filters the keyphrases collected by the Wincher integration from the post. * * @param array $keyphrases The keyphrases array. * @param int $post_id The ID of the post. */ return \apply_filters( 'wpseo_wincher_keyphrases_from_post', $keyphrases, $post->ID ); } /** * Collects all keyphrases known to Yoast. * * @return array */ protected function collect_all_keyphrases() { // Collect primary keyphrases first. $keyphrases = \array_column( $this->indexable_repository ->query() ->select( 'primary_focus_keyword' ) ->where_not_null( 'primary_focus_keyword' ) ->where( 'object_type', 'post' ) ->where_not_equal( 'post_status', 'trash' ) ->distinct() ->find_array(), 'primary_focus_keyword' ); /** * Filters the keyphrases collected by the Wincher integration from all the posts. * * @param array $keyphrases The keyphrases array. */ $keyphrases = \apply_filters( 'wpseo_wincher_all_keyphrases', $keyphrases ); // Filter out empty entries. return \array_filter( $keyphrases ); } /** * Filters the results based on the passed keyphrases. * * @param array $results The results to filter. * @param array $used_keyphrases The used keyphrases. * * @return array The filtered results. */ protected function filter_results_by_used_keyphrases( $results, $used_keyphrases ) { return \array_filter( $results, static function ( $result ) use ( $used_keyphrases ) { return \in_array( $result['keyword'], \array_map( 'strtolower', $used_keyphrases ), true ); } ); } /** * Determines whether the amount of keyphrases would mean the user exceeds their account limits. * * @param string|array $keyphrases The keyphrases to be added. * @param object $limits The current account limits. * * @return bool Whether the limit is exceeded. */ protected function would_exceed_limits( $keyphrases, $limits ) { if ( ! \is_array( $keyphrases ) ) { $keyphrases = [ $keyphrases ]; } if ( $limits->limit === null ) { return false; } return ( \count( $keyphrases ) + $limits->usage ) > $limits->limit; } /** * Converts the passed dataset to an object. * * @param array $result The result dataset to convert to an object. * * @return object The result object. */ protected function to_result_object( $result ) { if ( \array_key_exists( 'data', $result ) ) { $result['results'] = (object) $result['data']; unset( $result['data'] ); } if ( \array_key_exists( 'message', $result ) ) { $result['error'] = $result['message']; unset( $result['message'] ); } return (object) $result; } } wincher/wincher-login-action.php000066600000003336151733344100012746 0ustar00client = $client; $this->options_helper = $options_helper; } /** * Returns the authorization URL. * * @return object The response object. */ public function get_authorization_url() { return (object) [ 'status' => 200, 'url' => $this->client->get_authorization_url(), ]; } /** * Authenticates with Wincher to request the necessary tokens. * * @param string $code The authentication code to use to request a token with. * @param string $website_id The website id associated with the code. * * @return object The response object. */ public function authenticate( $code, $website_id ) { // Code has already been validated at this point. No need to do that again. try { $tokens = $this->client->request_tokens( $code ); $this->options_helper->set( 'wincher_website_id', $website_id ); return (object) [ 'tokens' => $tokens->to_array(), 'status' => 200, ]; } catch ( Authentication_Failed_Exception $e ) { return $e->get_response(); } } } configuration/first-time-configuration-action.php000066600000021142151733344100016334 0ustar00options_helper = $options_helper; $this->social_profiles_helper = $social_profiles_helper; } /** * Stores the values for the site representation. * * @param array $params The values to store. * * @return object The response object. */ public function set_site_representation( $params ) { $failures = []; $old_values = $this->get_old_values( self::SITE_REPRESENTATION_FIELDS ); foreach ( self::SITE_REPRESENTATION_FIELDS as $field_name ) { if ( isset( $params[ $field_name ] ) ) { $result = $this->options_helper->set( $field_name, $params[ $field_name ] ); if ( ! $result ) { $failures[] = $field_name; } } } // Delete cached logos in the db. $this->options_helper->set( 'company_logo_meta', false ); $this->options_helper->set( 'person_logo_meta', false ); /** * Action: 'wpseo_post_update_site_representation' - Allows for Hiive event tracking. * * @param array $params The new values of the options. * @param array $old_values The old values of the options. * @param array $failures The options that failed to be saved. * * @internal */ \do_action( 'wpseo_ftc_post_update_site_representation', $params, $old_values, $failures ); if ( \count( $failures ) === 0 ) { return (object) [ 'success' => true, 'status' => 200, ]; } return (object) [ 'success' => false, 'status' => 500, 'error' => 'Could not save some options in the database', 'failures' => $failures, ]; } /** * Stores the values for the social profiles. * * @param array $params The values to store. * * @return object The response object. */ public function set_social_profiles( $params ) { $old_values = $this->get_old_values( \array_keys( $this->social_profiles_helper->get_organization_social_profile_fields() ) ); $failures = $this->social_profiles_helper->set_organization_social_profiles( $params ); /** * Action: 'wpseo_post_update_social_profiles' - Allows for Hiive event tracking. * * @param array $params The new values of the options. * @param array $old_values The old values of the options. * @param array $failures The options that failed to be saved. * * @internal */ \do_action( 'wpseo_ftc_post_update_social_profiles', $params, $old_values, $failures ); if ( empty( $failures ) ) { return (object) [ 'success' => true, 'status' => 200, ]; } return (object) [ 'success' => false, 'status' => 200, 'error' => 'Could not save some options in the database', 'failures' => $failures, ]; } /** * Stores the values for the social profiles. * * @param array $params The values to store. * * @return object The response object. */ public function set_person_social_profiles( $params ) { $social_profiles = \array_filter( $params, static function ( $key ) { return $key !== 'user_id'; }, \ARRAY_FILTER_USE_KEY ); $failures = $this->social_profiles_helper->set_person_social_profiles( $params['user_id'], $social_profiles ); if ( \count( $failures ) === 0 ) { return (object) [ 'success' => true, 'status' => 200, ]; } return (object) [ 'success' => false, 'status' => 200, 'error' => 'Could not save some options in the database', 'failures' => $failures, ]; } /** * Gets the values for the social profiles. * * @param int $user_id The person ID. * * @return object The response object. */ public function get_person_social_profiles( $user_id ) { return (object) [ 'success' => true, 'status' => 200, 'social_profiles' => $this->social_profiles_helper->get_person_social_profiles( $user_id ), ]; } /** * Stores the values to enable/disable tracking. * * @param array $params The values to store. * * @return object The response object. */ public function set_enable_tracking( $params ) { $success = true; $option_value = $this->options_helper->get( 'tracking' ); if ( $option_value !== $params['tracking'] ) { $this->options_helper->set( 'toggled_tracking', true ); $success = $this->options_helper->set( 'tracking', $params['tracking'] ); } /** * Action: 'wpseo_post_update_enable_tracking' - Allows for Hiive event tracking. * * @param array $new_value The new value. * @param array $old_value The old value. * @param bool $failure Whether the option failed to be stored. * * @internal */ // $success is negated to be aligned with the other two actions which pass $failures. \do_action( 'wpseo_ftc_post_update_enable_tracking', $params['tracking'], $option_value, ! $success ); if ( $success ) { return (object) [ 'success' => true, 'status' => 200, ]; } return (object) [ 'success' => false, 'status' => 500, 'error' => 'Could not save the option in the database', ]; } /** * Checks if the current user has the capability a specific user. * * @param int $user_id The id of the user to be edited. * * @return object The response object. */ public function check_capability( $user_id ) { if ( $this->can_edit_profile( $user_id ) ) { return (object) [ 'success' => true, 'status' => 200, ]; } return (object) [ 'success' => false, 'status' => 403, ]; } /** * Stores the first time configuration state. * * @param array $params The values to store. * * @return object The response object. */ public function save_configuration_state( $params ) { // If the finishedSteps param is not present in the REST request, it's a malformed request. if ( ! isset( $params['finishedSteps'] ) ) { return (object) [ 'success' => false, 'status' => 400, 'error' => 'Bad request', ]; } // Sanitize input. $finished_steps = \array_map( '\sanitize_text_field', \wp_unslash( $params['finishedSteps'] ) ); $success = $this->options_helper->set( 'configuration_finished_steps', $finished_steps ); if ( ! $success ) { return (object) [ 'success' => false, 'status' => 500, 'error' => 'Could not save the option in the database', ]; } // If all the five steps of the configuration have been completed, set first_time_install option to false. if ( \count( $params['finishedSteps'] ) === 3 ) { $this->options_helper->set( 'first_time_install', false ); } return (object) [ 'success' => true, 'status' => 200, ]; } /** * Gets the first time configuration state. * * @return object The response object. */ public function get_configuration_state() { $configuration_option = $this->options_helper->get( 'configuration_finished_steps' ); if ( $configuration_option !== null ) { return (object) [ 'success' => true, 'status' => 200, 'data' => $configuration_option, ]; } return (object) [ 'success' => false, 'status' => 500, 'error' => 'Could not get data from the database', ]; } /** * Checks if the current user has the capability to edit a specific user. * * @param int $person_id The id of the person to edit. * * @return bool */ private function can_edit_profile( $person_id ) { return \current_user_can( 'edit_user', $person_id ); } /** * Gets the old values for the given fields. * * @param array $fields_names The fields to get the old values for. * * @return array The old values. */ private function get_old_values( array $fields_names ): array { $old_values = []; foreach ( $fields_names as $field_name ) { $old_values[ $field_name ] = $this->options_helper->get( $field_name ); } return $old_values; } } integrations-action.php000066600000002366151733344100011252 0ustar00options_helper = $options_helper; } /** * Sets an integration state. * * @param string $integration_name The name of the integration to activate/deactivate. * @param bool $value The value to store. * * @return object The response object. */ public function set_integration_active( $integration_name, $value ) { $option_name = $integration_name . '_integration_active'; $success = true; $option_value = $this->options_helper->get( $option_name ); if ( $option_value !== $value ) { $success = $this->options_helper->set( $option_name, $value ); } if ( $success ) { return (object) [ 'success' => true, 'status' => 200, ]; } return (object) [ 'success' => false, 'status' => 500, 'error' => 'Could not save the option in the database', ]; } } addon-installation/addon-activate-action.php000066600000004522151733344100015207 0ustar00addon_manager = $addon_manager; $this->require_file_helper = $require_file_helper; } /** * Activates the plugin based on the given plugin file. * * @param string $plugin_slug The plugin slug to get download url for. * * @return bool True when activation is successful. * * @throws Addon_Activation_Error_Exception Exception when the activation encounters an error. * @throws User_Cannot_Activate_Plugins_Exception Exception when the user is not allowed to activate. */ public function activate_addon( $plugin_slug ) { if ( ! \current_user_can( 'activate_plugins' ) ) { throw new User_Cannot_Activate_Plugins_Exception(); } if ( $this->addon_manager->is_installed( $plugin_slug ) ) { return true; } $this->load_wordpress_classes(); $plugin_file = $this->addon_manager->get_plugin_file( $plugin_slug ); $activation_result = \activate_plugin( $plugin_file ); if ( $activation_result !== null && \is_wp_error( $activation_result ) ) { throw new Addon_Activation_Error_Exception( $activation_result->get_error_message() ); } return true; } /** * Requires the files needed from WordPress itself. * * @codeCoverageIgnore Only loads a WordPress file. * * @return void */ protected function load_wordpress_classes() { if ( ! \function_exists( 'get_plugins' ) ) { $this->require_file_helper->require_file_once( \ABSPATH . 'wp-admin/includes/plugin.php' ); } } } addon-installation/addon-install-action.php000066600000007562151733344100015064 0ustar00addon_manager = $addon_manager; $this->require_file_helper = $require_file_helper; } /** * Installs the plugin based on the given slug. * * @param string $plugin_slug The plugin slug to install. * @param string $download_url The plugin download URL. * * @return bool True when install is successful. * * @throws Addon_Already_Installed_Exception When the addon is already installed. * @throws Addon_Installation_Error_Exception When the installation encounters an error. * @throws User_Cannot_Install_Plugins_Exception When the user does not have the permissions to install plugins. */ public function install_addon( $plugin_slug, $download_url ) { if ( ! \current_user_can( 'install_plugins' ) ) { throw new User_Cannot_Install_Plugins_Exception( $plugin_slug ); } if ( $this->is_installed( $plugin_slug ) ) { throw new Addon_Already_Installed_Exception( $plugin_slug ); } $this->load_wordpress_classes(); $install_result = $this->install( $download_url ); if ( \is_wp_error( $install_result ) ) { throw new Addon_Installation_Error_Exception( $install_result->get_error_message() ); } return $install_result; } /** * Requires the files needed from WordPress itself. * * @codeCoverageIgnore * * @return void */ protected function load_wordpress_classes() { if ( ! \class_exists( 'WP_Upgrader' ) ) { $this->require_file_helper->require_file_once( \ABSPATH . 'wp-admin/includes/class-wp-upgrader.php' ); } if ( ! \class_exists( 'Plugin_Upgrader' ) ) { $this->require_file_helper->require_file_once( \ABSPATH . 'wp-admin/includes/class-plugin-upgrader.php' ); } if ( ! \class_exists( 'WP_Upgrader_Skin' ) ) { $this->require_file_helper->require_file_once( \ABSPATH . 'wp-admin/includes/class-wp-upgrader-skin.php' ); } if ( ! \function_exists( 'get_plugin_data' ) ) { $this->require_file_helper->require_file_once( \ABSPATH . 'wp-admin/includes/plugin.php' ); } if ( ! \function_exists( 'request_filesystem_credentials' ) ) { $this->require_file_helper->require_file_once( \ABSPATH . 'wp-admin/includes/file.php' ); } } /** * Checks is a plugin is installed. * * @param string $plugin_slug The plugin to check. * * @return bool True when plugin is installed. */ protected function is_installed( $plugin_slug ) { return $this->addon_manager->get_plugin_file( $plugin_slug ) !== false; } /** * Runs the installation by using the WordPress installation routine. * * @codeCoverageIgnore Contains WordPress specific logic. * * @param string $plugin_download The url to the download. * * @return bool|WP_Error True when success, WP_Error when something went wrong. */ protected function install( $plugin_download ) { $plugin_upgrader = new Plugin_Upgrader(); return $plugin_upgrader->install( $plugin_download ); } } indexables/indexable-head-action.php000066600000007417151733344100013516 0ustar00meta_surface = $meta_surface; } /** * Retrieves the head for a url. * * @param string $url The url to get the head for. * * @return object Object with head and status properties. */ public function for_url( $url ) { if ( $url === \trailingslashit( \get_home_url() ) ) { return $this->with_404_fallback( $this->with_cache( 'home_page' ) ); } return $this->with_404_fallback( $this->with_cache( 'url', $url ) ); } /** * Retrieves the head for a post. * * @param int $id The id. * * @return object Object with head and status properties. */ public function for_post( $id ) { return $this->with_404_fallback( $this->with_cache( 'post', $id ) ); } /** * Retrieves the head for a term. * * @param int $id The id. * * @return object Object with head and status properties. */ public function for_term( $id ) { return $this->with_404_fallback( $this->with_cache( 'term', $id ) ); } /** * Retrieves the head for an author. * * @param int $id The id. * * @return object Object with head and status properties. */ public function for_author( $id ) { return $this->with_404_fallback( $this->with_cache( 'author', $id ) ); } /** * Retrieves the head for a post type archive. * * @param int $type The id. * * @return object Object with head and status properties. */ public function for_post_type_archive( $type ) { return $this->with_404_fallback( $this->with_cache( 'post_type_archive', $type ) ); } /** * Retrieves the head for the posts page. * * @return object Object with head and status properties. */ public function for_posts_page() { return $this->with_404_fallback( $this->with_cache( 'posts_page' ) ); } /** * Retrieves the head for the 404 page. Always sets the status to 404. * * @return object Object with head and status properties. */ public function for_404() { $meta = $this->with_cache( '404' ); if ( ! $meta ) { return (object) [ 'html' => '', 'json' => [], 'status' => 404, ]; } $head = $meta->get_head(); return (object) [ 'html' => $head->html, 'json' => $head->json, 'status' => 404, ]; } /** * Retrieves the head for a successful page load. * * @param object $head The calculated Yoast head. * * @return object The presentations and status code 200. */ protected function for_200( $head ) { return (object) [ 'html' => $head->html, 'json' => $head->json, 'status' => 200, ]; } /** * Returns the head with 404 fallback * * @param Meta|false $meta The meta object. * * @return object The head response. */ protected function with_404_fallback( $meta ) { if ( $meta === false ) { return $this->for_404(); } else { return $this->for_200( $meta->get_head() ); } } /** * Retrieves a value from the meta surface cached. * * @param string $type The type of value to retrieve. * @param string $argument Optional. The argument for the value. * * @return Meta The meta object. */ protected function with_cache( $type, $argument = '' ) { if ( ! isset( $this->cache[ $type ][ $argument ] ) ) { $this->cache[ $type ][ $argument ] = \call_user_func( [ $this->meta_surface, "for_$type" ], $argument ); } return $this->cache[ $type ][ $argument ]; } } indexing/indexable-post-indexation-action.php000066600000013313151733344100015421 0ustar00post_type_helper = $post_type_helper; $this->repository = $repository; $this->wpdb = $wpdb; $this->version = $builder_versions->get_latest_version_for_type( 'post' ); $this->post_helper = $post_helper; } /** * Creates indexables for unindexed posts. * * @return Indexable[] The created indexables. */ public function index() { $query = $this->get_select_query( $this->get_limit() ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Function get_select_query returns a prepared query. $post_ids = $this->wpdb->get_col( $query ); $indexables = []; foreach ( $post_ids as $post_id ) { $indexables[] = $this->repository->find_by_id_and_type( (int) $post_id, 'post' ); } if ( \count( $indexables ) > 0 ) { \delete_transient( static::UNINDEXED_COUNT_TRANSIENT ); \delete_transient( static::UNINDEXED_LIMITED_COUNT_TRANSIENT ); } return $indexables; } /** * Returns the number of posts that will be indexed in a single indexing pass. * * @return int The limit. */ public function get_limit() { /** * Filter 'wpseo_post_indexation_limit' - Allow filtering the amount of posts indexed during each indexing pass. * * @param int $limit The maximum number of posts indexed. */ $limit = \apply_filters( 'wpseo_post_indexation_limit', 25 ); if ( ! \is_int( $limit ) || $limit < 1 ) { $limit = 25; } return $limit; } /** * Builds a query for counting the number of unindexed posts. * * @return string The prepared query string. */ protected function get_count_query() { $indexable_table = Model::get_table_name( 'Indexable' ); $post_types = $this->post_type_helper->get_indexable_post_types(); $excluded_post_statuses = $this->post_helper->get_excluded_post_statuses(); $replacements = \array_merge( $post_types, $excluded_post_statuses ); $replacements[] = $this->version; // Warning: If this query is changed, makes sure to update the query in get_select_query as well. // @phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber return $this->wpdb->prepare( " SELECT COUNT(P.ID) FROM {$this->wpdb->posts} AS P WHERE P.post_type IN (" . \implode( ', ', \array_fill( 0, \count( $post_types ), '%s' ) ) . ') AND P.post_status NOT IN (' . \implode( ', ', \array_fill( 0, \count( $excluded_post_statuses ), '%s' ) ) . ") AND P.ID not in ( SELECT I.object_id from $indexable_table as I WHERE I.object_type = 'post' AND I.version = %d )", $replacements ); } /** * Builds a query for selecting the ID's of unindexed posts. * * @param bool $limit The maximum number of post IDs to return. * * @return string The prepared query string. */ protected function get_select_query( $limit = false ) { $indexable_table = Model::get_table_name( 'Indexable' ); $post_types = $this->post_type_helper->get_indexable_post_types(); $excluded_post_statuses = $this->post_helper->get_excluded_post_statuses(); $replacements = \array_merge( $post_types, $excluded_post_statuses ); $replacements[] = $this->version; $limit_query = ''; if ( $limit ) { $limit_query = 'LIMIT %d'; $replacements[] = $limit; } // Warning: If this query is changed, makes sure to update the query in get_count_query as well. // @phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber return $this->wpdb->prepare( " SELECT P.ID FROM {$this->wpdb->posts} AS P WHERE P.post_type IN (" . \implode( ', ', \array_fill( 0, \count( $post_types ), '%s' ) ) . ') AND P.post_status NOT IN (' . \implode( ', ', \array_fill( 0, \count( $excluded_post_statuses ), '%s' ) ) . ") AND P.ID not in ( SELECT I.object_id from $indexable_table as I WHERE I.object_type = 'post' AND I.version = %d ) $limit_query", $replacements ); } } indexing/indexable-general-indexation-action.php000066600000007601151733344100016054 0ustar00indexable_repository = $indexable_repository; } /** * Returns the total number of unindexed objects. * * @return int The total number of unindexed objects. */ public function get_total_unindexed() { $transient = \get_transient( static::UNINDEXED_COUNT_TRANSIENT ); if ( $transient !== false ) { return (int) $transient; } $indexables_to_create = $this->query(); $result = \count( $indexables_to_create ); \set_transient( static::UNINDEXED_COUNT_TRANSIENT, $result, \DAY_IN_SECONDS ); /** * Action: 'wpseo_indexables_unindexed_calculated' - sets an option to timestamp when there are no unindexed indexables left. * * @internal */ \do_action( 'wpseo_indexables_unindexed_calculated', static::UNINDEXED_COUNT_TRANSIENT, $result ); return $result; } /** * Returns a limited number of unindexed posts. * * @param int $limit Limit the maximum number of unindexed posts that are counted. * * @return int|false The limited number of unindexed posts. False if the query fails. */ public function get_limited_unindexed_count( $limit ) { return $this->get_total_unindexed(); } /** * Creates indexables for unindexed system pages, the date archive, and the homepage. * * @return Indexable[] The created indexables. */ public function index() { $indexables = []; $indexables_to_create = $this->query(); if ( isset( $indexables_to_create['404'] ) ) { $indexables[] = $this->indexable_repository->find_for_system_page( '404' ); } if ( isset( $indexables_to_create['search'] ) ) { $indexables[] = $this->indexable_repository->find_for_system_page( 'search-result' ); } if ( isset( $indexables_to_create['date_archive'] ) ) { $indexables[] = $this->indexable_repository->find_for_date_archive(); } if ( isset( $indexables_to_create['home_page'] ) ) { $indexables[] = $this->indexable_repository->find_for_home_page(); } \set_transient( static::UNINDEXED_COUNT_TRANSIENT, 0, \DAY_IN_SECONDS ); return $indexables; } /** * Returns the number of objects that will be indexed in a single indexing pass. * * @return int The limit. */ public function get_limit() { // This matches the maximum number of indexables created by this action. return 4; } /** * Check which indexables already exist and return the values of the ones to create. * * @return array The indexable types to create. */ private function query() { $indexables_to_create = []; if ( ! $this->indexable_repository->find_for_system_page( '404', false ) ) { $indexables_to_create['404'] = true; } if ( ! $this->indexable_repository->find_for_system_page( 'search-result', false ) ) { $indexables_to_create['search'] = true; } if ( ! $this->indexable_repository->find_for_date_archive( false ) ) { $indexables_to_create['date_archive'] = true; } $need_home_page_indexable = ( (int) \get_option( 'page_on_front' ) === 0 && \get_option( 'show_on_front' ) === 'posts' ); if ( $need_home_page_indexable && ! $this->indexable_repository->find_for_home_page( false ) ) { $indexables_to_create['home_page'] = true; } return $indexables_to_create; } } indexing/abstract-link-indexing-action.php000066600000006166151733344100014714 0ustar00link_builder = $link_builder; $this->indexable_helper = $indexable_helper; $this->repository = $repository; $this->wpdb = $wpdb; } /** * Builds links for indexables which haven't had their links indexed yet. * * @return SEO_Links[] The created SEO links. */ public function index() { $objects = $this->get_objects(); $indexables = []; foreach ( $objects as $object ) { $indexable = $this->repository->find_by_id_and_type( $object->id, $object->type ); if ( $indexable ) { $this->link_builder->build( $indexable, $object->content ); $this->indexable_helper->save_indexable( $indexable ); $indexables[] = $indexable; } } if ( \count( $indexables ) > 0 ) { \delete_transient( static::UNINDEXED_COUNT_TRANSIENT ); \delete_transient( static::UNINDEXED_LIMITED_COUNT_TRANSIENT ); } return $indexables; } /** * In the case of term-links and post-links we want to use the total unindexed count, because using * the limited unindexed count actually leads to worse performance. * * @param int|bool $limit Unused. * * @return int The total number of unindexed links. */ public function get_limited_unindexed_count( $limit = false ) { return $this->get_total_unindexed(); } /** * Returns the number of texts that will be indexed in a single link indexing pass. * * @return int The limit. */ public function get_limit() { /** * Filter 'wpseo_link_indexing_limit' - Allow filtering the number of texts indexed during each link indexing pass. * * @param int $limit The maximum number of texts indexed. */ return \apply_filters( 'wpseo_link_indexing_limit', 5 ); } /** * Returns objects to be indexed. * * @return array Objects to be indexed, should be an array of objects with object_id, object_type and content. */ abstract protected function get_objects(); } indexing/limited-indexing-action-interface.php000066600000000763151733344100015540 0ustar00indexing_helper = $indexing_helper; } /** * Wraps up the indexing process. * * @return void */ public function complete() { $this->indexing_helper->complete(); } } indexing/indexable-indexing-complete-action.php000066600000001316151733344100015707 0ustar00indexable_helper = $indexable_helper; } /** * Wraps up the indexing process. * * @return void */ public function complete() { $this->indexable_helper->finish_indexing(); } } indexing/post-link-indexing-action.php000066600000007532151733344100014074 0ustar00post_type_helper = $post_type_helper; } /** * Returns objects to be indexed. * * @return array Objects to be indexed. */ protected function get_objects() { $query = $this->get_select_query( $this->get_limit() ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Function get_select_query returns a prepared query. $posts = $this->wpdb->get_results( $query ); return \array_map( static function ( $post ) { return (object) [ 'id' => (int) $post->ID, 'type' => 'post', 'content' => $post->post_content, ]; }, $posts ); } /** * Builds a query for counting the number of unindexed post links. * * @return string The prepared query string. */ protected function get_count_query() { $public_post_types = $this->post_type_helper->get_indexable_post_types(); $indexable_table = Model::get_table_name( 'Indexable' ); $links_table = Model::get_table_name( 'SEO_Links' ); // Warning: If this query is changed, makes sure to update the query in get_select_query as well. return $this->wpdb->prepare( "SELECT COUNT(P.ID) FROM {$this->wpdb->posts} AS P LEFT JOIN $indexable_table AS I ON P.ID = I.object_id AND I.link_count IS NOT NULL AND I.object_type = 'post' LEFT JOIN $links_table AS L ON L.post_id = P.ID AND L.target_indexable_id IS NULL AND L.type = 'internal' AND L.target_post_id IS NOT NULL AND L.target_post_id != 0 WHERE ( I.object_id IS NULL OR L.post_id IS NOT NULL ) AND P.post_status = 'publish' AND P.post_type IN (" . \implode( ', ', \array_fill( 0, \count( $public_post_types ), '%s' ) ) . ')', $public_post_types ); } /** * Builds a query for selecting the ID's of unindexed post links. * * @param int|false $limit The maximum number of post link IDs to return. * * @return string The prepared query string. */ protected function get_select_query( $limit = false ) { $public_post_types = $this->post_type_helper->get_indexable_post_types(); $indexable_table = Model::get_table_name( 'Indexable' ); $links_table = Model::get_table_name( 'SEO_Links' ); $replacements = $public_post_types; $limit_query = ''; if ( $limit ) { $limit_query = 'LIMIT %d'; $replacements[] = $limit; } // Warning: If this query is changed, makes sure to update the query in get_count_query as well. return $this->wpdb->prepare( " SELECT P.ID, P.post_content FROM {$this->wpdb->posts} AS P LEFT JOIN $indexable_table AS I ON P.ID = I.object_id AND I.link_count IS NOT NULL AND I.object_type = 'post' LEFT JOIN $links_table AS L ON L.post_id = P.ID AND L.target_indexable_id IS NULL AND L.type = 'internal' AND L.target_post_id IS NOT NULL AND L.target_post_id != 0 WHERE ( I.object_id IS NULL OR L.post_id IS NOT NULL ) AND P.post_status = 'publish' AND P.post_type IN (" . \implode( ', ', \array_fill( 0, \count( $public_post_types ), '%s' ) ) . ") $limit_query", $replacements ); } } indexing/indexable-term-indexation-action.php000066600000012163151733344100015405 0ustar00taxonomy = $taxonomy; $this->repository = $repository; $this->wpdb = $wpdb; $this->version = $builder_versions->get_latest_version_for_type( 'term' ); } /** * Creates indexables for unindexed terms. * * @return Indexable[] The created indexables. */ public function index() { $query = $this->get_select_query( $this->get_limit() ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Function get_select_query returns a prepared query. $term_ids = ( $query === '' ) ? [] : $this->wpdb->get_col( $query ); $indexables = []; foreach ( $term_ids as $term_id ) { $indexables[] = $this->repository->find_by_id_and_type( (int) $term_id, 'term' ); } if ( \count( $indexables ) > 0 ) { \delete_transient( static::UNINDEXED_COUNT_TRANSIENT ); \delete_transient( static::UNINDEXED_LIMITED_COUNT_TRANSIENT ); } return $indexables; } /** * Returns the number of terms that will be indexed in a single indexing pass. * * @return int The limit. */ public function get_limit() { /** * Filter 'wpseo_term_indexation_limit' - Allow filtering the number of terms indexed during each indexing pass. * * @param int $limit The maximum number of terms indexed. */ $limit = \apply_filters( 'wpseo_term_indexation_limit', 25 ); if ( ! \is_int( $limit ) || $limit < 1 ) { $limit = 25; } return $limit; } /** * Builds a query for counting the number of unindexed terms. * * @return string The prepared query string. */ protected function get_count_query() { $indexable_table = Model::get_table_name( 'Indexable' ); $taxonomy_table = $this->wpdb->term_taxonomy; $public_taxonomies = $this->taxonomy->get_indexable_taxonomies(); if ( empty( $public_taxonomies ) ) { return ''; } $taxonomies_placeholders = \implode( ', ', \array_fill( 0, \count( $public_taxonomies ), '%s' ) ); $replacements = [ $this->version ]; \array_push( $replacements, ...$public_taxonomies ); // Warning: If this query is changed, makes sure to update the query in get_count_query as well. return $this->wpdb->prepare( " SELECT COUNT(term_id) FROM {$taxonomy_table} AS T LEFT JOIN $indexable_table AS I ON T.term_id = I.object_id AND I.object_type = 'term' AND I.version = %d WHERE I.object_id IS NULL AND taxonomy IN ($taxonomies_placeholders)", $replacements ); } /** * Builds a query for selecting the ID's of unindexed terms. * * @param bool $limit The maximum number of term IDs to return. * * @return string The prepared query string. */ protected function get_select_query( $limit = false ) { $indexable_table = Model::get_table_name( 'Indexable' ); $taxonomy_table = $this->wpdb->term_taxonomy; $public_taxonomies = $this->taxonomy->get_indexable_taxonomies(); if ( empty( $public_taxonomies ) ) { return ''; } $placeholders = \implode( ', ', \array_fill( 0, \count( $public_taxonomies ), '%s' ) ); $replacements = [ $this->version ]; \array_push( $replacements, ...$public_taxonomies ); $limit_query = ''; if ( $limit ) { $limit_query = 'LIMIT %d'; $replacements[] = $limit; } // Warning: If this query is changed, makes sure to update the query in get_count_query as well. return $this->wpdb->prepare( " SELECT term_id FROM {$taxonomy_table} AS T LEFT JOIN $indexable_table AS I ON T.term_id = I.object_id AND I.object_type = 'term' AND I.version = %d WHERE I.object_id IS NULL AND taxonomy IN ($placeholders) $limit_query", $replacements ); } } indexing/indexation-action-interface.php000066600000001432151733344100014442 0ustar00get_select_query( $limit ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Function get_count_query returns a prepared query. $unindexed_object_ids = ( $query === '' ) ? [] : $this->wpdb->get_col( $query ); $count = (int) \count( $unindexed_object_ids ); \set_transient( static::UNINDEXED_LIMITED_COUNT_TRANSIENT, $count, ( \MINUTE_IN_SECONDS * 15 ) ); return $count; } /** * Returns the total number of unindexed posts. * * @return int|false The total number of unindexed posts. False if the query fails. */ public function get_total_unindexed() { $transient = \get_transient( static::UNINDEXED_COUNT_TRANSIENT ); if ( $transient !== false ) { return (int) $transient; } // Store transient before doing the query so multiple requests won't make multiple queries. // Only store this for 15 minutes to ensure that if the query doesn't complete a wrong count is not kept too long. \set_transient( static::UNINDEXED_COUNT_TRANSIENT, 0, ( \MINUTE_IN_SECONDS * 15 ) ); $query = $this->get_count_query(); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Function get_count_query returns a prepared query. $count = ( $query === '' ) ? 0 : $this->wpdb->get_var( $query ); if ( $count === null ) { return false; } \set_transient( static::UNINDEXED_COUNT_TRANSIENT, $count, \DAY_IN_SECONDS ); /** * Action: 'wpseo_indexables_unindexed_calculated' - sets an option to timestamp when there are no unindexed indexables left. * * @internal */ \do_action( 'wpseo_indexables_unindexed_calculated', static::UNINDEXED_COUNT_TRANSIENT, $count ); return (int) $count; } } indexing/indexing-prepare-action.php000066600000001261151733344100013603 0ustar00indexing_helper = $indexing_helper; } /** * Prepares the indexing routine. * * @return void */ public function prepare() { $this->indexing_helper->prepare(); } } indexing/indexable-post-type-archive-indexation-action.php000066600000014026151733344100020021 0ustar00repository = $repository; $this->builder = $builder; $this->post_type = $post_type; $this->version = $versions->get_latest_version_for_type( 'post-type-archive' ); } /** * Returns the total number of unindexed post type archives. * * @param int|false $limit Limit the number of counted objects. * False for "no limit". * * @return int The total number of unindexed post type archives. */ public function get_total_unindexed( $limit = false ) { $transient = \get_transient( static::UNINDEXED_COUNT_TRANSIENT ); if ( $transient !== false ) { return (int) $transient; } \set_transient( static::UNINDEXED_COUNT_TRANSIENT, 0, \DAY_IN_SECONDS ); $result = \count( $this->get_unindexed_post_type_archives( $limit ) ); \set_transient( static::UNINDEXED_COUNT_TRANSIENT, $result, \DAY_IN_SECONDS ); /** * Action: 'wpseo_indexables_unindexed_calculated' - sets an option to timestamp when there are no unindexed indexables left. * * @internal */ \do_action( 'wpseo_indexables_unindexed_calculated', static::UNINDEXED_COUNT_TRANSIENT, $result ); return $result; } /** * Creates indexables for post type archives. * * @return Indexable[] The created indexables. */ public function index() { $unindexed_post_type_archives = $this->get_unindexed_post_type_archives( $this->get_limit() ); $indexables = []; foreach ( $unindexed_post_type_archives as $post_type_archive ) { $indexables[] = $this->builder->build_for_post_type_archive( $post_type_archive ); } if ( \count( $indexables ) > 0 ) { \delete_transient( static::UNINDEXED_COUNT_TRANSIENT ); } return $indexables; } /** * Returns the number of post type archives that will be indexed in a single indexing pass. * * @return int The limit. */ public function get_limit() { /** * Filter 'wpseo_post_type_archive_indexation_limit' - Allow filtering the number of posts indexed during each indexing pass. * * @param int $limit The maximum number of posts indexed. */ $limit = \apply_filters( 'wpseo_post_type_archive_indexation_limit', 25 ); if ( ! \is_int( $limit ) || $limit < 1 ) { $limit = 25; } return $limit; } /** * Retrieves the list of post types for which no indexable for its archive page has been made yet. * * @param int|false $limit Limit the number of retrieved indexables to this number. * * @return array The list of post types for which no indexable for its archive page has been made yet. */ protected function get_unindexed_post_type_archives( $limit = false ) { $post_types_with_archive_pages = $this->get_post_types_with_archive_pages(); $indexed_post_types = $this->get_indexed_post_type_archives(); $unindexed_post_types = \array_diff( $post_types_with_archive_pages, $indexed_post_types ); if ( $limit ) { return \array_slice( $unindexed_post_types, 0, $limit ); } return $unindexed_post_types; } /** * Returns the names of all the post types that have archive pages. * * @return array The list of names of all post types that have archive pages. */ protected function get_post_types_with_archive_pages() { // We only want to index archive pages of public post types that have them. $post_types_with_archive = $this->post_type->get_indexable_post_archives(); // We only need the post type names, not the objects. $post_types = []; foreach ( $post_types_with_archive as $post_type_with_archive ) { $post_types[] = $post_type_with_archive->name; } return $post_types; } /** * Retrieves the list of post type names for which an archive indexable exists. * * @return array The list of names of post types with unindexed archive pages. */ protected function get_indexed_post_type_archives() { $results = $this->repository->query() ->select( 'object_sub_type' ) ->where( 'object_type', 'post-type-archive' ) ->where_equal( 'version', $this->version ) ->find_array(); if ( $results === false ) { return []; } $callback = static function ( $result ) { return $result['object_sub_type']; }; return \array_map( $callback, $results ); } /** * Returns a limited number of unindexed posts. * * @param int $limit Limit the maximum number of unindexed posts that are counted. * * @return int|false The limited number of unindexed posts. False if the query fails. */ public function get_limited_unindexed_count( $limit ) { return $this->get_total_unindexed( $limit ); } } indexing/term-link-indexing-action.php000066600000006677151733344100014067 0ustar00taxonomy_helper = $taxonomy_helper; } /** * Returns objects to be indexed. * * @return array Objects to be indexed. */ protected function get_objects() { $query = $this->get_select_query( $this->get_limit() ); if ( $query === '' ) { return []; } // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Function get_select_query returns a prepared query. $terms = $this->wpdb->get_results( $query ); return \array_map( static function ( $term ) { return (object) [ 'id' => (int) $term->term_id, 'type' => 'term', 'content' => $term->description, ]; }, $terms ); } /** * Builds a query for counting the number of unindexed term links. * * @return string The prepared query string. */ protected function get_count_query() { $public_taxonomies = $this->taxonomy_helper->get_indexable_taxonomies(); if ( empty( $public_taxonomies ) ) { return ''; } $placeholders = \implode( ', ', \array_fill( 0, \count( $public_taxonomies ), '%s' ) ); $indexable_table = Model::get_table_name( 'Indexable' ); // Warning: If this query is changed, makes sure to update the query in get_select_query as well. return $this->wpdb->prepare( " SELECT COUNT(T.term_id) FROM {$this->wpdb->term_taxonomy} AS T LEFT JOIN $indexable_table AS I ON T.term_id = I.object_id AND I.object_type = 'term' AND I.link_count IS NOT NULL WHERE I.object_id IS NULL AND T.taxonomy IN ($placeholders)", $public_taxonomies ); } /** * Builds a query for selecting the ID's of unindexed term links. * * @param int|false $limit The maximum number of term link IDs to return. * * @return string The prepared query string. */ protected function get_select_query( $limit = false ) { $public_taxonomies = $this->taxonomy_helper->get_indexable_taxonomies(); if ( empty( $public_taxonomies ) ) { return ''; } $indexable_table = Model::get_table_name( 'Indexable' ); $replacements = $public_taxonomies; $limit_query = ''; if ( $limit ) { $limit_query = 'LIMIT %d'; $replacements[] = $limit; } // Warning: If this query is changed, makes sure to update the query in get_count_query as well. return $this->wpdb->prepare( " SELECT T.term_id, T.description FROM {$this->wpdb->term_taxonomy} AS T LEFT JOIN $indexable_table AS I ON T.term_id = I.object_id AND I.object_type = 'term' AND I.link_count IS NOT NULL WHERE I.object_id IS NULL AND T.taxonomy IN (" . \implode( ', ', \array_fill( 0, \count( $public_taxonomies ), '%s' ) ) . ") $limit_query", $replacements ); } }