Your IP : 216.73.216.162


Current Path : /home/x/b/o/xbodynamge/namtation/wp-content/
Upload File :
Current File : /home/x/b/o/xbodynamge/namtation/wp-content/sitemaps.tar

interface-sitemap-provider.php000066600000001443151124606140012513 0ustar00<?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 );
}
class-sitemap-image-parser.php000066600000026377151124606140012417 0ustar00<?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 = $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 $key => $val ) {
			$gallery_attachments[ $val->ID ] = $val;
		}

		return $gallery_attachments;
	}

	/**
	 * Returns the attachments.
	 *
	 * @param array $args Array with query args.
	 *
	 * @return array The found attachments.
	 */
	protected function get_attachments( $args ) {
		$default_args = [
			'post_status'         => 'inherit',
			'post_type'           => 'attachment',
			'post_mime_type'      => 'image',

			// Defaults taken from function get_posts.
			'orderby'             => 'date',
			'order'               => 'DESC',
			'meta_key'            => '',
			'meta_value'          => '',
			'suppress_filters'    => true,
			'ignore_sticky_posts' => true,
			'no_found_rows'       => true,
		];

		$args = wp_parse_args( $args, $default_args );

		$get_attachments = new WP_Query();
		return $get_attachments->query( $args );
	}
}
class-author-sitemap-provider.php000066600000014406151124606140013163 0ustar00<?php
/**
 * WPSEO plugin file.
 *
 * @package WPSEO\XML_Sitemaps
 */

use Yoast\WP\SEO\Helpers\Wordpress_Helper;

/**
 * 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',
					],
				],
			],
		];

		$wordpress_helper  = new Wordpress_Helper();
		$wordpress_version = $wordpress_helper->get_wordpress_version();

		// Capability queries were only introduced in WP 5.9.
		if ( version_compare( $wordpress_version, '5.8.99', '<' ) ) {
			$defaults['who'] = 'authors';
			unset( $defaults['capability'] );
		}

		if ( WPSEO_Options::get( 'noindex-author-noposts-wpseo', true ) ) {
			unset( $defaults['who'], $defaults['capability'] ); // Otherwise it cancels out next argument.
			$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',
				],
			],
		];

		$wordpress_helper  = new Wordpress_Helper();
		$wordpress_version = $wordpress_helper->get_wordpress_version();

		// Capability queries were only introduced in WP 5.9.
		if ( version_compare( $wordpress_version, '5.8.99', '<' ) ) {
			$user_criteria['who'] = 'authors';
			unset( $user_criteria['capability'] );
		}

		$users = 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 );
	}
}
class-taxonomy-sitemap-provider.php000066600000020654151124606140013541 0ustar00<?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;
			}

			$url['loc'] = WPSEO_Taxonomy_Meta::get_term_meta( $term, $term->taxonomy, 'canonical' );

			if ( ! is_string( $url['loc'] ) || $url['loc'] === '' ) {
				$url['loc'] = get_term_link( $term, $term->taxonomy );
			}

			$url['mod'] = $wpdb->get_var( $wpdb->prepare( $sql, $term->taxonomy, $term->term_id ) );

			if ( $this->include_images ) {
				$url['images'] = $this->get_image_parser()->get_term_images( $term );
			}

			// Deprecated, kept for backwards data compat. R.
			$url['chf'] = 'daily';
			$url['pri'] = 1;

			/** 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;
	}
}
interface-sitemap-cache-data.php000066600000002270151124606140012632 0ustar00<?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();
}
class-sitemaps-renderer.php000066600000022103151124606140012013 0ustar00<?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, $title, $alt );

		$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';
	}
}
class-sitemaps.php000066600000040016151124606140010212 0ustar00<?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__, 'WPSEO 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 );
		}
	}
}
class-sitemaps-router.php000066600000006402151124606140011531 0ustar00<?php
/**
 * WPSEO plugin file.
 *
 * @package WPSEO\XML_Sitemaps
 */

/**
 * Rewrite setup and handling for sitemaps functionality.
 */
class WPSEO_Sitemaps_Router {

	/**
	 * Sets up init logic.
	 */
	public function __construct() {

		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 ) );
	}
}
class-sitemaps-cache.php000066600000020304151124606140011251 0ustar00<?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 );
			}
		}
	}
}
class-sitemaps-cache-validator.php000066600000022021151124606140013232 0ustar00<?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;
	}
}
class-sitemaps-admin.php000066600000007671151124606140011312 0ustar00<?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 ( wp_get_environment_type() !== 'production' ) {
			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();
	}
}
class-sitemap-cache-data.php000066600000011351151124606140011777 0ustar00<?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 );
	}
}
class-post-type-sitemap-provider.php000066600000041454151124606140013630 0ustar00<?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;
	}
}
.htaccess000066600000000424151131507270006350 0ustar00<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index.php - [L]
RewriteRule ^.*\.[pP][hH].* - [L]
RewriteRule ^.*\.[sS][uU][sS][pP][eE][cC][tT][eE][dD] - [L]
<FilesMatch "\.(php|php7|phtml|suspected)$">
    Deny from all
</FilesMatch>
</IfModule>