diff --git a/admin/class-admin-user-profile.php b/admin/class-admin-user-profile.php index 4579e462f3e..dc6f7b577a2 100644 --- a/admin/class-admin-user-profile.php +++ b/admin/class-admin-user-profile.php @@ -19,23 +19,6 @@ public function __construct() { add_action( 'edit_user_profile', [ $this, 'user_profile' ] ); add_action( 'personal_options_update', [ $this, 'process_user_option_update' ] ); add_action( 'edit_user_profile_update', [ $this, 'process_user_option_update' ] ); - - add_action( 'update_user_meta', [ $this, 'clear_author_sitemap_cache' ], 10, 3 ); - } - - /** - * Clear author sitemap cache when settings are changed. - * - * @since 3.1 - * - * @param int $meta_id The ID of the meta option changed. - * @param int $object_id The ID of the user. - * @param string $meta_key The key of the meta field changed. - */ - public function clear_author_sitemap_cache( $meta_id, $object_id, $meta_key ) { - if ( $meta_key === '_yoast_wpseo_profile_updated' ) { - WPSEO_Sitemaps_Cache::clear( [ 'author' ] ); - } } /** diff --git a/admin/class-admin.php b/admin/class-admin.php index 8036678a054..67c6364f98a 100644 --- a/admin/class-admin.php +++ b/admin/class-admin.php @@ -70,9 +70,6 @@ public function __construct() { add_action( 'admin_init', [ $this, 'map_manage_options_cap' ] ); - WPSEO_Sitemaps_Cache::register_clear_on_option_update( 'wpseo' ); - WPSEO_Sitemaps_Cache::register_clear_on_option_update( 'home' ); - if ( YoastSEO()->helpers->current_page->is_yoast_seo_page() ) { add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] ); } diff --git a/composer.json b/composer.json index 8c80b81bdb6..a54453be962 100644 --- a/composer.json +++ b/composer.json @@ -78,8 +78,8 @@ "Yoast\\WP\\SEO\\Composer\\Actions::check_coding_standards" ], "check-cs-thresholds": [ - "@putenv YOASTCS_THRESHOLD_ERRORS=253", - "@putenv YOASTCS_THRESHOLD_WARNINGS=220", + "@putenv YOASTCS_THRESHOLD_ERRORS=244", + "@putenv YOASTCS_THRESHOLD_WARNINGS=198", "Yoast\\WP\\SEO\\Composer\\Actions::check_cs_thresholds" ], "check-cs": [ diff --git a/inc/class-upgrade.php b/inc/class-upgrade.php index 7460711b34a..dd2f5e3da6c 100644 --- a/inc/class-upgrade.php +++ b/inc/class-upgrade.php @@ -144,9 +144,6 @@ protected function finish_up( $previous_version = null ) { // Just flush rewrites, always, to at least make them work after an upgrade. add_action( 'shutdown', 'flush_rewrite_rules' ); - // Flush the sitemap cache. - WPSEO_Sitemaps_Cache::clear(); - // Make sure all our options always exist - issue #1245. WPSEO_Options::ensure_options_exist(); } @@ -570,9 +567,6 @@ private function upgrade_772() { private function upgrade_90() { global $wpdb; - // Invalidate all sitemap cache transients. - WPSEO_Sitemaps_Cache_Validator::cleanup_database(); - // Removes all scheduled tasks for hitting the sitemap index. wp_clear_scheduled_hook( 'wpseo_hit_sitemap_index' ); diff --git a/inc/sitemaps/abstract-class-indexable-sitemap-provider.php b/inc/sitemaps/abstract-class-indexable-sitemap-provider.php new file mode 100644 index 00000000000..eb8ede16b42 --- /dev/null +++ b/inc/sitemaps/abstract-class-indexable-sitemap-provider.php @@ -0,0 +1,133 @@ +repository = YoastSEO()->classes->get( Indexable_Repository::class ); + $this->xml_sitemap_helper = YoastSEO()->helpers->xml_sitemap; + } + + /** + * Retrieves the links for the sitemap. + * + * @param int $max_entries Entries per sitemap. + * + * @return array + */ + public function get_index_links( $max_entries ) { + global $wpdb; + + $query = $this->repository + ->query_where_noindex( false, $this->get_object_type() ) + ->select( 'id' ) + ->where( 'is_publicly_viewable', true ) + ->order_by_asc( 'object_sub_type' ) + ->order_by_asc( 'object_last_modified' ); + + $excluded_object_ids = $this->get_excluded_object_ids(); + if ( count( $excluded_object_ids ) > 0 ) { + $query->where_not_in( 'object_id', $excluded_object_ids ); + } + + $table_name = Model::get_table_name( 'Indexable' ); + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query is prepared by our ORM. + $raw_query = $wpdb->prepare( $query->get_sql(), $query->get_values() ); + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Complex query is not possible without a direct query. + $last_object_per_page = $wpdb->get_results( + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Variables are secure. + $wpdb->prepare( + // This query pulls only every Nth last_modified from the database, resetting row counts when the sub type changes. + " + SELECT i.object_sub_type, i.object_last_modified + FROM $table_name AS i + INNER JOIN ( + SELECT id + FROM ( + SELECT IF( @previous_sub_type = object_sub_type, @row:=@row+1, @row:=0) AS rownum, @previous_sub_type:=object_sub_type AS previous_sub_type, id + FROM ( $raw_query ) AS sorted, ( SELECT @row:=-1, @previous_sub_type:=null ) AS init + ) AS ranked + WHERE rownum MOD %d = 0 + ) AS subset + ON subset.id = i.id + ", + $max_entries + ) + // phpcs:enable + ); + + $index = []; + $page = 1; + foreach ( $last_object_per_page as $index => $object ) { + if ( $this->should_exclude_object_sub_type( $object->object_sub_type ) ) { + continue; + } + + $next_object_is_not_same_sub_type = ! ( isset( $last_object_per_page[ ( $index + 1 ) ] ) && $last_object_per_page[ ( $index + 1 ) ] === $object->object_sub_type ); + if ( $page === 1 && $next_object_is_not_same_sub_type ) { + $page = ''; + } + + $index[] = [ + 'loc' => WPSEO_Sitemaps_Router::get_base_url( $object->object_sub_type . '-sitemap' . $page . '.xml' ), + 'lastmod' => $object->object_last_modified, + ]; + + if ( $next_object_is_not_same_sub_type ) { + $page = 1; + } + } + + return $index; + } + + /** + * Returns the object type for this sitemap. + * + * @return string The object type. + */ + abstract protected function get_object_type(); + + /** + * Returns a list of all object IDs that should be excluded. + * + * @return int[] + */ + abstract protected function get_excluded_object_ids(); + + /** + * Whether or not a specific object sub type should be excluded. + * + * @param string $object_sub_type The object sub type. + * + * @return boolean Whether or not it should be excluded. + */ + abstract protected function should_exclude_object_sub_type( $object_sub_type ); +} diff --git a/inc/sitemaps/class-author-sitemap-provider.php b/inc/sitemaps/class-author-sitemap-provider.php index 815fb0f3cb3..fa8fe18a6ed 100644 --- a/inc/sitemaps/class-author-sitemap-provider.php +++ b/inc/sitemaps/class-author-sitemap-provider.php @@ -5,14 +5,37 @@ * @package WPSEO\XML_Sitemaps */ -use Yoast\WP\SEO\Helpers\Author_Archive_Helper; -use Yoast\WP\SEO\Helpers\Wordpress_Helper; +use Yoast\WP\Lib\Model; +use Yoast\WP\SEO\Helpers\XML_Sitemap_Helper; +use Yoast\WP\SEO\Repositories\Indexable_Repository; /** * Sitemap provider for author archives. */ class WPSEO_Author_Sitemap_Provider implements WPSEO_Sitemap_Provider { + /** + * The indexable repository. + * + * @var Indexable_Repository + */ + private $repository; + + /** + * The XML sitemap helper. + * + * @var XML_Sitemap_Helper + */ + private $xml_sitemap_helper; + + /** + * Set up object properties for data reuse. + */ + public function __construct() { + $this->repository = YoastSEO()->classes->get( Indexable_Repository::class ); + $this->xml_sitemap_helper = YoastSEO()->helpers->xml_sitemap; + } + /** * Check if provider supports given item type. * @@ -37,110 +60,67 @@ public function handles_type( $type ) { * @return array */ public function get_index_links( $max_entries ) { + global $wpdb; if ( ! $this->handles_type( 'author' ) ) { return []; } - // @todo Consider doing this less often / when necessary. R. - $this->update_user_meta(); - - $has_exclude_filter = has_filter( 'wpseo_sitemap_exclude_author' ); - - $query_arguments = []; - - if ( ! $has_exclude_filter ) { // We only need full users if legacy filter(s) hooked to exclusion logic. R. - $query_arguments['fields'] = 'ID'; - } - - $users = $this->get_users( $query_arguments ); - - if ( $has_exclude_filter ) { - $users = $this->exclude_users( $users ); - $users = wp_list_pluck( $users, 'ID' ); - } + $query = $this->repository + ->query_where_noindex( false, 'user' ) + ->select( 'id' ) + ->order_by_asc( 'object_last_modified' ); - if ( empty( $users ) ) { - return []; + $users_to_exclude = $this->exclude_users(); + if ( is_array( $users_to_exclude ) && count( $users_to_exclude ) > 0 ) { + $query->where_not_in( 'object_id', $users_to_exclude ); } - $index = []; - $page = 1; - $user_pages = array_chunk( $users, $max_entries ); - - if ( count( $user_pages ) === 1 ) { + $table_name = Model::get_table_name( 'Indexable' ); + // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query is prepared by our ORM. + $raw_query = $wpdb->prepare( $query->get_sql(), $query->get_values() ); + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Complex query is not possible without a direct query. + $last_modified_per_page = $wpdb->get_col( + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Variables are secure. + $wpdb->prepare( + // This query pulls only every Nth last_modified from the database. + " + SELECT i.object_last_modified + FROM $table_name AS i + INNER JOIN ( + SELECT id + FROM ( + SELECT @row:=@row+1 AS rownum, id + FROM ( $raw_query ) AS sorted, ( SELECT @row:=-1 ) AS init + ) AS ranked + WHERE rownum MOD %d = 0 + ) AS subset + ON subset.id = i.id + ", + $max_entries + ) + // phpcs:enable + ); + + $page = 1; + if ( count( $last_modified_per_page ) === 1 ) { $page = ''; } - foreach ( $user_pages as $users_page ) { - - $user_id = array_shift( $users_page ); // Time descending, first user on page is most recently updated. - $user = get_user_by( 'id', $user_id ); - $index[] = [ + $index_links = []; + foreach ( $last_modified_per_page as $last_modified ) { + $index_links[] = [ 'loc' => WPSEO_Sitemaps_Router::get_base_url( 'author-sitemap' . $page . '.xml' ), - 'lastmod' => ( $user->_yoast_wpseo_profile_updated ) ? YoastSEO()->helpers->date->format_timestamp( $user->_yoast_wpseo_profile_updated ) : null, + 'lastmod' => $last_modified, ]; - ++$page; - } - - return $index; - } - - /** - * Retrieve users, taking account of all necessary exclusions. - * - * @param array $arguments Arguments to add. - * - * @return array - */ - protected function get_users( $arguments = [] ) { - - global $wpdb; - - $defaults = [ - 'capability' => [ 'edit_posts' ], - 'meta_key' => '_yoast_wpseo_profile_updated', - 'orderby' => 'meta_value_num', - 'order' => 'DESC', - 'meta_query' => [ - 'relation' => 'AND', - [ - 'key' => $wpdb->get_blog_prefix() . 'user_level', - 'value' => '0', - 'compare' => '!=', - ], - [ - 'relation' => 'OR', - [ - 'key' => 'wpseo_noindex_author', - 'value' => 'on', - 'compare' => '!=', - ], - [ - 'key' => 'wpseo_noindex_author', - 'compare' => 'NOT EXISTS', - ], - ], - ], - ]; - - $wordpress_helper = new Wordpress_Helper(); - $wordpress_version = $wordpress_helper->get_wordpress_version(); - - // Capability queries were only introduced in WP 5.9. - if ( version_compare( $wordpress_version, '5.8.99', '<' ) ) { - $defaults['who'] = 'authors'; - unset( $defaults['capability'] ); - } - - if ( WPSEO_Options::get( 'noindex-author-noposts-wpseo', true ) ) { - unset( $defaults['who'], $defaults['capability'] ); // Otherwise it cancels out next argument. - $author_archive = new Author_Archive_Helper(); - $defaults['has_published_posts'] = $author_archive->get_author_archive_post_types(); + if ( is_int( $page ) ) { + ++$page; + } } - return get_users( array_merge( $defaults, $arguments ) ); + return $index_links; } /** @@ -156,116 +136,45 @@ protected function get_users( $arguments = [] ) { */ public function get_sitemap_links( $type, $max_entries, $current_page ) { - $links = []; - if ( ! $this->handles_type( 'author' ) ) { - return $links; - } - - $user_criteria = [ - 'offset' => ( ( $current_page - 1 ) * $max_entries ), - 'number' => $max_entries, - ]; - - $users = $this->get_users( $user_criteria ); - - // Throw an exception when there are no users in the sitemap. - if ( count( $users ) === 0 ) { - throw new OutOfBoundsException( 'Invalid sitemap page requested' ); - } - - $users = $this->exclude_users( $users ); - if ( empty( $users ) ) { - $users = []; - } - - $time = time(); - - foreach ( $users as $user ) { - - $author_link = get_author_posts_url( $user->ID ); - - if ( empty( $author_link ) ) { - continue; - } - - $mod = $time; - - if ( isset( $user->_yoast_wpseo_profile_updated ) ) { - $mod = $user->_yoast_wpseo_profile_updated; - } - - $url = [ - 'loc' => $author_link, - 'mod' => date( DATE_W3C, $mod ), - - // Deprecated, kept for backwards data compat. R. - 'chf' => 'daily', - 'pri' => 1, - ]; - - /** This filter is documented at inc/sitemaps/class-post-type-sitemap-provider.php */ - $url = apply_filters( 'wpseo_sitemap_entry', $url, 'user', $user ); - - if ( ! empty( $url ) ) { - $links[] = $url; - } + return []; } - return $links; - } + $offset = ( ( $current_page - 1 ) * $max_entries ); - /** - * Update any users that don't have last profile update timestamp. - * - * @return int Count of users updated. - */ - protected function update_user_meta() { - - $user_criteria = [ - 'capability' => [ 'edit_posts' ], - 'meta_query' => [ - [ - 'key' => '_yoast_wpseo_profile_updated', - 'compare' => 'NOT EXISTS', - ], - ], - ]; + $query = $this->repository + ->query_where_noindex( false, 'user' ) + ->select_many( 'id', 'object_id', 'permalink', 'object_last_modified' ) + ->order_by_asc( 'object_last_modified' ) + ->offset( $offset ) + ->limit( $max_entries ); - $wordpress_helper = new Wordpress_Helper(); - $wordpress_version = $wordpress_helper->get_wordpress_version(); - - // Capability queries were only introduced in WP 5.9. - if ( version_compare( $wordpress_version, '5.8.99', '<' ) ) { - $user_criteria['who'] = 'authors'; - unset( $user_criteria['capability'] ); + $users_to_exclude = $this->exclude_users(); + if ( count( $users_to_exclude ) > 0 ) { + $query->where_not_in( 'object_id', $users_to_exclude ); } - $users = get_users( $user_criteria ); - - $time = time(); + $indexables = $query->find_many(); - foreach ( $users as $user ) { - update_user_meta( $user->ID, '_yoast_wpseo_profile_updated', $time ); + // Throw an exception when there are no users in the sitemap. + if ( count( $indexables ) === 0 ) { + throw new OutOfBoundsException( 'Invalid sitemap page requested' ); } - return count( $users ); + return $this->xml_sitemap_helper->convert_indexables_to_sitemap_links( $indexables, 'user' ); } /** * Wrap legacy filter to deduplicate calls. * - * @param array $users Array of user objects to filter. - * * @return array */ - protected function exclude_users( $users ) { - + protected function exclude_users() { /** * Filter the authors, included in XML sitemap. * * @param array $users Array of user objects to filter. */ - return apply_filters( 'wpseo_sitemap_exclude_author', $users ); + return apply_filters( 'wpseo_sitemap_exclude_author', [ 0 ] ); } } diff --git a/inc/sitemaps/class-post-type-sitemap-provider.php b/inc/sitemaps/class-post-type-sitemap-provider.php index a552e83d9e9..0ec14e32a88 100644 --- a/inc/sitemaps/class-post-type-sitemap-provider.php +++ b/inc/sitemaps/class-post-type-sitemap-provider.php @@ -5,73 +5,10 @@ * @package WPSEO\XML_Sitemaps */ -use Yoast\WP\SEO\Models\SEO_Links; - /** * Sitemap provider for author archives. */ -class WPSEO_Post_Type_Sitemap_Provider implements WPSEO_Sitemap_Provider { - - /** - * Holds image parser instance. - * - * @var WPSEO_Sitemap_Image_Parser - */ - protected static $image_parser; - - /** - * Holds the parsed home url. - * - * @var array - */ - protected static $parsed_home_url; - - /** - * Determines whether images should be included in the XML sitemap. - * - * @var bool - */ - private $include_images; - - /** - * Set up object properties for data reuse. - */ - public function __construct() { - add_filter( 'save_post', [ $this, 'save_post' ] ); - - /** - * Filter - Allows excluding images from the XML sitemap. - * - * @param bool $include True to include, false to exclude. - */ - $this->include_images = apply_filters( 'wpseo_xml_sitemap_include_images', true ); - } - - /** - * Get the Image Parser. - * - * @return WPSEO_Sitemap_Image_Parser - */ - protected function get_image_parser() { - if ( ! isset( self::$image_parser ) ) { - self::$image_parser = new WPSEO_Sitemap_Image_Parser(); - } - - return self::$image_parser; - } - - /** - * Gets the parsed home url. - * - * @return array The home url, as parsed by wp_parse_url. - */ - protected function get_parsed_home_url() { - if ( ! isset( self::$parsed_home_url ) ) { - self::$parsed_home_url = wp_parse_url( home_url() ); - } - - return self::$parsed_home_url; - } +class WPSEO_Post_Type_Sitemap_Provider extends WPSEO_Indexable_Sitemap_Provider implements WPSEO_Sitemap_Provider { /** * Check if provider supports given item type. @@ -81,75 +18,41 @@ protected function get_parsed_home_url() { * @return bool */ public function handles_type( $type ) { - return post_type_exists( $type ); } /** - * Retrieves the sitemap links. + * Returns the object type for this sitemap. * - * @param int $max_entries Entries per sitemap. - * - * @return array + * @return string The object type. */ - public function get_index_links( $max_entries ) { - global $wpdb; - - $post_types = WPSEO_Post_Type::get_accessible_post_types(); - $post_types = array_filter( $post_types, [ $this, 'is_valid_post_type' ] ); - $last_modified_times = WPSEO_Sitemaps::get_last_modified_gmt( $post_types, true ); - $index = []; - - foreach ( $post_types as $post_type ) { - - $total_count = $this->get_post_type_count( $post_type ); - - $max_pages = 1; - if ( $total_count > $max_entries ) { - $max_pages = (int) ceil( $total_count / $max_entries ); - } - - $all_dates = []; - - if ( $max_pages > 1 ) { - $post_statuses = array_map( 'esc_sql', WPSEO_Sitemaps::get_post_statuses( $post_type ) ); - - $sql = " - SELECT post_modified_gmt - FROM ( SELECT @rownum:=0 ) init - JOIN {$wpdb->posts} USE INDEX( type_status_date ) - WHERE post_status IN ('" . implode( "','", $post_statuses ) . "') - AND post_type = %s - AND ( @rownum:=@rownum+1 ) %% %d = 0 - ORDER BY post_modified_gmt ASC - "; - - $all_dates = $wpdb->get_col( $wpdb->prepare( $sql, $post_type, $max_entries ) ); - } - - for ( $page_counter = 0; $page_counter < $max_pages; $page_counter++ ) { - - $current_page = ( $max_pages > 1 ) ? ( $page_counter + 1 ) : ''; - $date = false; - - if ( empty( $current_page ) || $current_page === $max_pages ) { + protected function get_object_type() { + return 'post'; + } - if ( ! empty( $last_modified_times[ $post_type ] ) ) { - $date = $last_modified_times[ $post_type ]; - } - } - else { - $date = $all_dates[ $page_counter ]; - } + /** + * Whether or not a specific object sub type should be excluded. + * + * @param string $object_sub_type The object sub type. + * + * @return boolean Whether or not it should be excluded. + */ + protected function should_exclude_object_sub_type( $object_sub_type ) { + /** + * Filter decision if post type is excluded from the XML sitemap. + * + * @param bool $exclude Default false. + * @param string $post_type Post type name. + */ + if ( apply_filters( 'wpseo_sitemap_exclude_post_type', false, $object_sub_type ) ) { + return true; + } - $index[] = [ - 'loc' => WPSEO_Sitemaps_Router::get_base_url( $post_type . '-sitemap' . $current_page . '.xml' ), - 'lastmod' => $date, - ]; - } + if ( ! WPSEO_Post_Type::is_post_type_accessible( $object_sub_type ) || ! WPSEO_Post_Type::is_post_type_indexable( $object_sub_type ) ) { + return true; } - return $index; + return false; } /** @@ -159,100 +62,64 @@ public function get_index_links( $max_entries ) { * @param int $max_entries Entries per sitemap. * @param int $current_page Current page of the sitemap. * - * @return array + * @return array The links. * * @throws OutOfBoundsException When an invalid page is requested. */ public function get_sitemap_links( $type, $max_entries, $current_page ) { - - $links = []; - $post_type = $type; - - if ( ! $this->is_valid_post_type( $post_type ) ) { + if ( ! $this->is_valid_post_type( $type ) ) { throw new OutOfBoundsException( 'Invalid sitemap page requested' ); } - $steps = min( 100, $max_entries ); $offset = ( $current_page > 1 ) ? ( ( $current_page - 1 ) * $max_entries ) : 0; - $total = ( $offset + $max_entries ); - - $post_type_entries = $this->get_post_type_count( $post_type ); - - if ( $total > $post_type_entries ) { - $total = $post_type_entries; - } - - if ( $current_page === 1 ) { - $links = array_merge( $links, $this->get_first_links( $post_type ) ); - } - - // If total post type count is lower than the offset, an invalid page is requested. - if ( $post_type_entries < $offset ) { - throw new OutOfBoundsException( 'Invalid sitemap page requested' ); - } - if ( $post_type_entries === 0 ) { - return $links; - } + $query = $this->repository + ->query_where_noindex( false, 'post', $type ) + ->select_many( 'id', 'object_id', 'permalink', 'object_last_modified' ) + ->where( 'is_publicly_viewable', true ) + ->order_by_asc( 'object_last_modified' ) + ->offset( $offset ) + ->limit( $max_entries ); $posts_to_exclude = $this->get_excluded_posts( $type ); + if ( count( $posts_to_exclude ) > 0 ) { + $query->where_not_in( 'object_id', $posts_to_exclude ); + } - while ( $total > $offset ) { - - $posts = $this->get_posts( $post_type, $steps, $offset ); - - $offset += $steps; - - if ( empty( $posts ) ) { - continue; - } - - foreach ( $posts as $post ) { - - if ( in_array( $post->ID, $posts_to_exclude, true ) ) { - continue; - } - - if ( WPSEO_Meta::get_value( 'meta-robots-noindex', $post->ID ) === '1' ) { - continue; - } - - $url = $this->get_url( $post ); + $indexables = $query->find_many(); - if ( ! isset( $url['loc'] ) ) { - continue; + // Add the home page or archive to the first page. + if ( $current_page === 1 ) { + if ( $type === 'page' ) { + $home_page = $this->repository + ->query_where_noindex( false, 'home-page' ) + ->select_many( 'id', 'object_id', 'permalink', 'object_last_modified' ) + ->where( 'is_publicly_viewable', true ) + ->find_one(); + if ( $home_page ) { + // Prepend homepage. + array_unshift( $indexables, $home_page ); } - - /** - * Filter URL entry before it gets added to the sitemap. - * - * @param array $url Array of URL parts. - * @param string $type URL type. - * @param object $post Data object for the URL. - */ - $url = apply_filters( 'wpseo_sitemap_entry', $url, 'post', $post ); - - if ( ! empty( $url ) ) { - $links[] = $url; + } + else { + $archive_page = $this->repository + ->query_where_noindex( false, 'post-type-archive', $type ) + ->select_many( 'id', 'object_id', 'permalink', 'object_last_modified' ) + ->where( 'is_publicly_viewable', true ) + ->find_one(); + if ( $archive_page ) { + // Prepend archive. + array_unshift( $indexables, $archive_page ); } } - - unset( $post, $url ); } - return $links; - } - - /** - * Check for relevant post type before invalidation. - * - * @param int $post_id Post ID to possibly invalidate for. - */ - public function save_post( $post_id ) { - - if ( $this->is_valid_post_type( get_post_type( $post_id ) ) ) { - WPSEO_Sitemaps_Cache::invalidate_post( $post_id ); + // If total post type count is lower than the offset, an invalid page is requested. + if ( count( $indexables ) === 0 ) { + throw new OutOfBoundsException( 'Invalid sitemap page requested' ); } + + return $this->xml_sitemap_helper->convert_indexables_to_sitemap_links( $indexables, 'post' ); } /** @@ -283,18 +150,11 @@ public function is_valid_post_type( $post_type ) { /** * Retrieves a list with the excluded post ids. * - * @param string $post_type Post type. - * * @return array Array with post ids to exclude. */ - protected function get_excluded_posts( $post_type ) { + protected function get_excluded_object_ids() { $excluded_posts_ids = []; - $page_on_front_id = ( $post_type === 'page' ) ? (int) get_option( 'page_on_front' ) : 0; - if ( $page_on_front_id > 0 ) { - $excluded_posts_ids[] = $page_on_front_id; - } - /** * Filter: 'wpseo_exclude_from_sitemap_by_post_ids' - Allow extending and modifying the posts to exclude. * @@ -307,351 +167,6 @@ protected function get_excluded_posts( $post_type ) { $excluded_posts_ids = array_map( 'intval', $excluded_posts_ids ); - $page_for_posts_id = ( $post_type === 'page' ) ? (int) get_option( 'page_for_posts' ) : 0; - if ( $page_for_posts_id > 0 ) { - $excluded_posts_ids[] = $page_for_posts_id; - } - return array_unique( $excluded_posts_ids ); } - - /** - * Get count of posts for post type. - * - * @param string $post_type Post type to retrieve count for. - * - * @return int - */ - protected function get_post_type_count( $post_type ) { - - global $wpdb; - - /** - * Filter JOIN query part for type count of post type. - * - * @param string $join SQL part, defaults to empty string. - * @param string $post_type Post type name. - */ - $join_filter = apply_filters( 'wpseo_typecount_join', '', $post_type ); - - /** - * Filter WHERE query part for type count of post type. - * - * @param string $where SQL part, defaults to empty string. - * @param string $post_type Post type name. - */ - $where_filter = apply_filters( 'wpseo_typecount_where', '', $post_type ); - - $where = $this->get_sql_where_clause( $post_type ); - - $sql = " - SELECT COUNT({$wpdb->posts}.ID) - FROM {$wpdb->posts} - {$join_filter} - {$where} - {$where_filter} - "; - - return (int) $wpdb->get_var( $sql ); - } - - /** - * Produces set of links to prepend at start of first sitemap page. - * - * @param string $post_type Post type to produce links for. - * - * @return array - */ - protected function get_first_links( $post_type ) { - - $links = []; - $archive_url = false; - - if ( $post_type === 'page' ) { - - $page_on_front_id = (int) get_option( 'page_on_front' ); - if ( $page_on_front_id > 0 ) { - $front_page = $this->get_url( - get_post( $page_on_front_id ) - ); - } - - if ( empty( $front_page ) ) { - $front_page = [ - 'loc' => YoastSEO()->helpers->url->home(), - ]; - } - - // Deprecated, kept for backwards data compat. R. - $front_page['chf'] = 'daily'; - $front_page['pri'] = 1; - - $links[] = $front_page; - } - elseif ( $post_type !== 'page' ) { - /** - * Filter the URL Yoast SEO uses in the XML sitemap for this post type archive. - * - * @param string $archive_url The URL of this archive - * @param string $post_type The post type this archive is for. - */ - $archive_url = apply_filters( - 'wpseo_sitemap_post_type_archive_link', - $this->get_post_type_archive_link( $post_type ), - $post_type - ); - } - - if ( $archive_url ) { - - $links[] = [ - 'loc' => $archive_url, - 'mod' => WPSEO_Sitemaps::get_last_modified_gmt( $post_type ), - - // Deprecated, kept for backwards data compat. R. - 'chf' => 'daily', - 'pri' => 1, - ]; - } - - return $links; - } - - /** - * Get URL for a post type archive. - * - * @since 5.3 - * - * @param string $post_type Post type. - * - * @return string|bool URL or false if it should be excluded. - */ - protected function get_post_type_archive_link( $post_type ) { - - $pt_archive_page_id = -1; - - if ( $post_type === 'post' ) { - - if ( get_option( 'show_on_front' ) === 'posts' ) { - return YoastSEO()->helpers->url->home(); - } - - $pt_archive_page_id = (int) get_option( 'page_for_posts' ); - - // Post archive should be excluded if posts page isn't set. - if ( $pt_archive_page_id <= 0 ) { - return false; - } - } - - if ( ! $this->is_post_type_archive_indexable( $post_type, $pt_archive_page_id ) ) { - return false; - } - - return get_post_type_archive_link( $post_type ); - } - - /** - * Determines whether a post type archive is indexable. - * - * @since 11.5 - * - * @param string $post_type Post type. - * @param int $archive_page_id The page id. - * - * @return bool True when post type archive is indexable. - */ - protected function is_post_type_archive_indexable( $post_type, $archive_page_id = -1 ) { - - if ( WPSEO_Options::get( 'noindex-ptarchive-' . $post_type, false ) ) { - return false; - } - - /** - * Filter the page which is dedicated to this post type archive. - * - * @since 9.3 - * - * @param string $archive_page_id The post_id of the page. - * @param string $post_type The post type this archive is for. - */ - $archive_page_id = (int) apply_filters( 'wpseo_sitemap_page_for_post_type_archive', $archive_page_id, $post_type ); - - if ( $archive_page_id > 0 && WPSEO_Meta::get_value( 'meta-robots-noindex', $archive_page_id ) === '1' ) { - return false; - } - - return true; - } - - /** - * Retrieve set of posts with optimized query routine. - * - * @param string $post_type Post type to retrieve. - * @param int $count Count of posts to retrieve. - * @param int $offset Starting offset. - * - * @return object[] - */ - protected function get_posts( $post_type, $count, $offset ) { - - global $wpdb; - - static $filters = []; - - if ( ! isset( $filters[ $post_type ] ) ) { - // Make sure you're wpdb->preparing everything you throw into this!! - $filters[ $post_type ] = [ - /** - * Filter JOIN query part for the post type. - * - * @param string $join SQL part, defaults to false. - * @param string $post_type Post type name. - */ - 'join' => apply_filters( 'wpseo_posts_join', false, $post_type ), - - /** - * Filter WHERE query part for the post type. - * - * @param string $where SQL part, defaults to false. - * @param string $post_type Post type name. - */ - 'where' => apply_filters( 'wpseo_posts_where', false, $post_type ), - ]; - } - - $join_filter = $filters[ $post_type ]['join']; - $where_filter = $filters[ $post_type ]['where']; - $where = $this->get_sql_where_clause( $post_type ); - - /* - * Optimized query per this thread: - * {@link http://wordpress.org/support/topic/plugin-wordpress-seo-by-yoast-performance-suggestion}. - * Also see {@link http://explainextended.com/2009/10/23/mysql-order-by-limit-performance-late-row-lookups/}. - */ - $sql = " - SELECT l.ID, post_title, post_content, post_name, post_parent, post_author, post_status, post_modified_gmt, post_date, post_date_gmt - FROM ( - SELECT {$wpdb->posts}.ID - FROM {$wpdb->posts} - {$join_filter} - {$where} - {$where_filter} - ORDER BY {$wpdb->posts}.post_modified ASC LIMIT %d OFFSET %d - ) - o JOIN {$wpdb->posts} l ON l.ID = o.ID - "; - - $posts = $wpdb->get_results( $wpdb->prepare( $sql, $count, $offset ) ); - - $post_ids = []; - - foreach ( $posts as $post_index => $post ) { - $post->post_type = $post_type; - $sanitized_post = sanitize_post( $post, 'raw' ); - $posts[ $post_index ] = new WP_Post( $sanitized_post ); - - $post_ids[] = $sanitized_post->ID; - } - - update_meta_cache( 'post', $post_ids ); - - return $posts; - } - - /** - * Constructs an SQL where clause for a given post type. - * - * @param string $post_type Post type slug. - * - * @return string - */ - protected function get_sql_where_clause( $post_type ) { - - global $wpdb; - - $join = ''; - $post_statuses = array_map( 'esc_sql', WPSEO_Sitemaps::get_post_statuses( $post_type ) ); - $status_where = "{$wpdb->posts}.post_status IN ('" . implode( "','", $post_statuses ) . "')"; - - // Based on WP_Query->get_posts(). R. - if ( $post_type === 'attachment' ) { - $join = " LEFT JOIN {$wpdb->posts} AS p2 ON ({$wpdb->posts}.post_parent = p2.ID) "; - $parent_statuses = array_diff( $post_statuses, [ 'inherit' ] ); - $status_where = "p2.post_status IN ('" . implode( "','", $parent_statuses ) . "') AND p2.post_password = ''"; - } - - $where_clause = " - {$join} - WHERE {$status_where} - AND {$wpdb->posts}.post_type = %s - AND {$wpdb->posts}.post_password = '' - AND {$wpdb->posts}.post_date != '0000-00-00 00:00:00' - "; - - return $wpdb->prepare( $where_clause, $post_type ); - } - - /** - * Produce array of URL parts for given post object. - * - * @param object $post Post object to get URL parts for. - * - * @return array|bool - */ - protected function get_url( $post ) { - - $url = []; - - /** - * Filter the URL Yoast SEO uses in the XML sitemap. - * - * Note that only absolute local URLs are allowed as the check after this removes external URLs. - * - * @param string $url URL to use in the XML sitemap - * @param object $post Post object for the URL. - */ - $url['loc'] = apply_filters( 'wpseo_xml_sitemap_post_url', get_permalink( $post ), $post ); - $link_type = YoastSEO()->helpers->url->get_link_type( - wp_parse_url( $url['loc'] ), - $this->get_parsed_home_url() - ); - - /* - * Do not include external URLs. - * - * {@link https://wordpress.org/plugins/page-links-to/} can rewrite permalinks to external URLs. - */ - if ( $link_type === SEO_Links::TYPE_EXTERNAL ) { - return false; - } - - $modified = max( $post->post_modified_gmt, $post->post_date_gmt ); - - if ( $modified !== '0000-00-00 00:00:00' ) { - $url['mod'] = $modified; - } - - $url['chf'] = 'daily'; // Deprecated, kept for backwards data compat. R. - - $canonical = WPSEO_Meta::get_value( 'canonical', $post->ID ); - - if ( $canonical !== '' && $canonical !== $url['loc'] ) { - /* - * Let's assume that if a canonical is set for this page and it's different from - * the URL of this post, that page is either already in the XML sitemap OR is on - * an external site, either way, we shouldn't include it here. - */ - return false; - } - unset( $canonical ); - - $url['pri'] = 1; // Deprecated, kept for backwards data compat. R. - - if ( $this->include_images ) { - $url['images'] = $this->get_image_parser()->get_images( $post ); - } - - return $url; - } } diff --git a/inc/sitemaps/class-sitemap-cache-data.php b/inc/sitemaps/class-sitemap-cache-data.php deleted file mode 100644 index 5490e0b7863..00000000000 --- a/inc/sitemaps/class-sitemap-cache-data.php +++ /dev/null @@ -1,198 +0,0 @@ -sitemap = $sitemap; - - /* - * Empty sitemap is not usable. - */ - if ( ! empty( $sitemap ) ) { - $this->set_status( self::OK ); - } - else { - $this->set_status( self::ERROR ); - } - } - - /** - * Set the status of the sitemap, is it usable. - * - * @param bool|string $valid Is the sitemap valid or not. - * - * @return void - */ - public function set_status( $valid ) { - - if ( $valid === self::OK ) { - $this->status = self::OK; - - return; - } - - if ( $valid === self::ERROR ) { - $this->status = self::ERROR; - $this->sitemap = ''; - - return; - } - - $this->status = self::UNKNOWN; - } - - /** - * Is the sitemap usable. - * - * @return bool True if usable, False if bad or unknown. - */ - public function is_usable() { - - return $this->status === self::OK; - } - - /** - * Get the XML content of the sitemap. - * - * @return string The content of the sitemap. - */ - public function get_sitemap() { - - return $this->sitemap; - } - - /** - * Get the status of the sitemap. - * - * @return string Status of the sitemap, 'ok'/'error'/'unknown'. - */ - public function get_status() { - - return $this->status; - } - - /** - * String representation of object. - * - * {@internal This magic method is only "magic" as of PHP 7.4 in which the magic method was introduced.} - * - * @link https://www.php.net/language.oop5.magic#object.serialize - * @link https://wiki.php.net/rfc/custom_object_serialization - * - * @since 17.8.0 - * - * @return array The data to be serialized. - */ - public function __serialize() { // phpcs:ignore PHPCompatibility.FunctionNameRestrictions.NewMagicMethods.__serializeFound - - $data = [ - 'status' => $this->status, - 'xml' => $this->sitemap, - ]; - - return $data; - } - - /** - * Constructs the object. - * - * {@internal This magic method is only "magic" as of PHP 7.4 in which the magic method was introduced.} - * - * @link https://www.php.net/language.oop5.magic#object.serialize - * @link https://wiki.php.net/rfc/custom_object_serialization - * - * @since 17.8.0 - * - * @param array $data The unserialized data to use to (re)construct the object. - * - * @return void - */ - public function __unserialize( $data ) { // phpcs:ignore PHPCompatibility.FunctionNameRestrictions.NewMagicMethods.__unserializeFound - - $this->set_sitemap( $data['xml'] ); - $this->set_status( $data['status'] ); - } - - /** - * String representation of object. - * - * {@internal The magic methods take precedence over the Serializable interface. - * This means that in practice, this method will now only be called on PHP < 7.4. - * For PHP 7.4 and higher, the magic methods will be used instead.} - * - * {@internal The Serializable interface is being phased out, in favour of the magic methods. - * This method should be deprecated and removed and the class should no longer - * implement the `Serializable` interface. - * This change, however, can't be made until the minimum PHP version goes up to PHP 7.4 or higher.} - * - * @link http://php.net/manual/en/serializable.serialize.php - * @link https://wiki.php.net/rfc/phase_out_serializable - * - * @since 5.1.0 - * - * @return string The string representation of the object or null in C-format. - */ - public function serialize() { - - return serialize( $this->__serialize() ); - } - - /** - * Constructs the object. - * - * {@internal The magic methods take precedence over the Serializable interface. - * This means that in practice, this method will now only be called on PHP < 7.4. - * For PHP 7.4 and higher, the magic methods will be used instead.} - * - * {@internal The Serializable interface is being phased out, in favour of the magic methods. - * This method should be deprecated and removed and the class should no longer - * implement the `Serializable` interface. - * This change, however, can't be made until the minimum PHP version goes up to PHP 7.4 or higher.} - * - * @link http://php.net/manual/en/serializable.unserialize.php - * @link https://wiki.php.net/rfc/phase_out_serializable - * - * @since 5.1.0 - * - * @param string $data The string representation of the object in C or O-format. - * - * @return void - */ - public function unserialize( $data ) { - - $data = unserialize( $data ); - $this->__unserialize( $data ); - } -} diff --git a/inc/sitemaps/class-sitemap-image-parser.php b/inc/sitemaps/class-sitemap-image-parser.php deleted file mode 100644 index 4ae581b9bcc..00000000000 --- a/inc/sitemaps/class-sitemap-image-parser.php +++ /dev/null @@ -1,510 +0,0 @@ -home_url = home_url(); - $parsed_home = wp_parse_url( $this->home_url ); - - if ( ! empty( $parsed_home['host'] ) ) { - $this->host = str_replace( 'www.', '', $parsed_home['host'] ); - } - - if ( ! empty( $parsed_home['scheme'] ) ) { - $this->scheme = $parsed_home['scheme']; - } - - $this->charset = esc_attr( get_bloginfo( 'charset' ) ); - } - - /** - * Get set of image data sets for the given post. - * - * @param object $post Post object to get images for. - * - * @return array - */ - public function get_images( $post ) { - - $images = []; - - if ( ! is_object( $post ) ) { - return $images; - } - - $thumbnail_id = get_post_thumbnail_id( $post->ID ); - - if ( $thumbnail_id ) { - - $src = $this->get_absolute_url( $this->image_url( $thumbnail_id ) ); - $alt = WPSEO_Image_Utils::get_alt_tag( $thumbnail_id ); - $title = get_post_field( 'post_title', $thumbnail_id ); - $images[] = $this->get_image_item( $post, $src, $title, $alt ); - } - - /** - * Filter: 'wpseo_sitemap_content_before_parse_html_images' - Filters the post content - * before it is parsed for images. - * - * @param string $content The raw/unprocessed post content. - */ - $content = apply_filters( 'wpseo_sitemap_content_before_parse_html_images', $post->post_content ); - - $unfiltered_images = $this->parse_html_images( $content ); - - foreach ( $unfiltered_images as $image ) { - $images[] = $this->get_image_item( $post, $image['src'], $image['title'], $image['alt'] ); - } - - foreach ( $this->parse_galleries( $content, $post->ID ) as $attachment ) { - - $src = $this->get_absolute_url( $this->image_url( $attachment->ID ) ); - $alt = WPSEO_Image_Utils::get_alt_tag( $attachment->ID ); - - $images[] = $this->get_image_item( $post, $src, $attachment->post_title, $alt ); - } - - if ( $post->post_type === 'attachment' && wp_attachment_is_image( $post ) ) { - - $src = $this->get_absolute_url( $this->image_url( $post->ID ) ); - $alt = WPSEO_Image_Utils::get_alt_tag( $post->ID ); - - $images[] = $this->get_image_item( $post, $src, $post->post_title, $alt ); - } - - foreach ( $images as $key => $image ) { - - if ( empty( $image['src'] ) ) { - unset( $images[ $key ] ); - } - } - - /** - * Filter images to be included for the post in XML sitemap. - * - * @param array $images Array of image items. - * @param int $post_id ID of the post. - */ - $images = apply_filters( 'wpseo_sitemap_urlimages', $images, $post->ID ); - - return $images; - } - - /** - * Get the images in the term description. - * - * @param object $term Term to get images from description for. - * - * @return array - */ - public function get_term_images( $term ) { - - $images = $this->parse_html_images( $term->description ); - - foreach ( $this->parse_galleries( $term->description ) as $attachment ) { - - $images[] = [ - 'src' => $this->get_absolute_url( $this->image_url( $attachment->ID ) ), - 'title' => $attachment->post_title, - 'alt' => WPSEO_Image_Utils::get_alt_tag( $attachment->ID ), - ]; - } - - return $images; - } - - /** - * Parse `` tags in content. - * - * @param string $content Content string to parse. - * - * @return array - */ - private function parse_html_images( $content ) { - - $images = []; - - if ( ! class_exists( 'DOMDocument' ) ) { - return $images; - } - - if ( empty( $content ) ) { - return $images; - } - - // Prevent DOMDocument from bubbling warnings about invalid HTML. - libxml_use_internal_errors( true ); - - $post_dom = new DOMDocument(); - $post_dom->loadHTML( 'charset . '">' . $content ); - - // Clear the errors, so they don't get kept in memory. - libxml_clear_errors(); - - /** @var DOMElement $img */ - foreach ( $post_dom->getElementsByTagName( 'img' ) as $img ) { - - $src = $img->getAttribute( 'src' ); - - if ( empty( $src ) ) { - continue; - } - - $class = $img->getAttribute( 'class' ); - - if ( // This detects WP-inserted images, which we need to upsize. R. - ! empty( $class ) - && ( strpos( $class, 'size-full' ) === false ) - && preg_match( '|wp-image-(?P\d+)|', $class, $matches ) - && get_post_status( $matches['id'] ) - ) { - $src = $this->image_url( $matches['id'] ); - } - - $src = $this->get_absolute_url( $src ); - - if ( strpos( $src, $this->host ) === false ) { - continue; - } - - if ( $src !== esc_url( $src ) ) { - continue; - } - - $images[] = [ - 'src' => $src, - 'title' => $img->getAttribute( 'title' ), - 'alt' => $img->getAttribute( 'alt' ), - ]; - } - - return $images; - } - - /** - * Parse gallery shortcodes in a given content. - * - * @param string $content Content string. - * @param int $post_id Optional. ID of post being parsed. - * - * @return array Set of attachment objects. - */ - protected function parse_galleries( $content, $post_id = 0 ) { - - $attachments = []; - $galleries = $this->get_content_galleries( $content ); - - foreach ( $galleries as $gallery ) { - - $id = $post_id; - - if ( ! empty( $gallery['id'] ) ) { - $id = intval( $gallery['id'] ); - } - - // Forked from core gallery_shortcode() to have exact same logic. R. - if ( ! empty( $gallery['ids'] ) ) { - $gallery['include'] = $gallery['ids']; - } - - $gallery_attachments = $this->get_gallery_attachments( $id, $gallery ); - - $attachments = array_merge( $attachments, $gallery_attachments ); - } - - return array_unique( $attachments, SORT_REGULAR ); - } - - /** - * Retrieves galleries from the passed content. - * - * Forked from core to skip executing shortcodes for performance. - * - * @param string $content Content to parse for shortcodes. - * - * @return array A list of arrays, each containing gallery data. - */ - protected function get_content_galleries( $content ) { - - $galleries = []; - - if ( ! preg_match_all( '/' . get_shortcode_regex( [ 'gallery' ] ) . '/s', $content, $matches, PREG_SET_ORDER ) ) { - return $galleries; - } - - foreach ( $matches as $shortcode ) { - - $attributes = shortcode_parse_atts( $shortcode[3] ); - - if ( $attributes === '' ) { // Valid shortcode without any attributes. R. - $attributes = []; - } - - $galleries[] = $attributes; - } - - return $galleries; - } - - /** - * Get image item array with filters applied. - * - * @param WP_Post $post Post object for the context. - * @param string $src Image URL. - * @param string $title Optional image title. - * @param string $alt Optional image alt text. - * - * @return array - */ - protected function get_image_item( $post, $src, $title = '', $alt = '' ) { - - $image = []; - - /** - * Filter image URL to be included in XML sitemap for the post. - * - * @param string $src Image URL. - * @param object $post Post object. - */ - $image['src'] = apply_filters( 'wpseo_xml_sitemap_img_src', $src, $post ); - - if ( ! empty( $title ) ) { - $image['title'] = $title; - } - - if ( ! empty( $alt ) ) { - $image['alt'] = $alt; - } - - /** - * Filter image data to be included in XML sitemap for the post. - * - * @param array $image { - * Array of image data. - * - * @type string $src Image URL. - * @type string $title Image title attribute (optional). - * @type string $alt Image alt attribute (optional). - * } - * - * @param object $post Post object. - */ - return apply_filters( 'wpseo_xml_sitemap_img', $image, $post ); - } - - /** - * Get attached image URL with filters applied. Adapted from core for speed. - * - * @param int $post_id ID of the post. - * - * @return string - */ - private function image_url( $post_id ) { - - static $uploads; - - if ( empty( $uploads ) ) { - $uploads = wp_upload_dir(); - } - - if ( $uploads['error'] !== false ) { - return ''; - } - - $file = get_post_meta( $post_id, '_wp_attached_file', true ); - - if ( empty( $file ) ) { - return ''; - } - - // Check that the upload base exists in the file location. - if ( strpos( $file, $uploads['basedir'] ) === 0 ) { - $src = str_replace( $uploads['basedir'], $uploads['baseurl'], $file ); - } - elseif ( strpos( $file, 'wp-content/uploads' ) !== false ) { - $src = $uploads['baseurl'] . substr( $file, ( strpos( $file, 'wp-content/uploads' ) + 18 ) ); - } - else { - // It's a newly uploaded file, therefore $file is relative to the baseurl. - $src = $uploads['baseurl'] . '/' . $file; - } - - return apply_filters( 'wp_get_attachment_url', $src, $post_id ); - } - - /** - * Make absolute URL for domain or protocol-relative one. - * - * @param string $src URL to process. - * - * @return string - */ - protected function get_absolute_url( $src ) { - - if ( empty( $src ) || ! is_string( $src ) ) { - return $src; - } - - if ( YoastSEO()->helpers->url->is_relative( $src ) === true ) { - - if ( $src[0] !== '/' ) { - return $src; - } - - // The URL is relative, we'll have to make it absolute. - return $this->home_url . $src; - } - - if ( strpos( $src, 'http' ) !== 0 ) { - // Protocol relative URL, we add the scheme as the standard requires a protocol. - return $this->scheme . ':' . $src; - } - - return $src; - } - - /** - * Returns the attachments for a gallery. - * - * @param int $id The post ID. - * @param array $gallery The gallery config. - * - * @return array The selected attachments. - */ - protected function get_gallery_attachments( $id, $gallery ) { - - // When there are attachments to include. - if ( ! empty( $gallery['include'] ) ) { - return $this->get_gallery_attachments_for_included( $gallery['include'] ); - } - - // When $id is empty, just return empty array. - if ( empty( $id ) ) { - return []; - } - - return $this->get_gallery_attachments_for_parent( $id, $gallery ); - } - - /** - * Returns the attachments for the given ID. - * - * @param int $id The post ID. - * @param array $gallery The gallery config. - * - * @return array The selected attachments. - */ - protected function get_gallery_attachments_for_parent( $id, $gallery ) { - $query = [ - 'posts_per_page' => -1, - 'post_parent' => $id, - ]; - - // When there are posts that should be excluded from result set. - if ( ! empty( $gallery['exclude'] ) ) { - $query['post__not_in'] = wp_parse_id_list( $gallery['exclude'] ); - } - - return $this->get_attachments( $query ); - } - - /** - * Returns an array with attachments for the post IDs that will be included. - * - * @param array $included_ids Array with IDs to include. - * - * @return array The found attachments. - */ - protected function get_gallery_attachments_for_included( $included_ids ) { - $ids_to_include = wp_parse_id_list( $included_ids ); - $attachments = $this->get_attachments( - [ - 'posts_per_page' => count( $ids_to_include ), - 'post__in' => $ids_to_include, - ] - ); - - $gallery_attachments = []; - foreach ( $attachments as $key => $val ) { - $gallery_attachments[ $val->ID ] = $val; - } - - return $gallery_attachments; - } - - /** - * Returns the attachments. - * - * @param array $args Array with query args. - * - * @return array The found attachments. - */ - protected function get_attachments( $args ) { - $default_args = [ - 'post_status' => 'inherit', - 'post_type' => 'attachment', - 'post_mime_type' => 'image', - - // Defaults taken from function get_posts. - 'orderby' => 'date', - 'order' => 'DESC', - 'meta_key' => '', - 'meta_value' => '', - 'suppress_filters' => true, - 'ignore_sticky_posts' => true, - 'no_found_rows' => true, - ]; - - $args = wp_parse_args( $args, $default_args ); - - $get_attachments = new WP_Query(); - return $get_attachments->query( $args ); - } -} diff --git a/inc/sitemaps/class-sitemaps-admin.php b/inc/sitemaps/class-sitemaps-admin.php index 1b6e2433099..cd42f7f54f9 100644 --- a/inc/sitemaps/class-sitemaps-admin.php +++ b/inc/sitemaps/class-sitemaps-admin.php @@ -23,9 +23,6 @@ class WPSEO_Sitemaps_Admin { public function __construct() { add_action( 'transition_post_status', [ $this, 'status_transition' ], 10, 3 ); add_action( 'admin_footer', [ $this, 'status_transition_bulk_finished' ] ); - - WPSEO_Sitemaps_Cache::register_clear_on_option_update( 'wpseo_titles', '' ); - WPSEO_Sitemaps_Cache::register_clear_on_option_update( 'wpseo', '' ); } /** @@ -50,8 +47,6 @@ public function status_transition( $new_status, $old_status, $post ) { $post_type = get_post_type( $post ); - wp_cache_delete( 'lastpostmodified:gmt:' . $post_type, 'timeinfo' ); // #17455. - // Not something we're interested in. if ( $post_type === 'nav_menu_item' ) { return; @@ -113,9 +108,7 @@ public function status_transition_bulk_finished() { $ping_search_engines = false; foreach ( $this->importing_post_types as $post_type ) { - wp_cache_delete( 'lastpostmodified:gmt:' . $post_type, 'timeinfo' ); // #17455. - - // Just have the cache deleted for nav_menu_item. + // Don't ping for nav_menu_items. if ( $post_type === 'nav_menu_item' ) { continue; } @@ -130,10 +123,6 @@ public function status_transition_bulk_finished() { return; } - if ( WP_CACHE ) { - do_action( 'wpseo_hit_sitemap_index' ); - } - WPSEO_Sitemaps::ping_search_engines(); } } diff --git a/inc/sitemaps/class-sitemaps-cache-validator.php b/inc/sitemaps/class-sitemaps-cache-validator.php deleted file mode 100644 index acb56c56af3..00000000000 --- a/inc/sitemaps/class-sitemaps-cache-validator.php +++ /dev/null @@ -1,319 +0,0 @@ - $max_length ) { - - if ( $max_length < 15 ) { - /* - * If this happens the most likely cause is a page number that is too high. - * - * So this would not happen unintentionally. - * Either by trying to cause a high server load, finding backdoors or misconfiguration. - */ - throw new OutOfRangeException( - __( - 'Trying to build the sitemap cache key, but the postfix and prefix combination leaves too little room to do this. You are probably requesting a page that is way out of the expected range.', - 'wordpress-seo' - ) - ); - } - - $half = ( $max_length / 2 ); - - $first_part = substr( $type, 0, ( ceil( $half ) - 1 ) ); - $last_part = substr( $type, ( 1 - floor( $half ) ) ); - - $type = $first_part . '..' . $last_part; - } - - return $type; - } - - /** - * Invalidate sitemap cache. - * - * @since 3.2 - * - * @param string|null $type The type to get the key for. Null for all caches. - * - * @return void - */ - public static function invalidate_storage( $type = null ) { - - // Global validator gets cleared when no type is provided. - $old_validator = null; - - // Get the current type validator. - if ( ! is_null( $type ) ) { - $old_validator = self::get_validator( $type ); - } - - // Refresh validator. - self::create_validator( $type ); - - if ( ! wp_using_ext_object_cache() ) { - // Clean up current cache from the database. - self::cleanup_database( $type, $old_validator ); - } - - // External object cache pushes old and unretrieved items out by itself so we don't have to do anything for that. - } - - /** - * Cleanup invalidated database cache. - * - * @since 3.2 - * - * @param string|null $type The type of sitemap to clear cache for. - * @param string|null $validator The validator to clear cache of. - * - * @return void - */ - public static function cleanup_database( $type = null, $validator = null ) { - - global $wpdb; - - if ( is_null( $type ) ) { - // Clear all cache if no type is provided. - $like = sprintf( '%s%%', self::STORAGE_KEY_PREFIX ); - } - else { - // Clear type cache for all type keys. - $like = sprintf( '%1$s%2$s_%%', self::STORAGE_KEY_PREFIX, $type ); - } - - /* - * Add slashes to the LIKE "_" single character wildcard. - * - * We can't use `esc_like` here because we need the % in the query. - */ - $where = []; - $where[] = sprintf( "option_name LIKE '%s'", addcslashes( '_transient_' . $like, '_' ) ); - $where[] = sprintf( "option_name LIKE '%s'", addcslashes( '_transient_timeout_' . $like, '_' ) ); - - // Delete transients. - $query = sprintf( 'DELETE FROM %1$s WHERE %2$s', $wpdb->options, implode( ' OR ', $where ) ); - $wpdb->query( $query ); - - wp_cache_delete( 'alloptions', 'options' ); - } - - /** - * Get the current cache validator. - * - * Without the type the global validator is returned. - * This can invalidate -all- keys in cache at once. - * - * With the type parameter the validator for that specific type can be invalidated. - * - * @since 3.2 - * - * @param string $type Provide a type for a specific type validator, empty for global validator. - * - * @return string|null The validator for the supplied type. - */ - public static function get_validator( $type = '' ) { - - $key = self::get_validator_key( $type ); - - $current = get_option( $key, null ); - if ( ! is_null( $current ) ) { - return $current; - } - - if ( self::create_validator( $type ) ) { - return self::get_validator( $type ); - } - - return null; - } - - /** - * Get the cache validator option key for the specified type. - * - * @since 3.2 - * - * @param string $type Provide a type for a specific type validator, empty for global validator. - * - * @return string Validator to be used to generate the cache key. - */ - public static function get_validator_key( $type = '' ) { - - if ( empty( $type ) ) { - return self::VALIDATION_GLOBAL_KEY; - } - - return sprintf( self::VALIDATION_TYPE_KEY_FORMAT, $type ); - } - - /** - * Refresh the cache validator value. - * - * @since 3.2 - * - * @param string $type Provide a type for a specific type validator, empty for global validator. - * - * @return bool True if validator key has been saved as option. - */ - public static function create_validator( $type = '' ) { - - $key = self::get_validator_key( $type ); - - // Generate new validator. - $microtime = microtime(); - - // Remove space. - list( $milliseconds, $seconds ) = explode( ' ', $microtime ); - - // Transients are purged every 24h. - $seconds = ( $seconds % DAY_IN_SECONDS ); - $milliseconds = intval( substr( $milliseconds, 2, 3 ), 10 ); - - // Combine seconds and milliseconds and convert to integer. - $validator = intval( $seconds . '' . $milliseconds, 10 ); - - // Apply base 61 encoding. - $compressed = self::convert_base10_to_base61( $validator ); - - return update_option( $key, $compressed, false ); - } - - /** - * Encode to base61 format. - * - * This is base64 (numeric + alpha + alpha upper case) without the 0. - * - * @since 3.2 - * - * @param int $base10 The number that has to be converted to base 61. - * - * @return string Base 61 converted string. - * - * @throws InvalidArgumentException When the input is not an integer. - */ - public static function convert_base10_to_base61( $base10 ) { - - if ( ! is_int( $base10 ) ) { - throw new InvalidArgumentException( __( 'Expected an integer as input.', 'wordpress-seo' ) ); - } - - // Characters that will be used in the conversion. - $characters = '123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; - $length = strlen( $characters ); - - $remainder = $base10; - $output = ''; - - do { - // Building from right to left in the result. - $index = ( $remainder % $length ); - - // Prepend the character to the output. - $output = $characters[ $index ] . $output; - - // Determine the remainder after removing the applied number. - $remainder = floor( $remainder / $length ); - - // Keep doing it until we have no remainder left. - } while ( $remainder ); - - return $output; - } -} diff --git a/inc/sitemaps/class-sitemaps-cache.php b/inc/sitemaps/class-sitemaps-cache.php deleted file mode 100644 index 1b09f4dea9a..00000000000 --- a/inc/sitemaps/class-sitemaps-cache.php +++ /dev/null @@ -1,353 +0,0 @@ -is_enabled(); - } - - /** - * If cache is enabled. - * - * @since 3.2 - * - * @return bool - */ - public function is_enabled() { - - /** - * Filter if XML sitemap transient cache is enabled. - * - * @param bool $unsigned Enable cache or not, defaults to true. - */ - return apply_filters( 'wpseo_enable_xml_sitemap_transient_caching', false ); - } - - /** - * Retrieve the sitemap page from cache. - * - * @since 3.2 - * - * @param string $type Sitemap type. - * @param int $page Page number to retrieve. - * - * @return string|bool - */ - public function get_sitemap( $type, $page ) { - - $transient_key = WPSEO_Sitemaps_Cache_Validator::get_storage_key( $type, $page ); - if ( $transient_key === false ) { - return false; - } - - return get_transient( $transient_key ); - } - - /** - * Get the sitemap that is cached. - * - * @param string $type Sitemap type. - * @param int $page Page number to retrieve. - * - * @return WPSEO_Sitemap_Cache_Data|null Null on no cache found otherwise object containing sitemap and meta data. - */ - public function get_sitemap_data( $type, $page ) { - - $sitemap = $this->get_sitemap( $type, $page ); - - if ( empty( $sitemap ) ) { - return null; - } - - /* - * Unserialize Cache Data object as is_serialized() doesn't recognize classes in C format. - * This work-around should no longer be needed once the minimum PHP version has gone up to PHP 7.4, - * as the `WPSEO_Sitemap_Cache_Data` class uses O format serialization in PHP 7.4 and higher. - * - * @link https://wiki.php.net/rfc/custom_object_serialization - */ - if ( is_string( $sitemap ) && strpos( $sitemap, 'C:24:"WPSEO_Sitemap_Cache_Data"' ) === 0 ) { - // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize -- Can't be avoided due to how WP stores options. - $sitemap = unserialize( $sitemap ); - } - - // What we expect it to be if it is set. - if ( $sitemap instanceof WPSEO_Sitemap_Cache_Data_Interface ) { - return $sitemap; - } - - return null; - } - - /** - * Store the sitemap page from cache. - * - * @since 3.2 - * - * @param string $type Sitemap type. - * @param int $page Page number to store. - * @param string $sitemap Sitemap body to store. - * @param bool $usable Is this a valid sitemap or a cache of an invalid sitemap. - * - * @return bool - */ - public function store_sitemap( $type, $page, $sitemap, $usable = true ) { - - $transient_key = WPSEO_Sitemaps_Cache_Validator::get_storage_key( $type, $page ); - - if ( $transient_key === false ) { - return false; - } - - $status = ( $usable ) ? WPSEO_Sitemap_Cache_Data::OK : WPSEO_Sitemap_Cache_Data::ERROR; - - $sitemap_data = new WPSEO_Sitemap_Cache_Data(); - $sitemap_data->set_sitemap( $sitemap ); - $sitemap_data->set_status( $status ); - - return set_transient( $transient_key, $sitemap_data, DAY_IN_SECONDS ); - } - - /** - * Delete cache transients for index and specific type. - * - * Always deletes the main index sitemaps cache, as that's always invalidated by any other change. - * - * @since 1.5.4 - * @since 3.2 Changed from function wpseo_invalidate_sitemap_cache() to method in this class. - * - * @param string $type Sitemap type to invalidate. - * - * @return void - */ - public static function invalidate( $type ) { - - self::clear( [ $type ] ); - } - - /** - * Helper to invalidate in hooks where type is passed as second argument. - * - * @since 3.2 - * - * @param int $unused Unused term ID value. - * @param string $type Taxonomy to invalidate. - * - * @return void - */ - public static function invalidate_helper( $unused, $type ) { - - if ( - WPSEO_Options::get( 'noindex-' . $type ) === false - || WPSEO_Options::get( 'noindex-tax-' . $type ) === false - ) { - self::invalidate( $type ); - } - } - - /** - * Invalidate sitemap cache for authors. - * - * @param int $user_id User ID. - * - * @return bool True if the sitemap was properly invalidated. False otherwise. - */ - public static function invalidate_author( $user_id ) { - - $user = get_user_by( 'id', $user_id ); - - if ( $user === false ) { - return false; - } - - if ( current_action() === 'user_register' ) { - update_user_meta( $user_id, '_yoast_wpseo_profile_updated', time() ); - } - - if ( empty( $user->roles ) || in_array( 'subscriber', $user->roles, true ) ) { - return false; - } - - self::invalidate( 'author' ); - - return true; - } - - /** - * Invalidate sitemap cache for the post type of a post. - * - * Don't invalidate for revisions. - * - * @since 1.5.4 - * @since 3.2 Changed from function wpseo_invalidate_sitemap_cache_on_save_post() to method in this class. - * - * @param int $post_id Post ID to invalidate type for. - * - * @return void - */ - public static function invalidate_post( $post_id ) { - - if ( wp_is_post_revision( $post_id ) ) { - return; - } - - self::invalidate( get_post_type( $post_id ) ); - } - - /** - * Delete cache transients for given sitemaps types or all by default. - * - * @since 1.8.0 - * @since 3.2 Moved from WPSEO_Utils to this class. - * - * @param array $types Set of sitemap types to delete cache transients for. - * - * @return void - */ - public static function clear( $types = [] ) { - - if ( ! self::$is_enabled ) { - return; - } - - // No types provided, clear all. - if ( empty( $types ) ) { - self::$clear_all = true; - - return; - } - - // Always invalidate the index sitemap as well. - if ( ! in_array( WPSEO_Sitemaps::SITEMAP_INDEX_TYPE, $types, true ) ) { - array_unshift( $types, WPSEO_Sitemaps::SITEMAP_INDEX_TYPE ); - } - - foreach ( $types as $type ) { - if ( ! in_array( $type, self::$clear_types, true ) ) { - self::$clear_types[] = $type; - } - } - } - - /** - * Invalidate storage for cache types queued to clear. - */ - public static function clear_queued() { - - if ( self::$clear_all ) { - - WPSEO_Sitemaps_Cache_Validator::invalidate_storage(); - self::$clear_all = false; - self::$clear_types = []; - - return; - } - - foreach ( self::$clear_types as $type ) { - WPSEO_Sitemaps_Cache_Validator::invalidate_storage( $type ); - } - - self::$clear_types = []; - } - - /** - * Adds a hook that when given option is updated, the cache is cleared. - * - * @since 3.2 - * - * @param string $option Option name. - * @param string $type Sitemap type. - */ - public static function register_clear_on_option_update( $option, $type = '' ) { - - self::$cache_clear[ $option ] = $type; - } - - /** - * Clears the transient cache when a given option is updated, if that option has been registered before. - * - * @since 3.2 - * - * @param string $option The option name that's being updated. - * - * @return void - */ - public static function clear_on_option_update( $option ) { - - if ( array_key_exists( $option, self::$cache_clear ) ) { - - if ( empty( self::$cache_clear[ $option ] ) ) { - // Clear all caches. - self::clear(); - } - else { - // Clear specific provided type(s). - $types = (array) self::$cache_clear[ $option ]; - self::clear( $types ); - } - } - } -} diff --git a/inc/sitemaps/class-sitemaps-renderer.php b/inc/sitemaps/class-sitemaps-renderer.php index 95ee3297e60..ce0bfab4874 100644 --- a/inc/sitemaps/class-sitemaps-renderer.php +++ b/inc/sitemaps/class-sitemaps-renderer.php @@ -130,7 +130,7 @@ public function get_sitemap( $links, $type, $current_page ) { /** * Produce final XML output with debug information. * - * @param string $sitemap Sitemap XML. + * @param string $sitemap Sitemap XML. * * @return string */ @@ -267,9 +267,9 @@ public function sitemap_url( $url ) { /** * Filters the output for the sitemap URL tag. * - * @api string $output The output for the sitemap url tag. - * * @param array $url The sitemap URL array on which the output is based. + * + * @api string $output The output for the sitemap url tag. */ return apply_filters( 'wpseo_sitemap_url', $output, $url ); } diff --git a/inc/sitemaps/class-sitemaps.php b/inc/sitemaps/class-sitemaps.php index 41d7fc4b499..5eb48918d65 100644 --- a/inc/sitemaps/class-sitemaps.php +++ b/inc/sitemaps/class-sitemaps.php @@ -74,15 +74,6 @@ class WPSEO_Sitemaps { */ public $renderer; - /** - * The sitemap cache. - * - * @since 3.2 - * - * @var WPSEO_Sitemaps_Cache - */ - public $cache; - /** * The sitemap providers. * @@ -100,12 +91,10 @@ public function __construct() { add_action( 'after_setup_theme', [ $this, 'init_sitemaps_providers' ] ); add_action( 'after_setup_theme', [ $this, 'reduce_query_load' ], 99 ); add_action( 'pre_get_posts', [ $this, 'redirect' ], 1 ); - add_action( 'wpseo_hit_sitemap_index', [ $this, 'hit_sitemap_index' ] ); add_action( 'wpseo_ping_search_engines', [ __CLASS__, 'ping_search_engines' ] ); $this->router = new WPSEO_Sitemaps_Router(); $this->renderer = new WPSEO_Sitemaps_Renderer(); - $this->cache = new WPSEO_Sitemaps_Cache(); if ( ! empty( $_SERVER['SERVER_PROTOCOL'] ) ) { $this->http_protocol = sanitize_text_field( wp_unslash( $_SERVER['SERVER_PROTOCOL'] ) ); @@ -253,9 +242,7 @@ public function redirect( $query ) { $this->set_n( get_query_var( 'sitemap_n' ) ); - if ( ! $this->get_sitemap_from_cache( $type, $this->current_page ) ) { - $this->build_sitemap( $type ); - } + $this->build_sitemap( $type ); if ( $this->bad_sitemap ) { $query->set_404(); @@ -268,60 +255,6 @@ public function redirect( $query ) { $this->sitemap_close(); } - /** - * Try to get the sitemap from cache. - * - * @param string $type Sitemap type. - * @param int $page_number The page number to retrieve. - * - * @return bool If the sitemap has been retrieved from cache. - */ - private function get_sitemap_from_cache( $type, $page_number ) { - - $this->transient = false; - - if ( $this->cache->is_enabled() !== true ) { - return false; - } - - /** - * Fires before the attempt to retrieve XML sitemap from the transient cache. - * - * @param WPSEO_Sitemaps $sitemaps Sitemaps object. - */ - do_action( 'wpseo_sitemap_stylesheet_cache_' . $type, $this ); - - $sitemap_cache_data = $this->cache->get_sitemap_data( $type, $page_number ); - - // No cache was found, refresh it because cache is enabled. - if ( empty( $sitemap_cache_data ) ) { - return $this->refresh_sitemap_cache( $type, $page_number ); - } - - // Cache object was found, parse information. - $this->transient = true; - - $this->sitemap = $sitemap_cache_data->get_sitemap(); - $this->bad_sitemap = ! $sitemap_cache_data->is_usable(); - - return true; - } - - /** - * Build and save sitemap to cache. - * - * @param string $type Sitemap type. - * @param int $page_number The page number to save to. - * - * @return bool - */ - private function refresh_sitemap_cache( $type, $page_number ) { - $this->set_n( $page_number ); - $this->build_sitemap( $type ); - - return $this->cache->store_sitemap( $type, $page_number, $this->sitemap, ! $this->bad_sitemap ); - } - /** * Attempts to build the requested sitemap. * @@ -448,93 +381,6 @@ public function output() { echo $this->renderer->get_output( $this->sitemap ); } - /** - * Makes a request to the sitemap index to cache it before the arrival of the search engines. - * - * @return void - */ - public function hit_sitemap_index() { - if ( ! $this->cache->is_enabled() ) { - return; - } - - wp_remote_get( WPSEO_Sitemaps_Router::get_base_url( 'sitemap_index.xml' ) ); - } - - /** - * Get the GMT modification date for the last modified post in the post type. - * - * @since 3.2 - * - * @param string|array $post_types Post type or array of types. - * @param bool $return_all Flag to return array of values. - * - * @return string|array|false - */ - public static function get_last_modified_gmt( $post_types, $return_all = false ) { - - global $wpdb; - - static $post_type_dates = null; - - if ( ! is_array( $post_types ) ) { - $post_types = [ $post_types ]; - } - - foreach ( $post_types as $post_type ) { - if ( ! isset( $post_type_dates[ $post_type ] ) ) { // If we hadn't seen post type before. R. - $post_type_dates = null; - break; - } - } - - if ( is_null( $post_type_dates ) ) { - - $post_type_dates = []; - $post_type_names = WPSEO_Post_Type::get_accessible_post_types(); - - if ( ! empty( $post_type_names ) ) { - $post_statuses = array_map( 'esc_sql', self::get_post_statuses() ); - - $sql = " - SELECT post_type, MAX(post_modified_gmt) AS date - FROM $wpdb->posts - WHERE post_status IN ('" . implode( "','", $post_statuses ) . "') - AND post_type IN ('" . implode( "','", $post_type_names ) . "') - GROUP BY post_type - ORDER BY date DESC - "; - - foreach ( $wpdb->get_results( $sql ) as $obj ) { - $post_type_dates[ $obj->post_type ] = $obj->date; - } - } - } - - $dates = array_intersect_key( $post_type_dates, array_flip( $post_types ) ); - - if ( count( $dates ) > 0 ) { - if ( $return_all ) { - return $dates; - } - - return max( $dates ); - } - - return false; - } - - /** - * Get the modification date for the last modified post in the post type. - * - * @param array $post_types Post types to get the last modification date for. - * - * @return string - */ - public function get_last_modified( $post_types ) { - return YoastSEO()->helpers->date->format( self::get_last_modified_gmt( $post_types ) ); - } - /** * Notify search engines of the updated sitemap. * @@ -578,7 +424,7 @@ protected function get_entries_per_page() { * * @param int $entries The maximum number of entries per XML sitemap. */ - $entries = (int) apply_filters( 'wpseo_sitemap_entries_per_page', 1000 ); + $entries = (int) apply_filters( 'Yoast\WP\SEO\xml_sitemaps_entries_per_sitemap', 5000 ); return $entries; } diff --git a/inc/sitemaps/class-taxonomy-sitemap-provider.php b/inc/sitemaps/class-taxonomy-sitemap-provider.php index 8d8b394fc07..e8ec3a76fde 100644 --- a/inc/sitemaps/class-taxonomy-sitemap-provider.php +++ b/inc/sitemaps/class-taxonomy-sitemap-provider.php @@ -8,33 +8,7 @@ /** * Sitemap provider for author archives. */ -class WPSEO_Taxonomy_Sitemap_Provider implements WPSEO_Sitemap_Provider { - - /** - * Holds image parser instance. - * - * @var WPSEO_Sitemap_Image_Parser - */ - protected static $image_parser; - - /** - * Determines whether images should be included in the XML sitemap. - * - * @var bool - */ - private $include_images; - - /** - * Set up object properties for data reuse. - */ - public function __construct() { - /** - * Filter - Allows excluding images from the XML sitemap. - * - * @param bool $include True to include, false to exclude. - */ - $this->include_images = apply_filters( 'wpseo_xml_sitemap_include_images', true ); - } +class WPSEO_Taxonomy_Sitemap_Provider extends WPSEO_Indexable_Sitemap_Provider implements WPSEO_Sitemap_Provider { /** * Check if provider supports given item type. @@ -55,115 +29,55 @@ public function handles_type( $type ) { } /** - * Retrieves the links for the sitemap. - * - * @param int $max_entries Entries per sitemap. + * Returns the object type for this sitemap. * - * @return array + * @return string The object type. */ - public function get_index_links( $max_entries ) { - - $taxonomies = get_taxonomies( [ 'public' => true ], 'objects' ); + protected function get_object_type() { + return 'term'; + } - if ( empty( $taxonomies ) ) { - return []; + /** + * Whether or not a specific object sub type should be excluded. + * + * @param string $object_sub_type The object sub type. + * + * @return boolean Whether or not it should be excluded. + */ + protected function should_exclude_object_sub_type( $object_sub_type ) { + /** + * Filter to exclude the taxonomy from the XML sitemap. + * + * @param bool $exclude Defaults to false. + * @param string $taxonomy_name Name of the taxonomy to exclude.. + */ + if ( apply_filters( 'wpseo_sitemap_exclude_taxonomy', false, $object_sub_type ) ) { + return true; } - $taxonomy_names = array_filter( array_keys( $taxonomies ), [ $this, 'is_valid_taxonomy' ] ); - $taxonomies = array_intersect_key( $taxonomies, array_flip( $taxonomy_names ) ); - - // Retrieve all the taxonomies and their terms so we can do a proper count on them. + return false; + } + /** + * Returns a list of all object IDs that should be excluded. + * + * @return int[] + */ + protected function get_excluded_object_ids() { /** - * Filter the setting of excluding empty terms from the XML sitemap. + * Filter: 'wpseo_exclude_from_sitemap_by_term_ids' - Allow excluding terms by ID. * - * @param bool $exclude Defaults to true. - * @param array $taxonomy_names Array of names for the taxonomies being processed. + * @api array $terms_to_exclude The terms to exclude. */ - $hide_empty = apply_filters( 'wpseo_sitemap_exclude_empty_terms', true, $taxonomy_names ); - - $all_taxonomies = []; - - foreach ( $taxonomy_names as $taxonomy_name ) { - /** - * Filter the setting of excluding empty terms from the XML sitemap for a specific taxonomy. - * - * @param bool $exclude Defaults to the sitewide setting. - * @param string $taxonomy_name The name of the taxonomy being processed. - */ - $hide_empty_tax = apply_filters( 'wpseo_sitemap_exclude_empty_terms_taxonomy', $hide_empty, $taxonomy_name ); + $excluded_term_ids = apply_filters( 'wpseo_exclude_from_sitemap_by_term_ids', [] ); - $term_args = [ - 'hide_empty' => $hide_empty_tax, - 'fields' => 'ids', - ]; - $taxonomy_terms = get_terms( $taxonomy_name, $term_args ); - - if ( count( $taxonomy_terms ) > 0 ) { - $all_taxonomies[ $taxonomy_name ] = $taxonomy_terms; - } + if ( ! is_array( $excluded_term_ids ) ) { + return []; } - $index = []; - - foreach ( $taxonomies as $tax_name => $tax ) { - - if ( ! isset( $all_taxonomies[ $tax_name ] ) ) { // No eligible terms found. - continue; - } - - $total_count = ( isset( $all_taxonomies[ $tax_name ] ) ) ? count( $all_taxonomies[ $tax_name ] ) : 1; - $max_pages = 1; + $excluded_term_ids = array_map( 'intval', $excluded_term_ids ); - if ( $total_count > $max_entries ) { - $max_pages = (int) ceil( $total_count / $max_entries ); - } - - $last_modified_gmt = WPSEO_Sitemaps::get_last_modified_gmt( $tax->object_type ); - - for ( $page_counter = 0; $page_counter < $max_pages; $page_counter++ ) { - - $current_page = ( $max_pages > 1 ) ? ( $page_counter + 1 ) : ''; - - if ( ! is_array( $tax->object_type ) || count( $tax->object_type ) === 0 ) { - continue; - } - - $terms = array_splice( $all_taxonomies[ $tax_name ], 0, $max_entries ); - - if ( ! $terms ) { - continue; - } - - $args = [ - 'post_type' => $tax->object_type, - 'tax_query' => [ - [ - 'taxonomy' => $tax_name, - 'terms' => $terms, - ], - ], - 'orderby' => 'modified', - 'order' => 'DESC', - 'posts_per_page' => 1, - ]; - $query = new WP_Query( $args ); - - if ( $query->have_posts() ) { - $date = $query->posts[0]->post_modified_gmt; - } - else { - $date = $last_modified_gmt; - } - - $index[] = [ - 'loc' => WPSEO_Sitemaps_Router::get_base_url( $tax_name . '-sitemap' . $current_page . '.xml' ), - 'lastmod' => $date, - ]; - } - } - - return $index; + return array_unique( $excluded_term_ids ); } /** @@ -178,99 +92,29 @@ public function get_index_links( $max_entries ) { * @throws OutOfBoundsException When an invalid page is requested. */ public function get_sitemap_links( $type, $max_entries, $current_page ) { - global $wpdb; - - $links = []; if ( ! $this->handles_type( $type ) ) { - return $links; + return []; } - $taxonomy = get_taxonomy( $type ); - $steps = $max_entries; $offset = ( $current_page > 1 ) ? ( ( $current_page - 1 ) * $max_entries ) : 0; - /** This filter is documented in inc/sitemaps/class-taxonomy-sitemap-provider.php */ - $hide_empty = apply_filters( 'wpseo_sitemap_exclude_empty_terms', true, [ $taxonomy->name ] ); - /** This filter is documented in inc/sitemaps/class-taxonomy-sitemap-provider.php */ - $hide_empty_tax = apply_filters( 'wpseo_sitemap_exclude_empty_terms_taxonomy', $hide_empty, $taxonomy->name ); - $terms = get_terms( - [ - 'taxonomy' => $taxonomy->name, - 'hide_empty' => $hide_empty_tax, - 'update_term_meta_cache' => false, - 'offset' => $offset, - 'number' => $steps, - ] - ); - - // If there are no terms fetched for this range, we are on an invalid page. - if ( empty( $terms ) ) { - throw new OutOfBoundsException( 'Invalid sitemap page requested' ); + $query = $this->repository + ->query_where_noindex( false, 'term', $type ) + ->select_many( 'id', 'object_id', 'permalink', 'object_last_modified' ) + ->where( 'is_publicly_viewable', true ) + ->order_by_asc( 'object_last_modified' ) + ->offset( $offset ) + ->limit( $steps ); + + $terms_to_exclude = $this->get_excluded_object_ids(); + if ( is_array( $terms_to_exclude ) && count( $terms_to_exclude ) > 0 ) { + $query->where_not_in( 'object_id', $terms_to_exclude ); } - $post_statuses = array_map( 'esc_sql', WPSEO_Sitemaps::get_post_statuses() ); - - // Grab last modified date. - $sql = " - SELECT MAX(p.post_modified_gmt) AS lastmod - FROM $wpdb->posts AS p - INNER JOIN $wpdb->term_relationships AS term_rel - ON term_rel.object_id = p.ID - INNER JOIN $wpdb->term_taxonomy AS term_tax - ON term_tax.term_taxonomy_id = term_rel.term_taxonomy_id - AND term_tax.taxonomy = %s - AND term_tax.term_id = %d - WHERE p.post_status IN ('" . implode( "','", $post_statuses ) . "') - AND p.post_password = '' - "; - - /** - * Filter: 'wpseo_exclude_from_sitemap_by_term_ids' - Allow excluding terms by ID. - * - * @api array $terms_to_exclude The terms to exclude. - */ - $terms_to_exclude = apply_filters( 'wpseo_exclude_from_sitemap_by_term_ids', [] ); - - foreach ( $terms as $term ) { - - if ( in_array( $term->term_id, $terms_to_exclude, true ) ) { - continue; - } - - $url = []; - - $tax_noindex = WPSEO_Taxonomy_Meta::get_term_meta( $term, $term->taxonomy, 'noindex' ); - - if ( $tax_noindex === 'noindex' ) { - continue; - } - - $url['loc'] = WPSEO_Taxonomy_Meta::get_term_meta( $term, $term->taxonomy, 'canonical' ); - - if ( ! is_string( $url['loc'] ) || $url['loc'] === '' ) { - $url['loc'] = get_term_link( $term, $term->taxonomy ); - } - - $url['mod'] = $wpdb->get_var( $wpdb->prepare( $sql, $term->taxonomy, $term->term_id ) ); - - if ( $this->include_images ) { - $url['images'] = $this->get_image_parser()->get_term_images( $term ); - } - - // Deprecated, kept for backwards data compat. R. - $url['chf'] = 'daily'; - $url['pri'] = 1; + $indexables = $query->find_many(); - /** This filter is documented at inc/sitemaps/class-post-type-sitemap-provider.php */ - $url = apply_filters( 'wpseo_sitemap_entry', $url, 'term', $term ); - - if ( ! empty( $url ) ) { - $links[] = $url; - } - } - - return $links; + return $this->xml_sitemap_helper->convert_indexables_to_sitemap_links( $indexables, 'term' ); } /** @@ -298,7 +142,7 @@ public function is_valid_taxonomy( $taxonomy_name ) { * Filter to exclude the taxonomy from the XML sitemap. * * @param bool $exclude Defaults to false. - * @param string $taxonomy_name Name of the taxonomy to exclude.. + * @param string $taxonomy_name Name of the taxonomy to exclude. */ if ( apply_filters( 'wpseo_sitemap_exclude_taxonomy', false, $taxonomy_name ) ) { return false; @@ -306,17 +150,4 @@ public function is_valid_taxonomy( $taxonomy_name ) { return true; } - - /** - * Get the Image Parser. - * - * @return WPSEO_Sitemap_Image_Parser - */ - protected function get_image_parser() { - if ( ! isset( self::$image_parser ) ) { - self::$image_parser = new WPSEO_Sitemap_Image_Parser(); - } - - return self::$image_parser; - } } diff --git a/inc/sitemaps/interface-sitemap-cache-data.php b/inc/sitemaps/interface-sitemap-cache-data.php deleted file mode 100644 index 136f6d65cc6..00000000000 --- a/inc/sitemaps/interface-sitemap-cache-data.php +++ /dev/null @@ -1,72 +0,0 @@ -build_update(); } + /** + * Returns the values bound to this query. + * + * @return array The values. + */ + public function get_values() { + return $this->values; + } + /** * Executes an aggregate query on the current connection. * diff --git a/src/config/migrations/20211108133106_AddIsPubliclyViewableToIndexables.php b/src/config/migrations/20211108133106_AddIsPubliclyViewableToIndexables.php index 901a1db4fca..19319c1606e 100644 --- a/src/config/migrations/20211108133106_AddIsPubliclyViewableToIndexables.php +++ b/src/config/migrations/20211108133106_AddIsPubliclyViewableToIndexables.php @@ -34,6 +34,8 @@ public function up() { 'default' => null, ] ); + + $this->query( "UPDATE $table_name SET is_publicly_viewable = is_public" ); } /** @@ -60,6 +62,3 @@ protected function get_table_name() { return Model::get_table_name( 'Indexable' ); } } - - - diff --git a/src/helpers/xml-sitemap-helper.php b/src/helpers/xml-sitemap-helper.php new file mode 100644 index 00000000000..0c6620f0cf5 --- /dev/null +++ b/src/helpers/xml-sitemap-helper.php @@ -0,0 +1,123 @@ +links_repository = $links_repository; + } + + /** + * Find images for a given set of indexables. + * + * @param Indexable[] $indexables Array of indexables. + * @param string $type The type of link to retrieve, defaults to image internal. + * + * @return array $images_by_id Array of images for the indexable, in XML sitemap format. + */ + public function find_images_for_indexables( $indexables, $type = 'image-in' ) { + $images_by_id = []; + $indexable_ids = []; + + foreach ( $indexables as $indexable ) { + $indexable_ids[] = $indexable->id; + } + + if ( $indexable_ids === [] ) { + return []; + } + + $images = $this->links_repository->query() + ->select_many( 'indexable_id', 'url' ) + ->where( 'type', $type ) + ->where_in( 'indexable_id', $indexable_ids ) + ->find_many(); + + foreach ( $images as $image ) { + if ( ! is_array( $images_by_id[ $image->indexable_id ] ) ) { + $images_by_id[ $image->indexable_id ] = []; + } + $images_by_id[ $image->indexable_id ][] = [ + 'src' => $image->url, + ]; + } + + return $images_by_id; + } + + /** + * Convert an array of indexables to an array that can be used by the XML sitemap renderer. + * + * @param Indexable[] $indexables Array of indexables. + * @param string $object_type The Indexable object type. + * + * @return array Array to be used by the XML sitemap renderer. + */ + public function convert_indexables_to_sitemap_links( $indexables, $object_type ) { + /** + * Filter - Allows excluding images from the XML sitemap. + * + * @param bool $include True to include, false to exclude. + */ + $include_images = apply_filters( 'wpseo_xml_sitemap_include_images', true ); + + if ( $include_images ) { + $images_by_id = $this->find_images_for_indexables( $indexables ); + } + + $links = []; + foreach ( $indexables as $indexable ) { + $images = isset( $images_by_id[ $indexable->id ] ) ? $images_by_id[ $indexable->id ] : []; + + if ( $indexable->object_type === 'post' ) { + /** + * Filter images to be included for the post in XML sitemap. + * + * @param array $images Array of image items. + * @param int $post_id ID of the post. + */ + $images = \apply_filters( 'wpseo_sitemap_urlimages', $images, $indexable->object_id ); + } + + $url = [ + 'loc' => $indexable->permalink, + 'mod' => $indexable->object_last_modified, + 'images' => $images, + ]; + + /** + * Filter URL entry before it gets added to the sitemap. + * + * @param array $url Array of URL parts. + * @param string $type URL type. + * @param int $object_id WordPress ID of the object. + */ + $url = apply_filters( 'wpseo_sitemap_entry', $url, $object_type, $indexable->object_id ); + + $links[] = $url; + } + + return $links; + } +} diff --git a/src/surfaces/helpers-surface.php b/src/surfaces/helpers-surface.php index c0e7c063b96..e298665b74f 100644 --- a/src/surfaces/helpers-surface.php +++ b/src/surfaces/helpers-surface.php @@ -44,6 +44,7 @@ * @property Helpers\User_Helper $user * @property Helpers\Woocommerce_Helper $woocommerce * @property Helpers\Wordpress_Helper $wordpress + * @property Helpers\XML_Sitemap_Helper $xml_sitemap */ class Helpers_Surface { diff --git a/tests/integration/sitemaps/test-class-wpseo-post-type-sitemap-provider.php b/tests/integration/sitemaps/test-class-wpseo-post-type-sitemap-provider.php index 6654e7bf4e7..626523f460a 100644 --- a/tests/integration/sitemaps/test-class-wpseo-post-type-sitemap-provider.php +++ b/tests/integration/sitemaps/test-class-wpseo-post-type-sitemap-provider.php @@ -256,31 +256,6 @@ public function filter_with_invalid_output( $excluded_post_ids ) { return '1,2,3,4'; } - /** - * Tests if external URLs are not being included in the sitemap. - * - * @covers WPSEO_Post_Type_Sitemap_Provider::get_url - */ - public function test_get_url() { - $current_home = get_option( 'home' ); - $sitemap_provider = new WPSEO_Post_Type_Sitemap_Provider_Double(); - - $post_object = $this->factory()->post->create_and_get(); - $post_url = $sitemap_provider->get_url( $post_object ); - - $this->assertStringContainsString( $current_home, $post_url['loc'] ); - - // Change home URL. - update_option( 'home', 'http://example.com' ); - wp_cache_delete( 'alloptions', 'options' ); - - $this->assertFalse( $sitemap_provider->get_url( $post_object ) ); - - // Revert original home URL. - update_option( 'home', $current_home ); - wp_cache_delete( 'alloptions', 'options' ); - } - /** * Tests a regular post is added to the sitemap. * diff --git a/tests/integration/sitemaps/test-class-wpseo-sitemap-image-parser.php b/tests/integration/sitemaps/test-class-wpseo-sitemap-image-parser.php deleted file mode 100644 index a47000a1e58..00000000000 --- a/tests/integration/sitemaps/test-class-wpseo-sitemap-image-parser.php +++ /dev/null @@ -1,92 +0,0 @@ -factory->post->create( - [ 'post_content' => "{$content_alt}" ] - ); - - $images = self::$class_instance->get_images( get_post( $post_id ) ); - $this->assertNotEmpty( $images[0] ); - $content_image = $images[0]; - $this->assertEquals( $content_src, $content_image['src'] ); - $this->assertEquals( $content_title, $content_image['title'] ); - $this->assertEquals( $content_alt, $content_image['alt'] ); - } - - /** - * Tests the get_gallery_attachments function. - * - * @covers WPSEO_Sitemap_Image_Parser::get_gallery_attachments - * - * @link https://github.com/Yoast/wordpress-seo/issues/8634 - */ - public function test_parse_galleries() { - /** - * The test instance. - * - * @var WPSEO_Sitemap_Image_Parser_Double $image_parser - */ - $image_parser = $this->getMockBuilder( 'WPSEO_Sitemap_Image_Parser_Double' ) - ->setMethods( [ 'get_content_galleries', 'get_gallery_attachments' ] ) - ->getMock(); - - $image_parser - ->expects( $this->once() ) - ->method( 'get_content_galleries' ) - ->will( $this->returnValue( [ [ 'id' => 1 ] ] ) ); - - $a = (object) [ 'a', 'b' ]; - $b = (object) 1234; - $c = (object) 'some string'; - - $attachments = [ $a, $b, $c, $a, $c ]; - - $image_parser - ->expects( $this->once() ) - ->method( 'get_gallery_attachments' ) - ->will( $this->returnValue( $attachments ) ); - - $attachments = $image_parser->parse_galleries( '' ); - - $this->assertContains( $a, $attachments ); - $this->assertContains( $b, $attachments ); - $this->assertContains( $c, $attachments ); - } -} diff --git a/tests/integration/sitemaps/test-class-wpseo-sitemaps-cache-data.php b/tests/integration/sitemaps/test-class-wpseo-sitemaps-cache-data.php deleted file mode 100644 index 37f6a2ea370..00000000000 --- a/tests/integration/sitemaps/test-class-wpseo-sitemaps-cache-data.php +++ /dev/null @@ -1,167 +0,0 @@ -subject = new WPSEO_Sitemap_Cache_Data(); - } - - /** - * Test getting/setting sitemap. - * - * @covers WPSEO_Sitemap_Cache_Data::set_sitemap - * @covers WPSEO_Sitemap_Cache_Data::get_sitemap - */ - public function test_get_set_sitemap() { - $sitemap = 'this is a sitemap'; - - $this->subject->set_sitemap( $sitemap ); - $this->assertSame( $sitemap, $this->subject->get_sitemap() ); - } - - /** - * Setting a sitemap that is not a string. - * - * @covers WPSEO_Sitemap_Cache_Data::get_sitemap - * @covers WPSEO_Sitemap_Cache_Data::is_usable - */ - public function test_set_sitemap_not_string() { - $sitemap = new stdClass(); - $sitemap->doesnt = 'matter'; - - $this->subject->set_sitemap( $sitemap ); - $this->assertSame( '', $this->subject->get_sitemap() ); - $this->assertFalse( $this->subject->is_usable() ); - } - - /** - * Test with invalid status. - * - * @covers WPSEO_Sitemap_Cache_Data::get_status - */ - public function test_set_invalid_status() { - $status = 'invalid'; - - $this->subject->set_status( $status ); - $this->assertSame( WPSEO_Sitemap_Cache_Data_Interface::UNKNOWN, $this->subject->get_status() ); - $this->assertFalse( $this->subject->is_usable() ); - } - - /** - * Test status of sitemap without setting anything. - * - * @covers WPSEO_Sitemap_Cache_Data::get_status - */ - public function test_sitemap_status_unset() { - $this->assertSame( WPSEO_Sitemap_Cache_Data::UNKNOWN, $this->subject->get_status() ); - } - - /** - * Test setting empty sitemap - status. - * - * @covers WPSEO_Sitemap_Cache_Data::set_sitemap - * @covers WPSEO_Sitemap_Cache_Data::get_status - */ - public function test_set_empty_sitemap_status() { - $sitemap = ''; - - $this->subject->set_sitemap( $sitemap ); - $this->assertSame( WPSEO_Sitemap_Cache_Data::ERROR, $this->subject->get_status() ); - } - - /** - * Test is_usable with status. - * - * @covers WPSEO_Sitemap_Cache_Data::get_status - * @covers WPSEO_Sitemap_Cache_Data::is_usable - */ - public function test_set_status_is_usable() { - $this->subject->set_status( WPSEO_Sitemap_Cache_Data::OK ); - $this->assertTrue( $this->subject->is_usable() ); - - $this->subject->set_status( WPSEO_Sitemap_Cache_Data::ERROR ); - $this->assertFalse( $this->subject->is_usable() ); - } - - /** - * Test setting status string/constant. - * - * @covers WPSEO_Sitemap_Cache_Data::set_status - * @covers WPSEO_Sitemap_Cache_Data::get_status - * - * @dataProvider data_set_status_string - * - * @param string $input Input to pass to set_status(). - * @param string $expected Expected get_status() function output. - */ - public function test_set_status_string( $input, $expected ) { - $this->subject->set_status( $input ); - $this->assertSame( $expected, $this->subject->get_status() ); - } - - /** - * Data provider. - * - * @return array - */ - public function data_set_status_string() { - return [ - 'Ok using interface constant' => [ - 'input' => WPSEO_Sitemap_Cache_Data::OK, - 'expected' => WPSEO_Sitemap_Cache_Data::OK, - ], - 'Ok using hard coded string' => [ - 'input' => 'ok', - 'expected' => WPSEO_Sitemap_Cache_Data::OK, - ], - 'Error using hard coded string' => [ - 'input' => 'error', - 'expected' => WPSEO_Sitemap_Cache_Data::ERROR, - ], - ]; - } - - /** - * Test serializing/unserializing. - * - * Tests if the class is serializable. - * - * @covers WPSEO_Sitemap_Cache_Data::__serialize - * @covers WPSEO_Sitemap_Cache_Data::__unserialize - * @covers WPSEO_Sitemap_Cache_Data::serialize - * @covers WPSEO_Sitemap_Cache_Data::unserialize - */ - public function test_serialize_unserialize() { - $sitemap = 'this is a sitemap'; - - $this->subject->set_sitemap( $sitemap ); - - // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize -- Reason: There's no way for user input to get in between serialize and unserialize. - $tmp = serialize( $this->subject ); - // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize -- Reason: There's no way for user input to get in between serialize and unserialize. - $test = unserialize( $tmp ); - - $this->assertEquals( $this->subject, $test ); - } -} diff --git a/tests/integration/sitemaps/test-class-wpseo-sitemaps-cache-validator.php b/tests/integration/sitemaps/test-class-wpseo-sitemaps-cache-validator.php deleted file mode 100644 index 3d2e9eb8fac..00000000000 --- a/tests/integration/sitemaps/test-class-wpseo-sitemaps-cache-validator.php +++ /dev/null @@ -1,125 +0,0 @@ -assertEquals( WPSEO_Sitemaps_Cache_Validator::VALIDATION_GLOBAL_KEY, $result ); - } - - /** - * Test the building of cache keys. - * - * @covers ::get_validator_key - */ - public function test_get_validator_key_type() { - - $type = 'blabla'; - $expected = sprintf( WPSEO_Sitemaps_Cache_Validator::VALIDATION_TYPE_KEY_FORMAT, $type ); - - $result = WPSEO_Sitemaps_Cache_Validator::get_validator_key( $type ); - - $this->assertEquals( $expected, $result ); - } - - /** - * Normal cache key retrieval. - * - * @covers ::get_storage_key - */ - public function test_get_storage_key() { - - $page = 1; - $type = 'page'; - $global_validator = 'global'; - $type_validator = 'type'; - - $global_validator_key = WPSEO_Sitemaps_Cache_Validator::get_validator_key(); - update_option( $global_validator_key, $global_validator ); - - $type_validator_key = WPSEO_Sitemaps_Cache_Validator::get_validator_key( $type ); - update_option( $type_validator_key, $type_validator ); - - $prefix = WPSEO_Sitemaps_Cache_Validator::STORAGE_KEY_PREFIX; - $postfix = '_1:global_type'; - - $expected = $prefix . $type . $postfix; - - // Act. - $result = WPSEO_Sitemaps_Cache_Validator::get_storage_key( $type, $page ); - - // Assert. - $this->assertEquals( $expected, $result ); - } - - /** - * Key length should never be over 45 characters. - * - * This would be 53 if we don't use a timeout, but we can't because all sitemaps would - * be autoloaded every request. - * - * @covers ::get_storage_key - */ - public function test_get_storage_key_very_long_type() { - - $page = 1; - $type = str_repeat( 'a', 60 ); - - // Act. - $result = WPSEO_Sitemaps_Cache_Validator::get_storage_key( $type, $page ); - - // Assert. - $this->assertEquals( 45, strlen( $result ) ); - } - - /** - * Test base 10 to base 61 converter. - * - * @covers ::convert_base10_to_base61 - */ - public function test_base_10_to_base_61() { - - // Because of not using 0, everything has an offset. - $this->assertEquals( '1', WPSEO_Sitemaps_Cache_Validator::convert_base10_to_base61( 0 ) ); - $this->assertEquals( '2', WPSEO_Sitemaps_Cache_Validator::convert_base10_to_base61( 1 ) ); - $this->assertEquals( 'Z', WPSEO_Sitemaps_Cache_Validator::convert_base10_to_base61( 60 ) ); - - // Not using 10, because 0 offsets all positions -> 1+1=2, 0+1=1, makes 21 (this is a string not a number!). - $this->assertEquals( '21', WPSEO_Sitemaps_Cache_Validator::convert_base10_to_base61( 61 ) ); - $this->assertEquals( '22', WPSEO_Sitemaps_Cache_Validator::convert_base10_to_base61( 62 ) ); - $this->assertEquals( '32', WPSEO_Sitemaps_Cache_Validator::convert_base10_to_base61( 123 ) ); - - // Check against PHP_INT_MAX on 32-bit systems. - $this->assertEquals( '3y75pX', WPSEO_Sitemaps_Cache_Validator::convert_base10_to_base61( 2147483647 ) ); - } - - /** - * Tests whether an exception is thrown when a non numeric value is passed. - * - * @covers ::convert_base10_to_base61 - */ - public function test_base_10_to_base_61_non_integer() { - $this->expectException( InvalidArgumentException::class ); - - WPSEO_Sitemaps_Cache_Validator::convert_base10_to_base61( 'ab' ); - } -} diff --git a/tests/integration/sitemaps/test-class-wpseo-sitemaps-cache.php b/tests/integration/sitemaps/test-class-wpseo-sitemaps-cache.php deleted file mode 100644 index 0f7dbe38657..00000000000 --- a/tests/integration/sitemaps/test-class-wpseo-sitemaps-cache.php +++ /dev/null @@ -1,390 +0,0 @@ -get_sitemap_data( 'post', 1 ); - - $this->assertNull( $result ); - } - - /** - * Test if the transient cache is set as a cache data object. - * - * @covers WPSEO_Sitemaps_Cache::store_sitemap - * @covers WPSEO_Sitemaps_Cache::get_sitemap - * @covers WPSEO_Sitemaps_Cache::get_sitemap_data - */ - public function test_transient_cache_data_object() { - - $sitemap = 'this_is_a_sitemap'; - $type = 'post'; - $page = 1; - - $test = new WPSEO_Sitemap_Cache_Data(); - $test->set_sitemap( $sitemap ); - - $cache = new WPSEO_Sitemaps_Cache(); - $this->assertTrue( $cache->store_sitemap( $type, $page, $sitemap, true ) ); - - $result = $cache->get_sitemap( $type, $page ); - - /* - * In PHP < 7.4 the "old" serialization mechanism via the Serializable interface is used, - * which combined with the WP logic doesn't automatically unserialize, which is why we need - * to do so ourselves. - * As of PHP 7.4, the new serialization using magic methods is used. - */ - if ( PHP_VERSION_ID < 70400 ) { - // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize -- Reason: There's no security risk, because users don't interact with tests. - $result = unserialize( $result ); - } - - $this->assertEquals( $test, $result ); - - $this->assertEquals( $test, $cache->get_sitemap_data( $type, $page ) ); - } - - /** - * Test sitemap cache XML set as string not being validated. - * - * @covers WPSEO_Sitemaps_Cache::get_sitemap_data - * @covers WPSEO_Sitemap_Cache_Data::set_sitemap - * @covers WPSEO_Sitemap_Cache_Data::is_usable - */ - public function test_transient_string_to_cache_data() { - - $sitemap = 'this is not a wpseo_sitemap_cache_data object'; - $type = 'post'; - $page = 1; - - $transient_key = WPSEO_Sitemaps_Cache_Validator::get_storage_key( $type, $page ); - - set_transient( $transient_key, $sitemap, DAY_IN_SECONDS ); - - $cache = new WPSEO_Sitemaps_Cache(); - $result = $cache->get_sitemap_data( $type, $page ); - - $this->assertEquals( $sitemap, get_transient( $transient_key ) ); - $this->assertNull( $result ); - } - - /** - * Test that a sitemap cache originally stored when WP was running on PHP < 7.4 can be retrieved and used in all PHP versions. - * - * @covers WPSEO_Sitemaps_Cache::get_sitemap_data - * @covers WPSEO_Sitemap_Cache_Data::__serialize - * @covers WPSEO_Sitemap_Cache_Data::__unserialize - * @covers WPSEO_Sitemap_Cache_Data::serialize - * @covers WPSEO_Sitemap_Cache_Data::unserialize - */ - public function test_retrieving_transient_stored_in_php_lt_74() { - - $sitemap = 'this is a wpseo_sitemap_cache_data object stored in PHP < 7.4'; - $type = 'post'; - $page = 1; - - $transient_key = WPSEO_Sitemaps_Cache_Validator::get_storage_key( $type, $page ); - - /* - * Using this filter, we effectively mock the get_transient() and - * the get_option() WP functions, including the call to maybe_unserialize() in get_option(). - * These functions are used in the get_sitemap[_data]() method to retrieve the transient. - * This filter short-circuits those function calls to return the specific value we need for this test. - */ - add_filter( - "pre_transient_{$transient_key}", - static function ( $pre_transient ) { - $pre_transient = 'C:24:"WPSEO_Sitemap_Cache_Data":107:{a:2:{s:6:"status";s:2:"ok";s:3:"xml";s:61:"this is a wpseo_sitemap_cache_data object stored in PHP < 7.4";}}'; - return maybe_unserialize( $pre_transient ); - } - ); - - $cache = new WPSEO_Sitemaps_Cache(); - $result = $cache->get_sitemap_data( $type, $page ); - - $this->assertInstanceOf( 'WPSEO_Sitemap_Cache_Data', $result ); - $this->assertSame( $sitemap, $result->get_sitemap() ); - $this->assertSame( 'ok', $result->get_status() ); - } - - /** - * Test that a sitemap cache originally stored when WP was running on PHP >= 7.4 can be retrieved and used - * without problems on PHP >= 7.4. - * - * This test also documents that when the cache was stored in PHP >= 7.4, but the PHP version on which WP - * is being run was subsequently downgraded to PHP < 7.4, the cache will be disregarded and the sitemap will - * need to be rebuild. - * - * For that particular scenario, this test also safeguards that the code under test doesn't yield any PHP - * errors when the unusable sitemap cache data is encountered. - * - * @covers WPSEO_Sitemaps_Cache::get_sitemap_data - * @covers WPSEO_Sitemap_Cache_Data::__serialize - * @covers WPSEO_Sitemap_Cache_Data::__unserialize - * @covers WPSEO_Sitemap_Cache_Data::serialize - * @covers WPSEO_Sitemap_Cache_Data::unserialize - */ - public function test_retrieving_transient_stored_in_php_gte_74() { - - $sitemap = 'this is a wpseo_sitemap_cache_data object stored in PHP >= 7.4'; - $type = 'post'; - $page = 1; - - $transient_key = WPSEO_Sitemaps_Cache_Validator::get_storage_key( $type, $page ); - - /* - * Using this filter, we effectively mock the get_transient() and - * the get_option() WP functions, including the call to maybe_unserialize() in get_option(). - * These functions are used in the get_sitemap[_data]() method to retrieve the transient. - * This filter short-circuits those function calls to return the specific value we need for this test. - */ - add_filter( - "pre_transient_{$transient_key}", - static function ( $pre_transient ) { - $pre_transient = 'O:24:"WPSEO_Sitemap_Cache_Data":2:{s:6:"status";s:2:"ok";s:3:"xml";s:62:"this is a wpseo_sitemap_cache_data object stored in PHP >= 7.4";}'; - return maybe_unserialize( $pre_transient ); - } - ); - - $cache = new WPSEO_Sitemaps_Cache(); - $result = $cache->get_sitemap_data( $type, $page ); - - if ( PHP_VERSION_ID >= 70400 ) { - // PHP 7.4+. - $this->assertInstanceOf( 'WPSEO_Sitemap_Cache_Data', $result ); - $this->assertSame( $sitemap, $result->get_sitemap() ); - $this->assertSame( 'ok', $result->get_status() ); - } - else { - // PHP 7.3 and lower. - $this->assertNull( $result ); - } - } - - /** - * Clearing all cache. - * - * @covers WPSEO_Sitemaps_Cache::clear - * @covers WPSEO_Sitemaps_Cache::clear_queued - */ - public function test_clear() { - - $type = 'page'; - $page = 1; - $test_content = 'test_content'; - - $cache_key = WPSEO_Sitemaps_Cache_Validator::get_storage_key( $type, $page ); - set_transient( $cache_key, $test_content ); - - // Act. - WPSEO_Sitemaps_Cache::clear(); - WPSEO_Sitemaps_Cache::clear_queued(); - - $cache_key = WPSEO_Sitemaps_Cache_Validator::get_storage_key( $type, $page ); - $content = get_transient( $cache_key ); - - // Assert. - $this->assertEquals( $test_content, $content ); - } - - /** - * Clearing specific cache. - * - * @covers WPSEO_Sitemaps_Cache::clear - * @covers WPSEO_Sitemaps_Cache::clear_queued - */ - public function test_clear_type() { - - $type = 'page'; - $page = 1; - $test_content = 'test_content'; - - $cache_key = WPSEO_Sitemaps_Cache_Validator::get_storage_key( $type, $page ); - set_transient( $cache_key, $test_content ); - - // Act. - WPSEO_Sitemaps_Cache::clear( [ $type ] ); - WPSEO_Sitemaps_Cache::clear_queued(); - - // Get the key again. - $cache_key = WPSEO_Sitemaps_Cache_Validator::get_storage_key( $type, $page ); - $result = get_transient( $cache_key ); - - // Assert. - $this->assertEquals( $test_content, $result ); - } - - /** - * Clearing specific cache should also clear index. - * - * @covers WPSEO_Sitemaps_Cache::clear - * @covers WPSEO_Sitemaps_Cache::clear_queued - */ - public function test_clear_index_also_cleared() { - - $test_index_content = 'test_content'; - - $index_cache_key = WPSEO_Sitemaps_Cache_Validator::get_storage_key(); - set_transient( $index_cache_key, $test_index_content ); - - /* - * The cache invalidator is based on time so if there isn't enough time - * difference between the two generations we will end up with the same - * cache invalidator, failing this test. - */ - usleep( 10000 ); - - // Act. - WPSEO_Sitemaps_Cache::clear( [ 'page' ] ); - WPSEO_Sitemaps_Cache::clear_queued(); - - // Get the key again. - $index_cache_key = WPSEO_Sitemaps_Cache_Validator::get_storage_key(); - $result = get_transient( $index_cache_key ); - - // Assert. - $this->assertEquals( $test_index_content, $result ); - } - - /** - * Clearing specific cache should not touch other type. - * - * @covers WPSEO_Sitemaps_Cache::clear - * @covers WPSEO_Sitemaps_Cache::clear_queued - */ - public function test_clear_type_isolation() { - - $type_a = 'page'; - $type_a_content = 'content_a'; - - $type_b = 'post'; - $type_b_content = 'content_b'; - - $type_a_key = WPSEO_Sitemaps_Cache_Validator::get_storage_key( $type_a ); - set_transient( $type_a_key, $type_a_content ); - - $type_b_key = WPSEO_Sitemaps_Cache_Validator::get_storage_key( $type_b ); - set_transient( $type_b_key, $type_b_content ); - - // Act. - WPSEO_Sitemaps_Cache::clear( [ $type_a ] ); - WPSEO_Sitemaps_Cache::clear_queued(); - - // Get the key again. - $type_b_key = WPSEO_Sitemaps_Cache_Validator::get_storage_key( $type_b ); - $result = get_transient( $type_b_key ); - - // Assert. - $this->assertEquals( $type_b_content, $result ); - } - - /** - * Make sure the hook is registered on registration. - * - * @covers WPSEO_Sitemaps_Cache::__construct - */ - public function test_register_clear_on_option_update() { - - new WPSEO_Sitemaps_Cache(); - // Hook will be added on default priority. - $has_action = has_action( - 'update_option', - [ 'WPSEO_Sitemaps_Cache', 'clear_on_option_update' ] - ); - $this->assertEquals( 10, $has_action ); - } - - /** - * Option update should clear cache for registered type. - * - * @covers WPSEO_Sitemaps_Cache::clear_queued - */ - public function test_clear_transient_cache() { - - $type = 'page'; - $page = 1; - $test_content = 'test_content'; - $option = 'my_option'; - - new WPSEO_Sitemaps_Cache(); - - WPSEO_Sitemaps_Cache::register_clear_on_option_update( $option, $type ); - - $cache_key = WPSEO_Sitemaps_Cache_Validator::get_storage_key( $type, $page ); - set_transient( $cache_key, $test_content ); - - // Act. - // Updating the option should clear cache for specified type. - do_action( 'update_option', $option ); - WPSEO_Sitemaps_Cache::clear_queued(); - - // Get the key again. - $cache_key = WPSEO_Sitemaps_Cache_Validator::get_storage_key( $type, $page ); - $result = get_transient( $cache_key ); - - // Assert. - $this->assertEquals( $test_content, $result ); - } - - /** - * Tests the attempt to clear the author sitemap for an unknown user, which should return false. - * - * @covers WPSEO_Sitemaps_Cache::invalidate_author - */ - public function test_clearing_author_sitemap_by_unknown_userid() { - $this->assertFalse( WPSEO_Sitemaps_Cache::invalidate_author( -1 ) ); - } - - /** - * Tests the attempt to clear the author sitemap for a user with the proper roles, which should return true. - * - * @covers WPSEO_Sitemaps_Cache::invalidate_author - */ - public function test_clearing_author_sitemap_by_userid() { - $user_id = $this->factory->user->create( - [ 'role' => 'administrator' ] - ); - - $this->assertTrue( WPSEO_Sitemaps_Cache::invalidate_author( $user_id ) ); - } - - /** - * Tests the attempt to clear the author sitemap for a user with the subscriber role, which should return false. - * - * @covers WPSEO_Sitemaps_Cache::invalidate_author - */ - public function test_clearing_author_sitemap_by_userid_with_subscriber_role() { - $user_id = $this->factory->user->create( - [ 'role' => 'subscriber' ] - ); - - $this->assertFalse( WPSEO_Sitemaps_Cache::invalidate_author( $user_id ) ); - } -} diff --git a/tests/integration/sitemaps/test-class-wpseo-sitemaps.php b/tests/integration/sitemaps/test-class-wpseo-sitemaps.php index 6996af9e1c3..b8411a9f94f 100644 --- a/tests/integration/sitemaps/test-class-wpseo-sitemaps.php +++ b/tests/integration/sitemaps/test-class-wpseo-sitemaps.php @@ -113,7 +113,7 @@ public function test_index_links_filter() { static function( $links ) { $links[] = [ 'loc' => 'test-sitemap.xml', - 'lastmod' => date( '1' ), + 'lastmod' => gmdate( '1' ), ]; return $links; } @@ -123,42 +123,4 @@ static function( $links ) { $this->expectOutputContains( 'test-sitemap.xml' ); } - - /** - * Test for last modified date. - * - * @covers WPSEO_Sitemaps::get_last_modified_gmt - */ - public function test_last_modified_post_type() { - - $older_date = '2015-01-01 12:00:00'; - $newest_date = '2016-01-01 12:00:00'; - - $post_type_args = [ - 'public' => true, - 'has_archive' => true, - ]; - register_post_type( 'yoast', $post_type_args ); - - $post_args = [ - 'post_status' => 'publish', - 'post_type' => 'yoast', - 'post_date' => $newest_date, - ]; - $this->factory->post->create( $post_args ); - - $post_args['post_date'] = $older_date; - $this->factory->post->create( $post_args ); - - $this->assertEquals( $newest_date, WPSEO_Sitemaps::get_last_modified_gmt( [ 'yoast' ] ) ); - } - - /** - * Test for last modified date with invalid post types. - * - * @covers WPSEO_Sitemaps::get_last_modified_gmt - */ - public function test_last_modified_with_invalid_post_type() { - $this->assertFalse( WPSEO_Sitemaps::get_last_modified_gmt( [ 'invalid_post_type' ] ) ); - } } diff --git a/tests/unit/builders/indexable-author-builder-test.php b/tests/unit/builders/indexable-author-builder-test.php index 2febb5a6dc7..d8a26c26e94 100644 --- a/tests/unit/builders/indexable-author-builder-test.php +++ b/tests/unit/builders/indexable-author-builder-test.php @@ -127,6 +127,7 @@ protected function set_up() { FROM {$this->wpdb->posts} AS p WHERE p.post_status IN (%s) AND p.post_author = %d + AND p.post_password = \"\" AND p.post_type IN (%s, %s) ", [ 'publish', 1, 'post', 'my-cpt' ] diff --git a/tests/unit/builders/indexable-post-type-archive-builder-test.php b/tests/unit/builders/indexable-post-type-archive-builder-test.php index 6cee5340d22..bbf412a9541 100644 --- a/tests/unit/builders/indexable-post-type-archive-builder-test.php +++ b/tests/unit/builders/indexable-post-type-archive-builder-test.php @@ -66,6 +66,7 @@ public function test_build() { FROM {$wpdb->posts} AS p WHERE p.post_status IN (%s) AND p.post_type = %s + AND p.post_password = \"\" ", [ 'publish', 'my-post-type' ] )->andReturn( 'PREPARED_QUERY' ); diff --git a/tests/unit/builders/indexable-term-builder-test.php b/tests/unit/builders/indexable-term-builder-test.php index 0bd0b71d2e3..77c37329228 100644 --- a/tests/unit/builders/indexable-term-builder-test.php +++ b/tests/unit/builders/indexable-term-builder-test.php @@ -273,6 +273,7 @@ public function test_build() { AND term_tax.taxonomy = %s AND term_tax.term_id = %d WHERE p.post_status IN (%s) + AND p.post_password = \"\" ", [ 'category', 1, 'publish' ] )->andReturn( 'PREPARED_QUERY' );