This commit is contained in:
2024-05-20 15:37:46 +03:00
commit 00b7dbd0b7
10404 changed files with 3285853 additions and 0 deletions

View File

@ -0,0 +1,244 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\XML_Sitemaps
*/
/**
* Sitemap provider for author archives.
*/
class WPSEO_Author_Sitemap_Provider implements WPSEO_Sitemap_Provider {
/**
* Check if provider supports given item type.
*
* @param string $type Type string to check for.
*
* @return bool
*/
public function handles_type( $type ) {
// If the author archives have been disabled, we don't do anything.
if ( WPSEO_Options::get( 'disable-author', false ) || WPSEO_Options::get( 'noindex-author-wpseo', false ) ) {
return false;
}
return $type === 'author';
}
/**
* Get the links for the sitemap index.
*
* @param int $max_entries Entries per sitemap.
*
* @return array
*/
public function get_index_links( $max_entries ) {
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' );
}
if ( empty( $users ) ) {
return [];
}
$index = [];
$user_pages = array_chunk( $users, $max_entries );
foreach ( $user_pages as $page_counter => $users_page ) {
$current_page = ( $page_counter === 0 ) ? '' : ( $page_counter + 1 );
$user_id = array_shift( $users_page ); // Time descending, first user on page is most recently updated.
$user = get_user_by( 'id', $user_id );
$index[] = [
'loc' => WPSEO_Sitemaps_Router::get_base_url( 'author-sitemap' . $current_page . '.xml' ),
'lastmod' => ( $user->_yoast_wpseo_profile_updated ) ? YoastSEO()->helpers->date->format_timestamp( $user->_yoast_wpseo_profile_updated ) : null,
];
}
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',
],
],
],
];
if ( WPSEO_Options::get( 'noindex-author-noposts-wpseo', true ) ) {
unset( $defaults['capability'] ); // Otherwise it cancels out next argument.
$defaults['has_published_posts'] = YoastSEO()->helpers->author_archive->get_author_archive_post_types();
}
return get_users( array_merge( $defaults, $arguments ) );
}
/**
* 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
*
* @throws OutOfBoundsException When an invalid page is requested.
*/
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 $links;
}
/**
* 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',
],
],
];
$users = get_users( $user_criteria );
$time = time();
foreach ( $users as $user ) {
update_user_meta( $user->ID, '_yoast_wpseo_profile_updated', $time );
}
return count( $users );
}
/**
* Wrap legacy filter to deduplicate calls.
*
* @param array $users Array of user objects to filter.
*
* @return array
*/
protected function exclude_users( $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 );
}
}

View File

@ -0,0 +1,667 @@
<?php
/**
* WPSEO plugin file.
*
* @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_action( '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;
}
/**
* Check if provider supports given item type.
*
* @param string $type Type string to check for.
*
* @return bool
*/
public function handles_type( $type ) {
return post_type_exists( $type );
}
/**
* Retrieves the sitemap links.
*
* @param int $max_entries Entries per sitemap.
*
* @return array
*/
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 );
if ( $total_count === 0 ) {
continue;
}
$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 = ( $page_counter === 0 ) ? '' : ( $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[] = [
'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
*
* @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 ) ) {
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;
}
$posts_to_exclude = $this->get_excluded_posts( $type );
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 ) ) {
$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_accessible( $post_type ) || ! 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.
*
* @param string $post_type Post type.
*
* @return array Array with post ids to exclude.
*/
protected function get_excluded_posts( $post_type ) {
$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.
*
* @api array $posts_to_exclude The posts to exclude.
*/
$excluded_posts_ids = apply_filters( 'wpseo_exclude_from_sitemap_by_post_ids', $excluded_posts_ids );
if ( ! is_array( $excluded_posts_ids ) ) {
$excluded_posts_ids = [];
}
$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,
];
}
/**
* Filters the first post type links.
*
* @param array $links The first post type links.
* @param string $post_type The post type this archive is for.
*/
return apply_filters( 'wpseo_sitemap_post_type_first_links', $links, $post_type );
}
/**
* 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;
}
}

View File

@ -0,0 +1,198 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\XML_Sitemaps
*/
/**
* Sitemap Cache Data object, manages sitemap data stored in cache.
*/
class WPSEO_Sitemap_Cache_Data implements Serializable, WPSEO_Sitemap_Cache_Data_Interface {
/**
* Sitemap XML data.
*
* @var string
*/
private $sitemap = '';
/**
* Status of the sitemap, usable or not.
*
* @var string
*/
private $status = self::UNKNOWN;
/**
* Set the sitemap XML data
*
* @param string $sitemap XML Content of the sitemap.
*/
public function set_sitemap( $sitemap ) {
if ( ! is_string( $sitemap ) ) {
$sitemap = '';
}
$this->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 $usable Is the sitemap usable or not.
*
* @return void
*/
public function set_status( $usable ) {
if ( $usable === self::OK ) {
$this->status = self::OK;
return;
}
if ( $usable === 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 );
}
}

View File

@ -0,0 +1,495 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\XML_Sitemaps
*/
/**
* Parses images from the given post.
*/
class WPSEO_Sitemap_Image_Parser {
/**
* Holds the home_url() value to speed up loops.
*
* @var string
*/
protected $home_url = '';
/**
* Holds site URL hostname.
*
* @var string
*/
protected $host = '';
/**
* Holds site URL protocol.
*
* @var string
*/
protected $scheme = 'http';
/**
* Cached set of attachments for multiple posts.
*
* @var array
*/
protected $attachments = [];
/**
* Holds blog charset value for use in DOM parsing.
*
* @var string
*/
protected $charset = 'UTF-8';
/**
* Set up URL properties for reuse.
*/
public function __construct() {
$this->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 ) );
$images[] = $this->get_image_item( $post, $src );
}
/**
* 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'] );
}
foreach ( $this->parse_galleries( $content, $post->ID ) as $attachment ) {
$src = $this->get_absolute_url( $this->image_url( $attachment->ID ) );
$images[] = $this->get_image_item( $post, $src );
}
if ( $post->post_type === 'attachment' && wp_attachment_is_image( $post ) ) {
$src = $this->get_absolute_url( $this->image_url( $post->ID ) );
$images[] = $this->get_image_item( $post, $src );
}
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 ) ),
];
}
return $images;
}
/**
* Parse `<img />` 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( '<?xml encoding="' . $this->charset . '">' . $content );
// Clear the errors, so they don't get kept in memory.
libxml_clear_errors();
/**
* Image attribute.
*
* @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<id>\d+)|', $class, $matches )
&& get_post_status( $matches['id'] )
) {
$query_params = wp_parse_url( $src, PHP_URL_QUERY );
$src = $this->image_url( $matches['id'] );
if ( $query_params ) {
$src .= '?' . $query_params;
}
}
$src = $this->get_absolute_url( $src );
if ( strpos( $src, $this->host ) === false ) {
continue;
}
if ( $src !== esc_url( $src, null, 'attribute' ) ) {
continue;
}
$images[] = [
'src' => $src,
];
}
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.
*
* @return array
*/
protected function get_image_item( $post, $src ) {
$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 );
/**
* Filter image data to be included in XML sitemap for the post.
*
* @param array $image {
* Array of image data.
*
* @type string $src Image URL.
* }
*
* @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 $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 );
}
}

View File

@ -0,0 +1,153 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\Admin\XML Sitemaps
*/
/**
* Class that handles the Admin side of XML sitemaps.
*/
class WPSEO_Sitemaps_Admin {
/**
* Post_types that are being imported.
*
* @var array
*/
private $importing_post_types = [];
/**
* Class constructor.
*/
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', '' );
}
/**
* Hooked into transition_post_status. Will initiate search engine pings
* if the post is being published, is a post type that a sitemap is built for
* and is a post that is included in sitemaps.
*
* @param string $new_status New post status.
* @param string $old_status Old post status.
* @param \WP_Post $post Post object.
*/
public function status_transition( $new_status, $old_status, $post ) {
if ( $new_status !== 'publish' ) {
return;
}
if ( defined( 'WP_IMPORTING' ) ) {
$this->status_transition_bulk( $new_status, $old_status, $post );
return;
}
$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;
}
// If the post type is excluded in options, we can stop.
if ( WPSEO_Options::get( 'noindex-' . $post_type, false ) ) {
return;
}
if ( ! YoastSEO()->helpers->environment->is_production_mode() ) {
return;
}
$this->ping_search_engines();
}
/**
* Notify Google of the updated sitemap.
*/
public function ping_search_engines() {
if ( get_option( 'blog_public' ) === '0' ) { // Don't ping if blog is not public.
return;
}
/**
* Filter: 'wpseo_allow_xml_sitemap_ping' - Check if pinging is not allowed (allowed by default).
*
* @api boolean $allow_ping The boolean that is set to true by default.
*/
if ( apply_filters( 'wpseo_allow_xml_sitemap_ping', true ) === false ) {
return;
}
$url = rawurlencode( WPSEO_Sitemaps_Router::get_base_url( 'sitemap_index.xml' ) );
// Ping Google about our sitemap change.
wp_remote_get( 'https://www.google.com/ping?sitemap=' . $url, [ 'blocking' => false ] );
if ( ! defined( 'WPSEO_PREMIUM_FILE' ) || WPSEO_Options::get( 'enable_index_now' ) === false ) {
wp_remote_get( 'https://www.bing.com/ping?sitemap=' . $url, [ 'blocking' => false ] );
}
}
/**
* While bulk importing, just save unique post_types.
*
* When importing is done, if we have a post_type that is saved in the sitemap
* try to ping the search engines.
*
* @param string $new_status New post status.
* @param string $old_status Old post status.
* @param \WP_Post $post Post object.
*/
private function status_transition_bulk( $new_status, $old_status, $post ) {
$this->importing_post_types[] = get_post_type( $post );
$this->importing_post_types = array_unique( $this->importing_post_types );
}
/**
* After import finished, walk through imported post_types and update info.
*/
public function status_transition_bulk_finished() {
if ( ! defined( 'WP_IMPORTING' ) ) {
return;
}
if ( empty( $this->importing_post_types ) ) {
return;
}
$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.
if ( $post_type === 'nav_menu_item' ) {
continue;
}
if ( WPSEO_Options::get( 'noindex-' . $post_type, false ) === false ) {
$ping_search_engines = true;
}
}
// Nothing to do.
if ( $ping_search_engines === false ) {
return;
}
if ( WP_CACHE ) {
do_action( 'wpseo_hit_sitemap_index' );
}
$this->ping_search_engines();
}
}

View File

@ -0,0 +1,319 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\XML_Sitemaps
*/
/**
* Handles storage keys for sitemaps caching and invalidation.
*
* @since 3.2
*/
class WPSEO_Sitemaps_Cache_Validator {
/**
* Prefix of the transient key for sitemap caches.
*
* @var string
*/
const STORAGE_KEY_PREFIX = 'yst_sm_';
/**
* Name of the option that holds the global validation value.
*
* @var string
*/
const VALIDATION_GLOBAL_KEY = 'wpseo_sitemap_cache_validator_global';
/**
* The format which creates the key of the option that holds the type validation value.
*
* @var string
*/
const VALIDATION_TYPE_KEY_FORMAT = 'wpseo_sitemap_%s_cache_validator';
/**
* Get the cache key for a certain type and page.
*
* A type of cache would be something like 'page', 'post' or 'video'.
*
* Example key format for sitemap type "post", page 1: wpseo_sitemap_post_1:akfw3e_23azBa .
*
* @since 3.2
*
* @param string|null $type The type to get the key for. Null or self::SITEMAP_INDEX_TYPE for index cache.
* @param int $page The page of cache to get the key for.
*
* @return bool|string The key where the cache is stored on. False if the key could not be generated.
*/
public static function get_storage_key( $type = null, $page = 1 ) {
// Using SITEMAP_INDEX_TYPE for sitemap index cache.
$type = is_null( $type ) ? WPSEO_Sitemaps::SITEMAP_INDEX_TYPE : $type;
$global_cache_validator = self::get_validator();
$type_cache_validator = self::get_validator( $type );
$prefix = self::STORAGE_KEY_PREFIX;
$postfix = sprintf( '_%d:%s_%s', $page, $global_cache_validator, $type_cache_validator );
try {
$type = self::truncate_type( $type, $prefix, $postfix );
} catch ( OutOfBoundsException $exception ) {
// Maybe do something with the exception, for now just mark as invalid.
return false;
}
// Build key.
$full_key = $prefix . $type . $postfix;
return $full_key;
}
/**
* If the type is over length make sure we compact it so we don't have any database problems.
*
* When there are more 'extremely long' post types, changes are they have variations in either the start or ending.
* Because of this, we cut out the excess in the middle which should result in less chance of collision.
*
* @since 3.2
*
* @param string $type The type of sitemap to be used.
* @param string $prefix The part before the type in the cache key. Only the length is used.
* @param string $postfix The part after the type in the cache key. Only the length is used.
*
* @return string The type with a safe length to use
*
* @throws OutOfRangeException When there is less than 15 characters of space for a key that is originally longer.
*/
public static function truncate_type( $type, $prefix = '', $postfix = '' ) {
/*
* This length has been restricted by the database column length of 64 in the past.
* The prefix added by WordPress is '_transient_' because we are saving to a transient.
* We need to use a timeout on the transient, otherwise the values get autoloaded, this adds
* another restriction to the length.
*/
$max_length = 45; // 64 - 19 ('_transient_timeout_')
$max_length -= strlen( $prefix );
$max_length -= strlen( $postfix );
if ( strlen( $type ) > $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;
}
}

View File

@ -0,0 +1,353 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\XML_Sitemaps
*/
/**
* Handles sitemaps caching and invalidation.
*
* @since 3.2
*/
class WPSEO_Sitemaps_Cache {
/**
* Holds the options that, when updated, should cause the cache to clear.
*
* @var array
*/
protected static $cache_clear = [];
/**
* Mirror of enabled status for static calls.
*
* @var bool
*/
protected static $is_enabled = false;
/**
* Holds the flag to clear all cache.
*
* @var bool
*/
protected static $clear_all = false;
/**
* Holds the array of types to clear.
*
* @var array
*/
protected static $clear_types = [];
/**
* Hook methods for invalidation on necessary events.
*/
public function __construct() {
add_action( 'init', [ $this, 'init' ] );
add_action( 'deleted_term_relationships', [ __CLASS__, 'invalidate' ] );
add_action( 'update_option', [ __CLASS__, 'clear_on_option_update' ] );
add_action( 'edited_terms', [ __CLASS__, 'invalidate_helper' ], 10, 2 );
add_action( 'clean_term_cache', [ __CLASS__, 'invalidate_helper' ], 10, 2 );
add_action( 'clean_object_term_cache', [ __CLASS__, 'invalidate_helper' ], 10, 2 );
add_action( 'user_register', [ __CLASS__, 'invalidate_author' ] );
add_action( 'delete_user', [ __CLASS__, 'invalidate_author' ] );
add_action( 'shutdown', [ __CLASS__, 'clear_queued' ] );
}
/**
* Setup context for static calls.
*/
public function init() {
self::$is_enabled = $this->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 );
}
}
}
}

View File

@ -0,0 +1,347 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\XML_Sitemaps
*/
/**
* Renders XML output for sitemaps.
*/
class WPSEO_Sitemaps_Renderer {
/**
* XSL stylesheet for styling a sitemap for web browsers.
*
* @var string
*/
protected $stylesheet = '';
/**
* Holds the get_bloginfo( 'charset' ) value to reuse for performance.
*
* @var string
*/
protected $charset = 'UTF-8';
/**
* Holds charset of output, might be converted.
*
* @var string
*/
protected $output_charset = 'UTF-8';
/**
* If data encoding needs to be converted for output.
*
* @var bool
*/
protected $needs_conversion = false;
/**
* Set up object properties.
*/
public function __construct() {
$stylesheet_url = preg_replace( '/(^http[s]?:)/', '', $this->get_xsl_url() );
$this->stylesheet = '<?xml-stylesheet type="text/xsl" href="' . esc_url( $stylesheet_url ) . '"?>';
$this->charset = get_bloginfo( 'charset' );
$this->output_charset = $this->charset;
if (
$this->charset !== 'UTF-8'
&& function_exists( 'mb_list_encodings' )
&& in_array( $this->charset, mb_list_encodings(), true )
) {
$this->output_charset = 'UTF-8';
}
$this->needs_conversion = $this->output_charset !== $this->charset;
}
/**
* Builds the sitemap index.
*
* @param array $links Set of sitemaps index links.
*
* @return string
*/
public function get_index( $links ) {
$xml = '<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . "\n";
foreach ( $links as $link ) {
$xml .= $this->sitemap_index_url( $link );
}
/**
* Filter to append sitemaps to the index.
*
* @param string $index String to append to sitemaps index, defaults to empty.
*/
$xml .= apply_filters( 'wpseo_sitemap_index', '' );
$xml .= '</sitemapindex>';
return $xml;
}
/**
* Builds the sitemap.
*
* @param array $links Set of sitemap links.
* @param string $type Sitemap type.
* @param int $current_page Current sitemap page number.
*
* @return string
*/
public function get_sitemap( $links, $type, $current_page ) {
$urlset = '<urlset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" '
. 'xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd '
. 'http://www.google.com/schemas/sitemap-image/1.1 http://www.google.com/schemas/sitemap-image/1.1/sitemap-image.xsd" '
. 'xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' . "\n";
/**
* Filters the `urlset` for a sitemap by type.
*
* @api string $urlset The output for the sitemap's `urlset`.
*/
$xml = apply_filters( "wpseo_sitemap_{$type}_urlset", $urlset );
foreach ( $links as $url ) {
$xml .= $this->sitemap_url( $url );
}
/**
* Filter to add extra URLs to the XML sitemap by type.
*
* Only runs for the first page, not on all.
*
* @param string $content String content to add, defaults to empty.
*/
if ( $current_page === 1 ) {
$xml .= apply_filters( "wpseo_sitemap_{$type}_content", '' );
}
$xml .= '</urlset>';
return $xml;
}
/**
* Produce final XML output with debug information.
*
* @param string $sitemap Sitemap XML.
*
* @return string
*/
public function get_output( $sitemap ) {
$output = '<?xml version="1.0" encoding="' . esc_attr( $this->output_charset ) . '"?>';
if ( $this->stylesheet ) {
/**
* Filter the stylesheet URL for the XML sitemap.
*
* @param string $stylesheet Stylesheet URL.
*/
$output .= apply_filters( 'wpseo_stylesheet_url', $this->stylesheet ) . "\n";
}
$output .= $sitemap;
$output .= "\n<!-- XML Sitemap generated by Yoast SEO -->";
return $output;
}
/**
* Get charset for the output.
*
* @return string
*/
public function get_output_charset() {
return $this->output_charset;
}
/**
* Set a custom stylesheet for this sitemap. Set to empty to just remove the default stylesheet.
*
* @param string $stylesheet Full XML-stylesheet declaration.
*/
public function set_stylesheet( $stylesheet ) {
$this->stylesheet = $stylesheet;
}
/**
* Build the `<sitemap>` tag for a given URL.
*
* @param array $url Array of parts that make up this entry.
*
* @return string
*/
protected function sitemap_index_url( $url ) {
$date = null;
if ( ! empty( $url['lastmod'] ) ) {
$date = YoastSEO()->helpers->date->format( $url['lastmod'] );
}
$url['loc'] = htmlspecialchars( $url['loc'], ENT_COMPAT, $this->output_charset, false );
$output = "\t<sitemap>\n";
$output .= "\t\t<loc>" . $url['loc'] . "</loc>\n";
$output .= empty( $date ) ? '' : "\t\t<lastmod>" . htmlspecialchars( $date, ENT_COMPAT, $this->output_charset, false ) . "</lastmod>\n";
$output .= "\t</sitemap>\n";
return $output;
}
/**
* Build the `<url>` tag for a given URL.
*
* Public access for backwards compatibility reasons.
*
* @param array $url Array of parts that make up this entry.
*
* @return string
*/
public function sitemap_url( $url ) {
$date = null;
if ( ! empty( $url['mod'] ) ) {
// Create a DateTime object date in the correct timezone.
$date = YoastSEO()->helpers->date->format( $url['mod'] );
}
$output = "\t<url>\n";
$output .= "\t\t<loc>" . $this->encode_and_escape( $url['loc'] ) . "</loc>\n";
$output .= empty( $date ) ? '' : "\t\t<lastmod>" . htmlspecialchars( $date, ENT_COMPAT, $this->output_charset, false ) . "</lastmod>\n";
if ( empty( $url['images'] ) ) {
$url['images'] = [];
}
foreach ( $url['images'] as $img ) {
if ( empty( $img['src'] ) ) {
continue;
}
$output .= "\t\t<image:image>\n";
$output .= "\t\t\t<image:loc>" . $this->encode_and_escape( $img['src'] ) . "</image:loc>\n";
$output .= "\t\t</image:image>\n";
}
unset( $img );
$output .= "\t</url>\n";
/**
* 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.
*/
return apply_filters( 'wpseo_sitemap_url', $output, $url );
}
/**
* Ensure the URL is encoded per RFC3986 and correctly escaped for use in an XML sitemap.
*
* This method works around a two quirks in esc_url():
* 1. `esc_url()` leaves schema-relative URLs alone, while according to the sitemap specs,
* the URL must always begin with a protocol.
* 2. `esc_url()` escapes ampersands as `&#038;` instead of the more common `&amp;`.
* According to the specs, `&amp;` should be used, and even though this shouldn't
* really make a difference in practice, to quote Jono: "I'd be nervous about &#038;
* given how many weird and wonderful things eat sitemaps", so better safe than sorry.
*
* @link https://www.sitemaps.org/protocol.html#xmlTagDefinitions
* @link https://www.sitemaps.org/protocol.html#escaping
* @link https://developer.wordpress.org/reference/functions/esc_url/
*
* @param string $url URL to encode and escape.
*
* @return string
*/
protected function encode_and_escape( $url ) {
$url = $this->encode_url_rfc3986( $url );
$url = esc_url( $url );
$url = str_replace( '&#038;', '&amp;', $url );
$url = str_replace( '&#039;', '&apos;', $url );
if ( strpos( $url, '//' ) === 0 ) {
// Schema-relative URL for which esc_url() does not add a scheme.
$url = 'http:' . $url;
}
return $url;
}
/**
* Apply some best effort conversion to comply with RFC3986.
*
* @param string $url URL to encode.
*
* @return string
*/
protected function encode_url_rfc3986( $url ) {
if ( filter_var( $url, FILTER_VALIDATE_URL ) ) {
return $url;
}
$path = wp_parse_url( $url, PHP_URL_PATH );
if ( ! empty( $path ) && $path !== '/' ) {
$encoded_path = explode( '/', $path );
// First decode the path, to prevent double encoding.
$encoded_path = array_map( 'rawurldecode', $encoded_path );
$encoded_path = array_map( 'rawurlencode', $encoded_path );
$encoded_path = implode( '/', $encoded_path );
$url = str_replace( $path, $encoded_path, $url );
}
$query = wp_parse_url( $url, PHP_URL_QUERY );
if ( ! empty( $query ) ) {
parse_str( $query, $parsed_query );
$parsed_query = http_build_query( $parsed_query, '', '&amp;', PHP_QUERY_RFC3986 );
$url = str_replace( $query, $parsed_query, $url );
}
return $url;
}
/**
* Retrieves the XSL URL that should be used in the current environment
*
* When home_url and site_url are not the same, the home_url should be used.
* This is because the XSL needs to be served from the same domain, protocol and port
* as the XML file that is loading it.
*
* @return string The XSL URL that needs to be used.
*/
protected function get_xsl_url() {
if ( home_url() !== site_url() ) {
return home_url( 'main-sitemap.xsl' );
}
/*
* Fallback to circumvent a cross-domain security problem when the XLS file is
* loaded from a different (sub)domain.
*/
if ( strpos( plugins_url(), home_url() ) !== 0 ) {
return home_url( 'main-sitemap.xsl' );
}
return plugin_dir_url( WPSEO_FILE ) . 'css/main-sitemap.xsl';
}
}

View File

@ -0,0 +1,132 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\XML_Sitemaps
*/
use Yoast\WP\SEO\Conditionals\Deactivating_Yoast_Seo_Conditional;
/**
* Rewrite setup and handling for sitemaps functionality.
*/
class WPSEO_Sitemaps_Router {
/**
* Sets up init logic.
*/
public function __construct() {
// If we add rewrite rules during the plugin's deactivation, the flush_rewrite_rules that we perform afterwards won't properly flush those new rules.
if ( YoastSEO()->classes->get( Deactivating_Yoast_Seo_Conditional::class )->is_met() ) {
return;
}
add_action( 'init', [ $this, 'init' ], 1 );
add_filter( 'redirect_canonical', [ $this, 'redirect_canonical' ] );
add_action( 'template_redirect', [ $this, 'template_redirect' ], 0 );
}
/**
* Sets up rewrite rules.
*/
public function init() {
global $wp;
$wp->add_query_var( 'sitemap' );
$wp->add_query_var( 'sitemap_n' );
$wp->add_query_var( 'yoast-sitemap-xsl' );
add_rewrite_rule( 'sitemap_index\.xml$', 'index.php?sitemap=1', 'top' );
add_rewrite_rule( '([^/]+?)-sitemap([0-9]+)?\.xml$', 'index.php?sitemap=$matches[1]&sitemap_n=$matches[2]', 'top' );
add_rewrite_rule( '([a-z]+)?-?sitemap\.xsl$', 'index.php?yoast-sitemap-xsl=$matches[1]', 'top' );
}
/**
* Stop trailing slashes on sitemap.xml URLs.
*
* @param string $redirect The redirect URL currently determined.
*
* @return bool|string
*/
public function redirect_canonical( $redirect ) {
if ( get_query_var( 'sitemap' ) || get_query_var( 'yoast-sitemap-xsl' ) ) {
return false;
}
return $redirect;
}
/**
* Redirects sitemap.xml to sitemap_index.xml.
*/
public function template_redirect() {
if ( ! $this->needs_sitemap_index_redirect() ) {
return;
}
wp_safe_redirect( home_url( '/sitemap_index.xml' ), 301, 'Yoast SEO' );
exit;
}
/**
* Checks whether the current request needs to be redirected to sitemap_index.xml.
*
* @global WP_Query $wp_query Current query.
*
* @return bool True if redirect is needed, false otherwise.
*/
public function needs_sitemap_index_redirect() {
global $wp_query;
$protocol = 'http://';
if ( ! empty( $_SERVER['HTTPS'] ) && $_SERVER['HTTPS'] === 'on' ) {
$protocol = 'https://';
}
$domain = '';
if ( isset( $_SERVER['SERVER_NAME'] ) ) {
$domain = sanitize_text_field( wp_unslash( $_SERVER['SERVER_NAME'] ) );
}
$path = '';
if ( isset( $_SERVER['REQUEST_URI'] ) ) {
$path = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) );
}
// Due to different environment configurations, we need to check both SERVER_NAME and HTTP_HOST.
$check_urls = [ $protocol . $domain . $path ];
if ( ! empty( $_SERVER['HTTP_HOST'] ) ) {
$check_urls[] = $protocol . sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) . $path;
}
return $wp_query->is_404 && in_array( home_url( '/sitemap.xml' ), $check_urls, true );
}
/**
* Create base URL for the sitemap.
*
* @param string $page Page to append to the base URL.
*
* @return string base URL (incl page)
*/
public static function get_base_url( $page ) {
global $wp_rewrite;
$base = $wp_rewrite->using_index_permalinks() ? 'index.php/' : '/';
/**
* Filter the base URL of the sitemaps.
*
* @param string $base The string that should be added to home_url() to make the full base URL.
*/
$base = apply_filters( 'wpseo_sitemaps_base_url', $base );
/*
* Get the scheme from the configured home URL instead of letting WordPress
* determine the scheme based on the requested URI.
*/
return home_url( $base . $page, wp_parse_url( get_option( 'home' ), PHP_URL_SCHEME ) );
}
}

View File

@ -0,0 +1,643 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\XML_Sitemaps
*/
/**
* Class WPSEO_Sitemaps.
*
* @todo This class could use a general description with some explanation on sitemaps. OR.
*/
class WPSEO_Sitemaps {
/**
* Sitemap index identifier.
*
* @var string
*/
const SITEMAP_INDEX_TYPE = '1';
/**
* Content of the sitemap to output.
*
* @var string
*/
protected $sitemap = '';
/**
* Flag to indicate if this is an invalid or empty sitemap.
*
* @var bool
*/
public $bad_sitemap = false;
/**
* Whether or not the XML sitemap was served from a transient or not.
*
* @var bool
*/
private $transient = false;
/**
* HTTP protocol to use in headers.
*
* @since 3.2
*
* @var string
*/
protected $http_protocol = 'HTTP/1.1';
/**
* Holds the n variable.
*
* @var int
*/
private $current_page = 1;
/**
* The sitemaps router.
*
* @since 3.2
*
* @var WPSEO_Sitemaps_Router
*/
public $router;
/**
* The sitemap renderer.
*
* @since 3.2
*
* @var WPSEO_Sitemaps_Renderer
*/
public $renderer;
/**
* The sitemap cache.
*
* @since 3.2
*
* @var WPSEO_Sitemaps_Cache
*/
public $cache;
/**
* The sitemap providers.
*
* @since 3.2
*
* @var WPSEO_Sitemap_Provider[]
*/
public $providers;
/**
* Class constructor.
*/
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' ] );
$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'] ) );
}
}
/**
* Initialize sitemap providers classes.
*
* @since 5.3
*/
public function init_sitemaps_providers() {
$this->providers = [
new WPSEO_Post_Type_Sitemap_Provider(),
new WPSEO_Taxonomy_Sitemap_Provider(),
new WPSEO_Author_Sitemap_Provider(),
];
$external_providers = apply_filters( 'wpseo_sitemaps_providers', [] );
foreach ( $external_providers as $provider ) {
if ( is_object( $provider ) && $provider instanceof WPSEO_Sitemap_Provider ) {
$this->providers[] = $provider;
}
}
}
/**
* Check the current request URI, if we can determine it's probably an XML sitemap, kill loading the widgets.
*/
public function reduce_query_load() {
if ( ! isset( $_SERVER['REQUEST_URI'] ) ) {
return;
}
$request_uri = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) );
$extension = substr( $request_uri, -4 );
if ( stripos( $request_uri, 'sitemap' ) !== false && in_array( $extension, [ '.xml', '.xsl' ], true ) ) {
remove_all_actions( 'widgets_init' );
}
}
/**
* Register your own sitemap. Call this during 'init'.
*
* @param string $name The name of the sitemap.
* @param callback $building_function Function to build your sitemap.
* @param string $rewrite Optional. Regular expression to match your sitemap with.
*/
public function register_sitemap( $name, $building_function, $rewrite = '' ) {
add_action( 'wpseo_do_sitemap_' . $name, $building_function );
if ( ! empty( $rewrite ) ) {
add_rewrite_rule( $rewrite, 'index.php?sitemap=' . $name, 'top' );
}
}
/**
* Register your own XSL file. Call this during 'init'.
*
* @since 1.4.23
*
* @param string $name The name of the XSL file.
* @param callback $building_function Function to build your XSL file.
* @param string $rewrite Optional. Regular expression to match your sitemap with.
*/
public function register_xsl( $name, $building_function, $rewrite = '' ) {
add_action( 'wpseo_xsl_' . $name, $building_function );
if ( ! empty( $rewrite ) ) {
add_rewrite_rule( $rewrite, 'index.php?yoast-sitemap-xsl=' . $name, 'top' );
}
}
/**
* Set the sitemap current page to allow creating partial sitemaps with WP-CLI
* in a one-off process.
*
* @param int $current_page The part that should be generated.
*/
public function set_n( $current_page ) {
if ( is_scalar( $current_page ) && intval( $current_page ) > 0 ) {
$this->current_page = intval( $current_page );
}
}
/**
* Set the sitemap content to display after you have generated it.
*
* @param string $sitemap The generated sitemap to output.
*/
public function set_sitemap( $sitemap ) {
$this->sitemap = $sitemap;
}
/**
* Set as true to make the request 404. Used stop the display of empty sitemaps or invalid requests.
*
* @param bool $is_bad Is this a bad request. True or false.
*/
public function set_bad_sitemap( $is_bad ) {
$this->bad_sitemap = (bool) $is_bad;
}
/**
* Prevent stupid plugins from running shutdown scripts when we're obviously not outputting HTML.
*
* @since 1.4.16
*/
public function sitemap_close() {
remove_all_actions( 'wp_footer' );
die();
}
/**
* Hijack requests for potential sitemaps and XSL files.
*
* @param \WP_Query $query Main query instance.
*/
public function redirect( $query ) {
if ( ! $query->is_main_query() ) {
return;
}
$yoast_sitemap_xsl = get_query_var( 'yoast-sitemap-xsl' );
if ( ! empty( $yoast_sitemap_xsl ) ) {
/*
* This is a method to provide the XSL via the home_url.
* Needed when the site_url and home_url are not the same.
* Loading the XSL needs to come from the same domain, protocol and port as the XML.
*
* Whenever home_url and site_url are the same, the file can be loaded directly.
*/
$this->xsl_output( $yoast_sitemap_xsl );
$this->sitemap_close();
return;
}
$type = get_query_var( 'sitemap' );
if ( empty( $type ) ) {
return;
}
if ( get_query_var( 'sitemap_n' ) === '1' || get_query_var( 'sitemap_n' ) === '0' ) {
wp_safe_redirect( home_url( "/$type-sitemap.xml" ), 301, 'Yoast SEO' );
exit;
}
$this->set_n( get_query_var( 'sitemap_n' ) );
if ( ! $this->get_sitemap_from_cache( $type, $this->current_page ) ) {
$this->build_sitemap( $type );
}
if ( $this->bad_sitemap ) {
$query->set_404();
status_header( 404 );
return;
}
$this->output();
$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.
*
* Sets $bad_sitemap if this isn't for the root sitemap, a post type or taxonomy.
*
* @param string $type The requested sitemap's identifier.
*/
public function build_sitemap( $type ) {
/**
* Filter the type of sitemap to build.
*
* @param string $type Sitemap type, determined by the request.
*/
$type = apply_filters( 'wpseo_build_sitemap_post_type', $type );
if ( $type === '1' ) {
$this->build_root_map();
return;
}
$entries_per_page = $this->get_entries_per_page();
foreach ( $this->providers as $provider ) {
if ( ! $provider->handles_type( $type ) ) {
continue;
}
try {
$links = $provider->get_sitemap_links( $type, $entries_per_page, $this->current_page );
} catch ( OutOfBoundsException $exception ) {
$this->bad_sitemap = true;
return;
}
$this->sitemap = $this->renderer->get_sitemap( $links, $type, $this->current_page );
return;
}
if ( has_action( 'wpseo_do_sitemap_' . $type ) ) {
/**
* Fires custom handler, if hooked to generate sitemap for the type.
*/
do_action( 'wpseo_do_sitemap_' . $type );
return;
}
$this->bad_sitemap = true;
}
/**
* Build the root sitemap (example.com/sitemap_index.xml) which lists sub-sitemaps for other content types.
*/
public function build_root_map() {
$links = [];
$entries_per_page = $this->get_entries_per_page();
foreach ( $this->providers as $provider ) {
$links = array_merge( $links, $provider->get_index_links( $entries_per_page ) );
}
/**
* Filter the sitemap links array before the index sitemap is built.
*
* @param array $links Array of sitemap links
*/
$links = apply_filters( 'wpseo_sitemap_index_links', $links );
if ( empty( $links ) ) {
$this->bad_sitemap = true;
$this->sitemap = '';
return;
}
$this->sitemap = $this->renderer->get_index( $links );
}
/**
* Spits out the XSL for the XML sitemap.
*
* @since 1.4.13
*
* @param string $type Type to output.
*/
public function xsl_output( $type ) {
if ( $type !== 'main' ) {
/**
* Fires for the output of XSL for XML sitemaps, other than type "main".
*/
do_action( 'wpseo_xsl_' . $type );
return;
}
header( $this->http_protocol . ' 200 OK', true, 200 );
// Prevent the search engines from indexing the XML Sitemap.
header( 'X-Robots-Tag: noindex, follow', true );
header( 'Content-Type: text/xml' );
// Make the browser cache this file properly.
$expires = YEAR_IN_SECONDS;
header( 'Pragma: public' );
header( 'Cache-Control: max-age=' . $expires );
header( 'Expires: ' . YoastSEO()->helpers->date->format_timestamp( ( time() + $expires ), 'D, d M Y H:i:s' ) . ' GMT' );
// Don't use WP_Filesystem() here because that's not initialized yet. See https://yoast.atlassian.net/browse/QAK-2043.
readfile( WPSEO_PATH . 'css/main-sitemap.xsl' );
}
/**
* Spit out the generated sitemap.
*/
public function output() {
$this->send_headers();
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Escaping sitemap as either xml or html results in empty document.
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
";
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery -- They are prepared on the lines above and a direct query is required.
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 ) );
}
// phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.Found -- Argument is kept for documentation purposes.
/**
* Notify search engines of the updated sitemap.
*
* @deprecated 19.2
*
* @codeCoverageIgnore
*
* @param string|null $url Optional URL to make the ping for.
*/
public static function ping_search_engines( $url = null ) {
_deprecated_function( __METHOD__, 'Yoast SEO 19.2', 'WPSEO_Sitemaps_Admin::ping_search_engines' );
$admin = new WPSEO_Sitemaps_Admin();
$admin->ping_search_engines();
}
// phpcs:enable
/**
* Get the maximum number of entries per XML sitemap.
*
* @return int The maximum number of entries.
*/
protected function get_entries_per_page() {
/**
* Filter the maximum number of entries per XML sitemap.
*
* After changing the output of the filter, make sure that you disable and enable the
* sitemaps to make sure the value is picked up for the sitemap cache.
*
* @param int $entries The maximum number of entries per XML sitemap.
*/
$entries = (int) apply_filters( 'wpseo_sitemap_entries_per_page', 1000 );
return $entries;
}
/**
* Get post statuses for post_type or the root sitemap.
*
* @since 10.2
*
* @param string $type Provide a type for a post_type sitemap, SITEMAP_INDEX_TYPE for the root sitemap.
*
* @return array List of post statuses.
*/
public static function get_post_statuses( $type = self::SITEMAP_INDEX_TYPE ) {
/**
* Filter post status list for sitemap query for the post type.
*
* @param array $post_statuses Post status list, defaults to array( 'publish' ).
* @param string $type Post type or SITEMAP_INDEX_TYPE.
*/
$post_statuses = apply_filters( 'wpseo_sitemap_post_statuses', [ 'publish' ], $type );
if ( ! is_array( $post_statuses ) || empty( $post_statuses ) ) {
$post_statuses = [ 'publish' ];
}
if ( ( $type === self::SITEMAP_INDEX_TYPE || $type === 'attachment' )
&& ! in_array( 'inherit', $post_statuses, true )
) {
$post_statuses[] = 'inherit';
}
return $post_statuses;
}
/**
* Sends all the required HTTP Headers.
*/
private function send_headers() {
if ( headers_sent() ) {
return;
}
$headers = [
$this->http_protocol . ' 200 OK' => 200,
// Prevent the search engines from indexing the XML Sitemap.
'X-Robots-Tag: noindex, follow' => '',
'Content-Type: text/xml; charset=' . esc_attr( $this->renderer->get_output_charset() ) => '',
];
/**
* Filter the HTTP headers we send before an XML sitemap.
*
* @param array $headers The HTTP headers we're going to send out.
*/
$headers = apply_filters( 'wpseo_sitemap_http_headers', $headers );
foreach ( $headers as $header => $status ) {
if ( is_numeric( $status ) ) {
header( $header, true, $status );
continue;
}
header( $header, true );
}
}
}

View File

@ -0,0 +1,323 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\XML_Sitemaps
*/
/**
* 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 );
}
/**
* Check if provider supports given item type.
*
* @param string $type Type string to check for.
*
* @return bool
*/
public function handles_type( $type ) {
$taxonomy = get_taxonomy( $type );
if ( $taxonomy === false || ! $this->is_valid_taxonomy( $taxonomy->name ) || ! $taxonomy->public ) {
return false;
}
return true;
}
/**
* Retrieves the links for the sitemap.
*
* @param int $max_entries Entries per sitemap.
*
* @return array
*/
public function get_index_links( $max_entries ) {
$taxonomies = get_taxonomies( [ 'public' => true ], 'objects' );
if ( empty( $taxonomies ) ) {
return [];
}
$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.
/**
* Filter the setting of excluding empty terms from the XML sitemap.
*
* @param bool $exclude Defaults to true.
* @param array $taxonomy_names Array of names for the taxonomies being processed.
*/
$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 );
$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;
}
}
$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;
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 = ( $page_counter === 0 ) ? '' : ( $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;
}
/**
* 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
*
* @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;
}
$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' );
}
$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;
}
$canonical = WPSEO_Taxonomy_Meta::get_term_meta( $term, $term->taxonomy, 'canonical' );
$url['loc'] = get_term_link( $term, $term->taxonomy );
if ( is_string( $canonical ) && $canonical !== '' && $canonical !== $url['loc'] ) {
continue;
}
$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;
/** 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;
}
/**
* Check if taxonomy by name is valid to appear in sitemaps.
*
* @param string $taxonomy_name Taxonomy name to check.
*
* @return bool
*/
public function is_valid_taxonomy( $taxonomy_name ) {
if ( WPSEO_Options::get( "noindex-tax-{$taxonomy_name}" ) === true ) {
return false;
}
if ( in_array( $taxonomy_name, [ 'link_category', 'nav_menu' ], true ) ) {
return false;
}
if ( $taxonomy_name === 'post_format' && WPSEO_Options::get( 'disable-post_format', false ) ) {
return false;
}
/**
* 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, $taxonomy_name ) ) {
return false;
}
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;
}
}

View File

@ -0,0 +1,72 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\XML_Sitemaps
*/
/**
* Cache Data interface.
*/
interface WPSEO_Sitemap_Cache_Data_Interface {
/**
* Status for normal, usable sitemap.
*
* @var string
*/
const OK = 'ok';
/**
* Status for unusable sitemap.
*
* @var string
*/
const ERROR = 'error';
/**
* Status for unusable sitemap because it cannot be identified.
*
* @var string
*/
const UNKNOWN = 'unknown';
/**
* Set the content of the sitemap.
*
* @param string $sitemap The XML content of the sitemap.
*
* @return void
*/
public function set_sitemap( $sitemap );
/**
* Set the status of the sitemap.
*
* @param bool|string $usable True/False or 'ok'/'error' for status.
*
* @return void
*/
public function set_status( $usable );
/**
* Builds the sitemap.
*
* @return string The XML content of the sitemap.
*/
public function get_sitemap();
/**
* Get the status of this sitemap.
*
* @return string Status 'ok', 'error' or 'unknown'.
*/
public function get_status();
/**
* Is the sitemap content usable ?
*
* @return bool True if the sitemap is usable, False if not.
*/
public function is_usable();
}

View File

@ -0,0 +1,41 @@
<?php
/**
* WPSEO plugin file.
*
* @package WPSEO\XML_Sitemaps
*/
/**
* Sitemap Provider interface.
*/
interface WPSEO_Sitemap_Provider {
/**
* Check if provider supports given item type.
*
* @param string $type Type string to check for.
*
* @return bool
*/
public function handles_type( $type );
/**
* Get set of sitemaps index link data.
*
* @param int $max_entries Entries per sitemap.
*
* @return array
*/
public function get_index_links( $max_entries );
/**
* 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 );
}