/** * WPSEO plugin file. * * @package WPSEO\XML_Sitemaps */ /** * Sitemap provider for author archives. */ class WPSEO_Post_Type_Sitemap_Provider implements WPSEO_Sitemap_Provider { /** @var string $home_url Holds the home_url() value. */ protected static $home_url; /** @var WPSEO_Sitemap_Image_Parser $image_parser Holds image parser instance. */ protected static $image_parser; /** @var object $classifier Holds instance of classifier for a link. */ protected static $classifier; /** @var int $page_on_front_id Static front page ID. */ protected static $page_on_front_id; /** @var int $page_for_posts_id Posts page ID. */ protected static $page_for_posts_id; /** * Set up object properties for data reuse. */ public function __construct() { add_filter( 'save_post', array( $this, 'save_post' ) ); } /** * Get all the options * * @deprecated 7.0 */ protected function get_options() { _deprecated_function( __METHOD__, 'WPSEO 7.0', 'WPSEO_Options::get' ); } /** * Get front page ID * * @return int */ protected function get_page_on_front_id() { if ( ! isset( self::$page_on_front_id ) ) { self::$page_on_front_id = (int) get_option( 'page_on_front' ); } return self::$page_on_front_id; } /** * Get page for posts ID * * @return int */ protected function get_page_for_posts_id() { if ( ! isset( self::$page_for_posts_id ) ) { self::$page_for_posts_id = (int) get_option( 'page_for_posts' ); } return self::$page_for_posts_id; } /** * 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; } /** * Get the Classifier for a link * * @return WPSEO_Link_Type_Classifier */ protected function get_classifier() { if ( ! isset( self::$classifier ) ) { self::$classifier = new WPSEO_Link_Type_Classifier( $this->get_home_url() ); } return self::$classifier; } /** * Get Home URL * * This has been moved from the constructor because wp_rewrite is not available on plugins_loaded in multisite. * It will now be requested on need and not on initialization. * * @return string */ protected function get_home_url() { if ( ! isset( self::$home_url ) ) { self::$home_url = WPSEO_Utils::home_url(); } return self::$home_url; } /** * Check if provider supports given item type. * * @param string $type Type string to check for. * * @return boolean */ public function handles_type( $type ) { return post_type_exists( $type ); } /** * @param int $max_entries Entries per sitemap. * * @return array */ public function get_index_links( $max_entries ) { global $wpdb; // Consider using WPSEO_Post_Type::get_accessible_post_types() to filter out any `no-index` post-types. $post_types = WPSEO_Post_Type::get_accessible_post_types(); $post_types = array_filter( $post_types, array( $this, 'is_valid_post_type' ) ); $last_modified_times = WPSEO_Sitemaps::get_last_modified_gmt( $post_types, true ); $index = array(); foreach ( $post_types as $post_type ) { $total_count = $this->get_post_type_count( $post_type ); if ( $total_count === 0 ) { continue; } $max_pages = 1; if ( $total_count > $max_entries ) { $max_pages = (int) ceil( $total_count / $max_entries ); } $all_dates = array(); if ( $max_pages > 1 ) { $sql = " SELECT post_modified_gmt FROM ( SELECT @rownum:=0 ) init JOIN {$wpdb->posts} USE INDEX( type_status_date ) WHERE post_status IN ( 'publish', 'inherit' ) 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 ) { if ( ! empty( $last_modified_times[ $post_type ] ) ) { $date = $last_modified_times[ $post_type ]; } } else { $date = $all_dates[ $page_counter ]; } $index[] = array( 'loc' => WPSEO_Sitemaps_Router::get_base_url( $post_type . '-sitemap' . $current_page . '.xml' ), 'lastmod' => $date, ); } } return $index; } /** * Get set of sitemap link data. * * @param string $type Sitemap type. * @param int $max_entries Entries per sitemap. * @param int $current_page Current page of the sitemap. * * @return array */ public function get_sitemap_links( $type, $max_entries, $current_page ) { $links = array(); $post_type = $type; if ( ! $this->is_valid_post_type( $post_type ) ) { return $links; } $steps = min( 100, $max_entries ); $offset = ( $current_page > 1 ) ? ( ( $current_page - 1 ) * $max_entries ) : 0; $total = ( $offset + $max_entries ); $typecount = $this->get_post_type_count( $post_type ); if ( $total > $typecount ) { $total = $typecount; } if ( $current_page === 1 ) { $links = array_merge( $links, $this->get_first_links( $post_type ) ); } if ( $typecount === 0 ) { return $links; } $stacked_urls = array(); $posts_to_exclude = $this->get_excluded_posts(); 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 ); if ( ! isset( $url['loc'] ) ) { continue; } /** * 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 ) ) { continue; } if ( $post->ID === $this->get_page_for_posts_id() || $post->ID === $this->get_page_on_front_id() ) { array_unshift( $links, $url ); continue; } $links[] = $url; } 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 ); } } /** * Check if post type should be present in sitemaps. * * @param string $post_type Post type string to check for. * * @return bool */ public function is_valid_post_type( $post_type ) { if ( ! WPSEO_Post_Type::is_post_type_indexable( $post_type ) ) { return false; } /** * 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, $post_type ) ) { return false; } return true; } /** * Retrieves a list with the excluded post ids. * * @return array Array with post ids to exclude. */ protected function get_excluded_posts() { /** * Filter: 'wpseo_exclude_from_sitemap_by_post_ids' - Allow extending and modifying the posts to exclude. * * @api array $posts_to_exclude The posts to exclude. */ $excluded_posts_ids = apply_filters( 'wpseo_exclude_from_sitemap_by_post_ids', array() ); if ( ! is_array( $excluded_posts_ids ) || $excluded_posts_ids === array() ) { return array(); } return array_map( 'intval', $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 = array(); $needs_archive = true; if ( ! $this->get_page_on_front_id() && ( $post_type === 'post' || $post_type === 'page' ) ) { $links[] = array( 'loc' => $this->get_home_url(), // Deprecated, kept for backwards data compat. R. 'chf' => 'daily', 'pri' => 1, ); $needs_archive = false; } elseif ( $this->get_page_on_front_id() && $post_type === 'post' && $this->get_page_for_posts_id() ) { $page_for_posts_url = get_permalink( $this->get_page_for_posts_id() ); $links[] = array( 'loc' => $page_for_posts_url, // Deprecated, kept for backwards data compat. R. 'chf' => 'daily', 'pri' => 1, ); $needs_archive = false; } if ( ! $needs_archive ) { return $links; } $archive_url = $this->get_post_type_archive_link( $post_type ); /** * 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', $archive_url, $post_type ); if ( $archive_url ) { /** * Filter the priority of the URL Yoast SEO uses in the XML sitemap. * * @param float $priority The priority for this URL, ranging from 0 to 1 * @param string $post_type The post type this archive is for. */ $links[] = array( '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 ) { if ( WPSEO_Options::get( 'noindex-ptarchive-' . $post_type, false ) ) { return false; } // Post archive should be excluded if it isn't front page or posts page. if ( $post_type === 'post' && get_option( 'show_on_front' ) !== 'posts' && ! $this->get_page_for_posts_id() ) { return false; } $archive_url = get_post_type_archive_link( $post_type ); return $archive_url; } /** * 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 = array(); if ( ! isset( $filters[ $post_type ] ) ) { // Make sure you're wpdb->preparing everything you throw into this!! $filters[ $post_type ] = array( /** * 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: http://wordpress.org/support/topic/plugin-wordpress-seo-by-yoast-performance-suggestion. // Also see 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_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 = array(); foreach ( $posts as $post ) { $post->post_type = $post_type; $post->post_status = 'publish'; $post->filter = 'sample'; $post->ID = (int) $post->ID; $post->post_parent = (int) $post->post_parent; $post->post_author = (int) $post->post_author; $post_ids[] = $post->ID; } update_meta_cache( 'post', $post_ids ); return $posts; } /** * @param string $post_type Post type slug. * * @return string */ protected function get_sql_where_clause( $post_type ) { global $wpdb; $join = ''; $status = "{$wpdb->posts}.post_status = 'publish'"; // Based on WP_Query->get_posts(). R. if ( 'attachment' === $post_type ) { $join = " LEFT JOIN {$wpdb->posts} AS p2 ON ({$wpdb->posts}.post_parent = p2.ID) "; $status = "p2.post_status = 'publish' AND p2.post_password = ''"; } $where_clause = " {$join} WHERE {$status} 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 = array(); /** * 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 ); /** * Do not include external URLs. * * @see https://wordpress.org/plugins/page-links-to/ can rewrite permalinks to external URLs. */ if ( $this->get_classifier()->classify( $url['loc'] ) === WPSEO_Link::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. $url['images'] = $this->get_image_parser()->get_images( $post ); return $url; } }