���� 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*!indexable-repository.php000066600000042131151734251650011443 0ustar00builder = $builder; $this->current_page = $current_page; $this->logger = $logger; $this->hierarchy_repository = $hierarchy_repository; $this->wpdb = $wpdb; $this->version_manager = $version_manager; } /** * Starts a query for this repository. * * @return ORM */ public function query() { return Model::of_type( 'Indexable' ); } /** * Attempts to find the indexable for the current WordPress page. Returns false if no indexable could be found. * This may be the result of the indexable not existing or of being unable to determine what type of page the * current page is. * * @return bool|Indexable The indexable. If no indexable is found returns an empty indexable. Returns false if there is a database error. */ public function for_current_page() { $indexable = false; switch ( true ) { case $this->current_page->is_simple_page(): $indexable = $this->find_by_id_and_type( $this->current_page->get_simple_page_id(), 'post' ); break; case $this->current_page->is_home_static_page(): $indexable = $this->find_by_id_and_type( $this->current_page->get_front_page_id(), 'post' ); break; case $this->current_page->is_home_posts_page(): $indexable = $this->find_for_home_page(); break; case $this->current_page->is_term_archive(): $indexable = $this->find_by_id_and_type( $this->current_page->get_term_id(), 'term' ); break; case $this->current_page->is_date_archive(): $indexable = $this->find_for_date_archive(); break; case $this->current_page->is_search_result(): $indexable = $this->find_for_system_page( 'search-result' ); break; case $this->current_page->is_post_type_archive(): $indexable = $this->find_for_post_type_archive( $this->current_page->get_queried_post_type() ); break; case $this->current_page->is_author_archive(): $indexable = $this->find_by_id_and_type( $this->current_page->get_author_id(), 'user' ); break; case $this->current_page->is_404(): $indexable = $this->find_for_system_page( '404' ); break; } if ( $indexable === false ) { return $this->query()->create( [ 'object_type' => 'unknown', 'post_status' => 'unindexed', 'version' => 1, ] ); } return $indexable; } /** * Retrieves an indexable by its permalink. * * @param string $permalink The indexable permalink. * * @return bool|Indexable The indexable, false if none could be found. */ public function find_by_permalink( $permalink ) { $permalink_hash = \strlen( $permalink ) . ':' . \md5( $permalink ); // Find by both permalink_hash and permalink, permalink_hash is indexed so will be used first by the DB to optimize the query. return $this->query() ->where( 'permalink_hash', $permalink_hash ) ->where( 'permalink', $permalink ) ->find_one(); } /** * Retrieves all the indexable instances of a certain object type. * * @param string $object_type The object type. * * @return Indexable[] The array with all the indexable instances of a certain object type. */ public function find_all_with_type( $object_type ) { /** * The array with all the indexable instances of a certain object type. * * @var Indexable[] $indexables */ $indexables = $this ->query() ->where( 'object_type', $object_type ) ->find_many(); return \array_map( [ $this, 'upgrade_indexable' ], $indexables ); } /** * Retrieves all the indexable instances of a certain object subtype. * * @param string $object_type The object type. * @param string $object_sub_type The object subtype. * * @return Indexable[] The array with all the indexable instances of a certain object subtype. */ public function find_all_with_type_and_sub_type( $object_type, $object_sub_type ) { /** * The array with all the indexable instances of a certain object type and subtype. * * @var Indexable[] $indexables */ $indexables = $this ->query() ->where( 'object_type', $object_type ) ->where( 'object_sub_type', $object_sub_type ) ->find_many(); return \array_map( [ $this, 'upgrade_indexable' ], $indexables ); } /** * Retrieves the homepage indexable. * * @param bool $auto_create Optional. Create the indexable if it does not exist. * * @return bool|Indexable Instance of indexable. */ public function find_for_home_page( $auto_create = true ) { $indexable = \wp_cache_get( 'home-page', 'yoast-seo-indexables' ); if ( ! $indexable ) { /** * Indexable instance. * * @var Indexable $indexable */ $indexable = $this->query()->where( 'object_type', 'home-page' )->find_one(); if ( $auto_create && ! $indexable ) { $indexable = $this->builder->build_for_home_page(); } $indexable = $this->upgrade_indexable( $indexable ); \wp_cache_set( 'home-page', $indexable, 'yoast-seo-indexables', ( 5 * \MINUTE_IN_SECONDS ) ); } return $indexable; } /** * Retrieves the date archive indexable. * * @param bool $auto_create Optional. Create the indexable if it does not exist. * * @return bool|Indexable Instance of indexable. */ public function find_for_date_archive( $auto_create = true ) { /** * Indexable instance. * * @var Indexable $indexable */ $indexable = $this->query()->where( 'object_type', 'date-archive' )->find_one(); if ( $auto_create && ! $indexable ) { $indexable = $this->builder->build_for_date_archive(); } return $this->upgrade_indexable( $indexable ); } /** * Retrieves an indexable for a post type archive. * * @param string $post_type The post type. * @param bool $auto_create Optional. Create the indexable if it does not exist. * * @return bool|Indexable The indexable, false if none could be found. */ public function find_for_post_type_archive( $post_type, $auto_create = true ) { /** * Indexable instance. * * @var Indexable $indexable */ $indexable = $this->query() ->where( 'object_type', 'post-type-archive' ) ->where( 'object_sub_type', $post_type ) ->find_one(); if ( $auto_create && ! $indexable ) { $indexable = $this->builder->build_for_post_type_archive( $post_type ); } return $this->upgrade_indexable( $indexable ); } /** * Retrieves the indexable for a system page. * * @param string $object_sub_type The type of system page. * @param bool $auto_create Optional. Create the indexable if it does not exist. * * @return bool|Indexable Instance of indexable. */ public function find_for_system_page( $object_sub_type, $auto_create = true ) { /** * Indexable instance. * * @var Indexable $indexable */ $indexable = $this->query() ->where( 'object_type', 'system-page' ) ->where( 'object_sub_type', $object_sub_type ) ->find_one(); if ( $auto_create && ! $indexable ) { $indexable = $this->builder->build_for_system_page( $object_sub_type ); } return $this->upgrade_indexable( $indexable ); } /** * Retrieves an indexable by its ID and type. * * @param int $object_id The indexable object ID. * @param string $object_type The indexable object type. * @param bool $auto_create Optional. Create the indexable if it does not exist. * * @return bool|Indexable Instance of indexable. */ public function find_by_id_and_type( $object_id, $object_type, $auto_create = true ) { $indexable = $this->query() ->where( 'object_id', $object_id ) ->where( 'object_type', $object_type ) ->find_one(); if ( $auto_create && ! $indexable ) { $indexable = $this->builder->build_for_id_and_type( $object_id, $object_type ); } else { $indexable = $this->upgrade_indexable( $indexable ); } return $indexable; } /** * Retrieves multiple indexables at once by their id's and type. * * @param int[] $object_ids The array of indexable object id's. * @param string $object_type The indexable object type. * @param bool $auto_create Optional. Create the indexable if it does not exist. * * @return Indexable[] An array of indexables. */ public function find_by_multiple_ids_and_type( $object_ids, $object_type, $auto_create = true ) { if ( empty( $object_ids ) ) { return []; } /** * Represents an array of indexable objects. * * @var Indexable[] $indexables */ $indexables = $this->query() ->where_in( 'object_id', $object_ids ) ->where( 'object_type', $object_type ) ->find_many(); if ( $auto_create ) { $indexables_available = []; foreach ( $indexables as $indexable ) { $indexables_available[] = $indexable->object_id; } $indexables_to_create = \array_diff( $object_ids, $indexables_available ); foreach ( $indexables_to_create as $indexable_to_create ) { $indexables[] = $this->builder->build_for_id_and_type( $indexable_to_create, $object_type ); } } return \array_map( [ $this, 'upgrade_indexable' ], $indexables ); } /** * Finds the indexables by id's. * * @param array $indexable_ids The indexable id's. * * @return Indexable[] The found indexables. */ public function find_by_ids( array $indexable_ids ) { if ( empty( $indexable_ids ) ) { return []; } $indexables = $this ->query() ->where_in( 'id', $indexable_ids ) ->find_many(); return \array_map( [ $this, 'upgrade_indexable' ], $indexables ); } /** * Returns all ancestors of a given indexable. * * @param Indexable $indexable The indexable to find the ancestors of. * * @return Indexable[] All ancestors of the given indexable. */ public function get_ancestors( Indexable $indexable ) { // If we've already set ancestors on the indexable no need to get them again. if ( \is_array( $indexable->ancestors ) && ! empty( $indexable->ancestors ) ) { return \array_map( [ $this, 'upgrade_indexable' ], $indexable->ancestors ); } $indexable_ids = $this->hierarchy_repository->find_ancestors( $indexable ); // If we've set ancestors on the indexable because we had to build them to find them. if ( \is_array( $indexable->ancestors ) && ! empty( $indexable->ancestors ) ) { return \array_map( [ $this, 'upgrade_indexable' ], $indexable->ancestors ); } if ( empty( $indexable_ids ) ) { return []; } if ( $indexable_ids[0] === 0 && \count( $indexable_ids ) === 1 ) { return []; } $indexables = $this->query() ->where_in( 'id', $indexable_ids ) ->order_by_expr( 'FIELD(id,' . \implode( ',', $indexable_ids ) . ')' ) ->find_many(); return \array_map( [ $this, 'upgrade_indexable' ], $indexables ); } /** * Returns all subpages with a given post_parent. * * @param int $post_parent The post parent. * @param array $exclude_ids The id's to exclude. * * @return Indexable[] array of indexables. */ public function get_subpages_by_post_parent( $post_parent, $exclude_ids = [] ) { $query = $this->query() ->where( 'post_parent', $post_parent ) ->where( 'object_type', 'post' ) ->where( 'post_status', 'publish' ); if ( ! empty( $exclude_ids ) ) { $query->where_not_in( 'object_id', $exclude_ids ); } return $query->find_many(); } /** * Returns most recently modified posts of a post type. * * @param string $post_type The post type. * @param int $limit The maximum number of posts to return. * @param bool $exclude_older_than_one_year Whether to exclude posts older than one year. * @param string $search_filter Optional. A search filter to apply to the breadcrumb title. * * @return Indexable[] array of indexables. */ public function get_recently_modified_posts( string $post_type, int $limit, bool $exclude_older_than_one_year, string $search_filter = '' ) { $query = $this->query() ->where( 'object_type', 'post' ) ->where( 'object_sub_type', $post_type ) ->where_raw( '( is_public IS NULL OR is_public = 1 )' ) ->order_by_desc( 'object_last_modified' ) ->limit( $limit ); if ( $exclude_older_than_one_year === true ) { $query->where_gte( 'object_published_at', \gmdate( 'Y-m-d H:i:s', \strtotime( '-1 year' ) ) ); } if ( $search_filter !== '' ) { $query->where_like( 'breadcrumb_title', '%' . $search_filter . '%' ); } $query->order_by_desc( 'object_last_modified' ) ->limit( $limit ); return $query->find_many(); } /** * Returns the most recently modified cornerstone content of a post type. * * @param string $post_type The post type. * @param int|null $limit The maximum number of posts to return. * * @return Indexable[] array of indexables. */ public function get_recent_cornerstone_for_post_type( string $post_type, ?int $limit ) { $query = $this->query() ->where( 'object_type', 'post' ) ->where( 'object_sub_type', $post_type ) ->where_raw( '( is_public IS NULL OR is_public = 1 )' ) ->where( 'is_cornerstone', 1 ) ->order_by_desc( 'object_last_modified' ); if ( $limit !== null ) { $query->limit( $limit ); } return $query->find_many(); } /** * Updates the incoming link count for an indexable without first fetching it. * * @param int $indexable_id The indexable id. * @param int $count The incoming link count. * * @return bool Whether or not the update was succeful. */ public function update_incoming_link_count( $indexable_id, $count ) { return (bool) $this->query() ->set( 'incoming_link_count', $count ) ->where( 'id', $indexable_id ) ->update_many(); } /** * Ensures that the given indexable has a permalink. * * Will be deprecated in 17.3 - Use upgrade_indexable instead. * * @codeCoverageIgnore * * @param Indexable $indexable The indexable. * * @return bool|Indexable The indexable. */ public function ensure_permalink( $indexable ) { // @phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- self::class is safe. // @phpcs:ignore Squiz.PHP.CommentedOutCode.Found // _deprecated_function( __METHOD__, 'Yoast SEO 17.3', self::class . '::upgrade_indexable' ); return $this->upgrade_indexable( $indexable ); } /** * Checks if an Indexable is outdated, and rebuilds it when necessary. * * @param Indexable $indexable The indexable. * * @return Indexable The indexable. */ public function upgrade_indexable( $indexable ) { if ( $this->version_manager->indexable_needs_upgrade( $indexable ) ) { $indexable = $this->builder->build( $indexable ); } return $indexable; } /** * Resets the permalinks of the passed object type and subtype. * * @param string|null $type The type of the indexable. Can be null. * @param string|null $subtype The subtype. Can be null. * * @return int|bool The number of permalinks changed if the query was succesful. False otherwise. */ public function reset_permalink( $type = null, $subtype = null ) { $query = $this->query()->set( [ 'permalink' => null, 'permalink_hash' => null, 'version' => 0, ] ); if ( $type !== null ) { $query->where( 'object_type', $type ); } if ( $type !== null && $subtype !== null ) { $query->where( 'object_sub_type', $subtype ); } return $query->update_many(); } /** * Gets the total number of stored indexables. * * @return int The total number of stored indexables. */ public function get_total_number_of_indexables() { return $this->query()->count(); } } primary-term-repository.php000066600000002236151734251650012142 0ustar00query() ->where( 'post_id', $post_id ) ->where( 'taxonomy', $taxonomy ) ->find_one(); if ( $auto_create && ! $primary_term_indexable ) { $primary_term_indexable = $this->query()->create(); } return $primary_term_indexable; } } indexable-cleanup-repository.php000066600000066314151734251650013101 0ustar00taxonomy = $taxonomy; $this->post_type = $post_type; $this->author_archive = $author_archive; } /** * Starts a query for this repository. * * @return ORM */ public function query() { return Model::of_type( 'Indexable' ); } /** * Deletes rows from the indexable table depending on the object_type and object_sub_type. * * @param string $object_type The object type to query. * @param string $object_sub_type The object subtype to query. * @param int $limit The limit we'll apply to the delete query. * * @return int|bool The number of rows that was deleted or false if the query failed. */ public function clean_indexables_with_object_type_and_object_sub_type( string $object_type, string $object_sub_type, int $limit ) { global $wpdb; $indexable_table = Model::get_table_name( 'Indexable' ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: There is no unescaped user input. $sql = $wpdb->prepare( "DELETE FROM $indexable_table WHERE object_type = %s AND object_sub_type = %s ORDER BY id LIMIT %d", $object_type, $object_sub_type, $limit ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: Already prepared. return $wpdb->query( $sql ); } /** * Counts amount of indexables by object type and object sub type. * * @param string $object_type The object type to check. * @param string $object_sub_type The object sub type to check. * * @return float|int */ public function count_indexables_with_object_type_and_object_sub_type( string $object_type, string $object_sub_type ) { return $this ->query() ->where( 'object_type', $object_type ) ->where( 'object_sub_type', $object_sub_type ) ->count(); } /** * Deletes rows from the indexable table depending on the post_status. * * @param string $post_status The post status to query. * @param int $limit The limit we'll apply to the delete query. * * @return int|bool The number of rows that was deleted or false if the query failed. */ public function clean_indexables_with_post_status( $post_status, $limit ) { global $wpdb; $indexable_table = Model::get_table_name( 'Indexable' ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: There is no unescaped user input. $sql = $wpdb->prepare( "DELETE FROM $indexable_table WHERE object_type = 'post' AND post_status = %s ORDER BY id LIMIT %d", $post_status, $limit ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: Already prepared. return $wpdb->query( $sql ); } /** * Counts indexables with a certain post status. * * @param string $post_status The post status to count. * * @return float|int */ public function count_indexables_with_post_status( string $post_status ) { return $this ->query() ->where( 'object_type', 'post' ) ->where( 'post_status', $post_status ) ->count(); } /** * Cleans up any indexables that belong to post types that are not/no longer publicly viewable. * * @param int $limit The limit we'll apply to the queries. * * @return bool|int The number of deleted rows, false if the query fails. */ public function clean_indexables_for_non_publicly_viewable_post( $limit ) { global $wpdb; $indexable_table = Model::get_table_name( 'Indexable' ); $included_post_types = $this->post_type->get_indexable_post_types(); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: Too hard to fix. if ( empty( $included_post_types ) ) { $delete_query = $wpdb->prepare( "DELETE FROM $indexable_table WHERE object_type = 'post' AND object_sub_type IS NOT NULL LIMIT %d", $limit ); } else { // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- Reason: we're passing an array instead. $delete_query = $wpdb->prepare( "DELETE FROM $indexable_table WHERE object_type = 'post' AND object_sub_type IS NOT NULL AND object_sub_type NOT IN ( " . \implode( ', ', \array_fill( 0, \count( $included_post_types ), '%s' ) ) . ' ) LIMIT %d', \array_merge( $included_post_types, [ $limit ] ) ); } // phpcs:enable // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- Reason: Is it prepared already. return $wpdb->query( $delete_query ); // phpcs:enable } /** * Counts all indexables for non public post types. * * @return float|int */ public function count_indexables_for_non_publicly_viewable_post() { $included_post_types = $this->post_type->get_indexable_post_types(); if ( empty( $included_post_types ) ) { return $this ->query() ->where( 'object_type', 'post' ) ->where_not_equal( 'object_sub_type', 'null' ) ->count(); } else { return $this ->query() ->where( 'object_type', 'post' ) ->where_not_equal( 'object_sub_type', 'null' ) ->where_not_in( 'object_sub_type', $included_post_types ) ->count(); } } /** * Cleans up any indexables that belong to taxonomies that are not/no longer publicly viewable. * * @param int $limit The limit we'll apply to the queries. * * @return bool|int The number of deleted rows, false if the query fails. */ public function clean_indexables_for_non_publicly_viewable_taxonomies( $limit ) { global $wpdb; $indexable_table = Model::get_table_name( 'Indexable' ); $included_taxonomies = $this->taxonomy->get_indexable_taxonomies(); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: Too hard to fix. if ( empty( $included_taxonomies ) ) { $delete_query = $wpdb->prepare( "DELETE FROM $indexable_table WHERE object_type = 'term' AND object_sub_type IS NOT NULL LIMIT %d", $limit ); } else { // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- Reason: we're passing an array instead. $delete_query = $wpdb->prepare( "DELETE FROM $indexable_table WHERE object_type = 'term' AND object_sub_type IS NOT NULL AND object_sub_type NOT IN ( " . \implode( ', ', \array_fill( 0, \count( $included_taxonomies ), '%s' ) ) . ' ) LIMIT %d', \array_merge( $included_taxonomies, [ $limit ] ) ); } // phpcs:enable // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- Reason: Is it prepared already. return $wpdb->query( $delete_query ); // phpcs:enable } /** * Cleans up any indexables that belong to post type archive page that are not/no longer publicly viewable. * * @param int $limit The limit we'll apply to the queries. * * @return bool|int The number of deleted rows, false if the query fails. */ public function clean_indexables_for_non_publicly_viewable_post_type_archive_pages( $limit ) { global $wpdb; $indexable_table = Model::get_table_name( 'Indexable' ); $included_post_types = $this->post_type->get_indexable_post_archives(); $post_archives = []; foreach ( $included_post_types as $post_type ) { $post_archives[] = $post_type->name; } // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: Too hard to fix. if ( empty( $post_archives ) ) { $delete_query = $wpdb->prepare( "DELETE FROM $indexable_table WHERE object_type = 'post-type-archive' AND object_sub_type IS NOT NULL LIMIT %d", $limit ); } else { // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- Reason: we're passing an array instead. $delete_query = $wpdb->prepare( "DELETE FROM $indexable_table WHERE object_type = 'post-type-archive' AND object_sub_type IS NOT NULL AND object_sub_type NOT IN ( " . \implode( ', ', \array_fill( 0, \count( $post_archives ), '%s' ) ) . ' ) LIMIT %d', \array_merge( $post_archives, [ $limit ] ) ); } // phpcs:enable // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- Reason: Is it prepared already. return $wpdb->query( $delete_query ); // phpcs:enable } /** * Counts indexables for non publicly viewable taxonomies. * * @return float|int */ public function count_indexables_for_non_publicly_viewable_taxonomies() { $included_taxonomies = $this->taxonomy->get_indexable_taxonomies(); if ( empty( $included_taxonomies ) ) { return $this ->query() ->where( 'object_type', 'term' ) ->where_not_equal( 'object_sub_type', 'null' ) ->count(); } else { return $this ->query() ->where( 'object_type', 'term' ) ->where_not_equal( 'object_sub_type', 'null' ) ->where_not_in( 'object_sub_type', $included_taxonomies ) ->count(); } } /** * Counts indexables for non publicly viewable taxonomies. * * @return float|int */ public function count_indexables_for_non_publicly_post_type_archive_pages() { $included_post_types = $this->post_type->get_indexable_post_archives(); $post_archives = []; foreach ( $included_post_types as $post_type ) { $post_archives[] = $post_type->name; } if ( empty( $post_archives ) ) { return $this ->query() ->where( 'object_type', 'post-type-archive' ) ->where_not_equal( 'object_sub_type', 'null' ) ->count(); } return $this ->query() ->where( 'object_type', 'post-type-archive' ) ->where_not_equal( 'object_sub_type', 'null' ) ->where_not_in( 'object_sub_type', $post_archives ) ->count(); } /** * Cleans up any user indexables when the author archives have been disabled. * * @param int $limit The limit we'll apply to the queries. * * @return bool|int The number of deleted rows, false if the query fails. */ public function clean_indexables_for_authors_archive_disabled( $limit ) { global $wpdb; if ( ! $this->author_archive->are_disabled() ) { return 0; } $indexable_table = Model::get_table_name( 'Indexable' ); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: Too hard to fix. $delete_query = $wpdb->prepare( "DELETE FROM $indexable_table WHERE object_type = 'user' LIMIT %d", $limit ); // phpcs:enable // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- Reason: Is it prepared already. return $wpdb->query( $delete_query ); // phpcs:enable } /** * Counts the amount of author archive indexables if they are not disabled. * * @return float|int */ public function count_indexables_for_authors_archive_disabled() { if ( ! $this->author_archive->are_disabled() ) { return 0; } return $this ->query() ->where( 'object_type', 'user' ) ->count(); } /** * Cleans up any indexables that belong to users that have their author archives disabled. * * @param int $limit The limit we'll apply to the queries. * * @return bool|int The number of deleted rows, false if the query fails. */ public function clean_indexables_for_authors_without_archive( $limit ) { global $wpdb; $indexable_table = Model::get_table_name( 'Indexable' ); $author_archive_post_types = $this->author_archive->get_author_archive_post_types(); $viewable_post_stati = \array_filter( \get_post_stati(), 'is_post_status_viewable' ); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: Too hard to fix. // phpcs:disable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- Reason: we're passing an array instead. $delete_query = $wpdb->prepare( "DELETE FROM $indexable_table WHERE object_type = 'user' AND object_id NOT IN ( SELECT DISTINCT post_author FROM $wpdb->posts WHERE post_type IN ( " . \implode( ', ', \array_fill( 0, \count( $author_archive_post_types ), '%s' ) ) . ' ) AND post_status IN ( ' . \implode( ', ', \array_fill( 0, \count( $viewable_post_stati ), '%s' ) ) . ' ) ) LIMIT %d', \array_merge( $author_archive_post_types, $viewable_post_stati, [ $limit ] ) ); // phpcs:enable // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- Reason: Is it prepared already. return $wpdb->query( $delete_query ); // phpcs:enable } /** * Counts total amount of indexables for authors without archives. * * @return bool|int|mysqli_result|resource|null */ public function count_indexables_for_authors_without_archive() { global $wpdb; $indexable_table = Model::get_table_name( 'Indexable' ); $author_archive_post_types = $this->author_archive->get_author_archive_post_types(); $viewable_post_stati = \array_filter( \get_post_stati(), 'is_post_status_viewable' ); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: Too hard to fix. // phpcs:disable WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- Reason: we're passing an array instead. $count_query = $wpdb->prepare( "SELECT count(*) FROM $indexable_table WHERE object_type = 'user' AND object_id NOT IN ( SELECT DISTINCT post_author FROM $wpdb->posts WHERE post_type IN ( " . \implode( ', ', \array_fill( 0, \count( $author_archive_post_types ), '%s' ) ) . ' ) AND post_status IN ( ' . \implode( ', ', \array_fill( 0, \count( $viewable_post_stati ), '%s' ) ) . ' ) )', \array_merge( $author_archive_post_types, $viewable_post_stati ) ); // phpcs:enable // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: No relevant caches. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Reason: Most performant way. // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- Reason: Is it prepared already. return $wpdb->get_col( $count_query )[0]; // phpcs:enable } /** * Deletes rows from the indexable table where the source is no longer there. * * @param string $source_table The source table which we need to check the indexables against. * @param string $source_identifier The identifier which the indexables are matched to. * @param string $object_type The indexable object type. * @param int $limit The limit we'll apply to the delete query. * * @return int|bool The number of rows that was deleted or false if the query failed. */ public function clean_indexables_for_object_type_and_source_table( $source_table, $source_identifier, $object_type, $limit ) { global $wpdb; $indexable_table = Model::get_table_name( 'Indexable' ); $source_table = $wpdb->prefix . $source_table; // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: There is no unescaped user input. $query = $wpdb->prepare( " SELECT indexable_table.object_id FROM {$indexable_table} indexable_table LEFT JOIN {$source_table} AS source_table ON indexable_table.object_id = source_table.{$source_identifier} WHERE source_table.{$source_identifier} IS NULL AND indexable_table.object_id IS NOT NULL AND indexable_table.object_type = '{$object_type}' LIMIT %d", $limit ); // phpcs:enable // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: Already prepared. $orphans = $wpdb->get_col( $query ); if ( empty( $orphans ) ) { return 0; } // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: Already prepared. return $wpdb->query( "DELETE FROM $indexable_table WHERE object_type = '{$object_type}' AND object_id IN( " . \implode( ',', $orphans ) . ' )' ); } /** * Deletes rows from the indexable table where the source is no longer there. * * @param int $limit The limit we'll apply to the delete query. * * @return int|bool The number of rows that was deleted or false if the query failed. */ public function clean_indexables_for_orphaned_users( $limit ) { global $wpdb; $indexable_table = Model::get_table_name( 'Indexable' ); $source_table = $wpdb->users; // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: There is no unescaped user input. $query = $wpdb->prepare( " SELECT indexable_table.object_id FROM {$indexable_table} indexable_table LEFT JOIN {$source_table} AS source_table ON indexable_table.object_id = source_table.ID WHERE source_table.ID IS NULL AND indexable_table.object_id IS NOT NULL AND indexable_table.object_type = 'user' LIMIT %d", $limit ); // phpcs:enable // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: Already prepared. $orphans = $wpdb->get_col( $query ); if ( empty( $orphans ) ) { return 0; } // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: Already prepared. return $wpdb->query( "DELETE FROM $indexable_table WHERE object_type = 'user' AND object_id IN( " . \implode( ',', $orphans ) . ' )' ); } /** * Counts indexables for given source table + source identifier + object type. * * @param string $source_table The source table. * @param string $source_identifier The source identifier. * @param string $object_type The object type. * * @return mixed */ public function count_indexables_for_object_type_and_source_table( string $source_table, string $source_identifier, string $object_type ) { global $wpdb; $indexable_table = Model::get_table_name( 'Indexable' ); $source_table = $wpdb->prefix . $source_table; // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: Already prepared. return $wpdb->get_col( " SELECT count(*) FROM {$indexable_table} indexable_table LEFT JOIN {$source_table} AS source_table ON indexable_table.object_id = source_table.{$source_identifier} WHERE source_table.{$source_identifier} IS NULL AND indexable_table.object_id IS NOT NULL AND indexable_table.object_type = '{$object_type}'" )[0]; // phpcs:enable } /** * Counts indexables for orphaned users. * * @return mixed */ public function count_indexables_for_orphaned_users() { global $wpdb; $indexable_table = Model::get_table_name( 'Indexable' ); $source_table = $wpdb->users; //phpcs:disable WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: Already prepared. return $wpdb->get_col( " SELECT count(*) FROM {$indexable_table} indexable_table LEFT JOIN {$source_table} AS source_table ON indexable_table.object_id = source_table.ID WHERE source_table.ID IS NULL AND indexable_table.object_id IS NOT NULL AND indexable_table.object_type = 'user'" )[0]; // phpcs:enable } /** * Cleans orphaned rows from a yoast table. * * @param string $table The table to clean up. * @param string $column The table column the cleanup will rely on. * @param int $limit The limit we'll apply to the queries. * * @return int|bool The number of deleted rows, false if the query fails. */ public function cleanup_orphaned_from_table( $table, $column, $limit ) { global $wpdb; $table = Model::get_table_name( $table ); $indexable_table = Model::get_table_name( 'Indexable' ); // Warning: If this query is changed, make sure to update the query in cleanup_orphaned_from_table in Premium as well. // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: There is no unescaped user input. $query = $wpdb->prepare( " SELECT table_to_clean.{$column} FROM {$table} table_to_clean LEFT JOIN {$indexable_table} AS indexable_table ON table_to_clean.{$column} = indexable_table.id WHERE indexable_table.id IS NULL AND table_to_clean.{$column} IS NOT NULL LIMIT %d", $limit ); // phpcs:enable // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: Already prepared. $orphans = $wpdb->get_col( $query ); if ( empty( $orphans ) ) { return 0; } // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: Already prepared. return $wpdb->query( "DELETE FROM $table WHERE {$column} IN( " . \implode( ',', $orphans ) . ' )' ); } /** * Counts orphaned rows from a yoast table. * * @param string $table The table to clean up. * @param string $column The table column the cleanup will rely on. * * @return int|bool The number of deleted rows, false if the query fails. */ public function count_orphaned_from_table( string $table, string $column ) { global $wpdb; $table = Model::get_table_name( $table ); $indexable_table = Model::get_table_name( 'Indexable' ); // Warning: If this query is changed, make sure to update the query in cleanup_orphaned_from_table in Premium as well. // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: Already prepared. return $wpdb->get_col( " SELECT count(*) FROM {$table} table_to_clean LEFT JOIN {$indexable_table} AS indexable_table ON table_to_clean.{$column} = indexable_table.id WHERE indexable_table.id IS NULL AND table_to_clean.{$column} IS NOT NULL" )[0]; // phpcs:enable } /** * Updates the author_id of indexables which author_id is not in the wp_users table with the id of the reassingned * user. * * @param int $limit The limit we'll apply to the queries. * * @return int|bool The number of updated rows, false if query to get data fails. */ public function update_indexables_author_to_reassigned( $limit ) { // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: Already prepared. $reassigned_authors_objs = $this->get_reassigned_authors( $limit ); if ( $reassigned_authors_objs === false ) { return false; } return $this->update_indexable_authors( $reassigned_authors_objs, $limit ); } /** * Fetches pairs of old_id -> new_id indexed by old_id. * By using the old_id (i.e. the id of the user that has been deleted) as key of the associative array, we can * easily compose an array of unique pairs of old_id -> new_id. * * @param int $limit The limit we'll apply to the queries. * * @return int|bool The associative array with shape [ old_id => [ old_id, new_author ] ] or false if query to get * data fails. */ private function get_reassigned_authors( $limit ) { global $wpdb; $indexable_table = Model::get_table_name( 'Indexable' ); $posts_table = $wpdb->posts; // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: There is no unescaped user input. $query = $wpdb->prepare( " SELECT {$indexable_table}.author_id, {$posts_table}.post_author FROM {$indexable_table} JOIN {$posts_table} on {$indexable_table}.object_id = {$posts_table}.id WHERE object_type='post' AND {$indexable_table}.author_id <> {$posts_table}.post_author GROUP BY {$indexable_table}.author_id, {$posts_table}.post_author ORDER BY {$indexable_table}.author_id LIMIT %d", $limit ); // phpcs:enable // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: Already prepared. return $wpdb->get_results( $query, \OBJECT_K ); } /** * Updates the indexable's author_id referring to a deleted author with the id of the reassigned user. * * @param array $reassigned_authors_objs The array of objects with shape [ old_id => [ old_id, new_id ] ]. * @param int $limit The limit we'll apply to the queries. * * @return int|bool The associative array with shape [ old_id => [ old_id, new_author ] ] or false if query to get * data fails. */ private function update_indexable_authors( $reassigned_authors_objs, $limit ) { global $wpdb; $indexable_table = Model::get_table_name( 'Indexable' ); // This is a workaround for the fact that the array_column function does not work on objects in PHP 5.6. $reassigned_authors_array = \array_map( static function ( $obj ) { return (array) $obj; }, $reassigned_authors_objs ); $reassigned_authors = \array_combine( \array_column( $reassigned_authors_array, 'author_id' ), \array_column( $reassigned_authors_array, 'post_author' ) ); foreach ( $reassigned_authors as $old_author_id => $new_author_id ) { // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Reason: There is no unescaped user input. $query = $wpdb->prepare( " UPDATE {$indexable_table} SET {$indexable_table}.author_id = {$new_author_id} WHERE {$indexable_table}.author_id = {$old_author_id} AND object_type='post' LIMIT %d", $limit ); // phpcs:enable // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Reason: Already prepared. $wpdb->query( $query ); } return \count( $reassigned_authors ); } } indexable-hierarchy-repository.php000066600000007622151734251650013425 0ustar00builder = $builder; } /** * Sets the indexable helper. * * @required * * @param Indexable_Helper $indexable_helper The indexable helper. * * @return void */ public function set_helper( Indexable_Helper $indexable_helper ) { $this->indexable_helper = $indexable_helper; } /** * Removes all ancestors for an indexable. * * @param int $indexable_id The indexable id. * * @return bool Whether or not the indexables were successfully deleted. */ public function clear_ancestors( $indexable_id ) { return $this->query()->where( 'indexable_id', $indexable_id )->delete_many(); } /** * Adds an ancestor to an indexable. * * @param int $indexable_id The indexable id. * @param int $ancestor_id The ancestor id. * @param int $depth The depth. * * @return bool Whether or not the ancestor was added successfully. */ public function add_ancestor( $indexable_id, $ancestor_id, $depth ) { if ( ! $this->indexable_helper->should_index_indexables() ) { return false; } $hierarchy = $this->query()->create( [ 'indexable_id' => $indexable_id, 'ancestor_id' => $ancestor_id, 'depth' => $depth, 'blog_id' => \get_current_blog_id(), ] ); return $hierarchy->save(); } /** * Retrieves the ancestors. Create them when empty. * * @param Indexable $indexable The indexable to get the ancestors for. * * @return int[] The indexable id's of the ancestors in order of grandparent to child. */ public function find_ancestors( Indexable $indexable ) { $ancestors = $this->query() ->select( 'ancestor_id' ) ->where( 'indexable_id', $indexable->id ) ->order_by_desc( 'depth' ) ->find_array(); if ( ! empty( $ancestors ) ) { if ( \count( $ancestors ) === 1 && $ancestors[0]['ancestor_id'] === '0' ) { return []; } return \wp_list_pluck( $ancestors, 'ancestor_id' ); } $indexable = $this->builder->build( $indexable ); return \wp_list_pluck( $indexable->ancestors, 'id' ); } /** * Finds the children for a given indexable. * * @param Indexable $indexable The indexable to find the children for. * * @return array Array with indexable id's for the children. */ public function find_children( Indexable $indexable ) { $children = $this->query() ->select( 'indexable_id' ) ->where( 'ancestor_id', $indexable->id ) ->find_array(); if ( empty( $children ) ) { return []; } return \wp_list_pluck( $children, 'indexable_id' ); } /** * Starts a query for this repository. * * @return ORM */ public function query() { return Model::of_type( 'Indexable_Hierarchy' ); } /** * Finds all the children by given ancestor id's. * * @param array $object_ids List of id's to get the children for. * * @return array List of indexable id's for the children. */ public function find_children_by_ancestor_ids( array $object_ids ) { if ( empty( $object_ids ) ) { return []; } $children = $this->query() ->select( 'indexable_id' ) ->where_in( 'ancestor_id', $object_ids ) ->find_array(); if ( empty( $children ) ) { return []; } return \wp_list_pluck( $children, 'indexable_id' ); } }